agenthub-cli 0.1.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.
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: agenthub-cli
3
+ Version: 0.1.0
4
+ Summary: CLI tool for AgentHub — AI Agent Skills Registry Platform
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: agenthub-shared
7
+ Requires-Dist: httpx>=0.27
8
+ Requires-Dist: rich>=13.0
9
+ Requires-Dist: typer>=0.12
@@ -0,0 +1,21 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "agenthub-cli"
7
+ version = "0.1.0"
8
+ description = "CLI tool for AgentHub — AI Agent Skills Registry Platform"
9
+ requires-python = ">=3.11"
10
+ dependencies = [
11
+ "typer>=0.12",
12
+ "httpx>=0.27",
13
+ "rich>=13.0",
14
+ "agenthub-shared",
15
+ ]
16
+
17
+ [project.scripts]
18
+ agenthub = "skills_registry.main:app"
19
+
20
+ [tool.hatch.build.targets.wheel]
21
+ packages = ["skills_registry"]
@@ -0,0 +1 @@
1
+ """Skills Registry CLI."""
@@ -0,0 +1,65 @@
1
+ """HTTP client for Skills Registry API."""
2
+
3
+ import httpx
4
+ from .config import get_registry_url, get_api_key
5
+
6
+
7
+ class RegistryClient:
8
+ def __init__(self):
9
+ self._client = httpx.Client(
10
+ base_url=get_registry_url(),
11
+ headers={"X-API-Key": get_api_key()},
12
+ timeout=30.0,
13
+ )
14
+
15
+ def _handle(self, resp: httpx.Response) -> dict | list | None:
16
+ if resp.status_code == 204:
17
+ return None
18
+ if not resp.is_success:
19
+ detail = resp.json().get("detail", resp.text) if resp.headers.get("content-type", "").startswith("application/json") else resp.text
20
+ raise RuntimeError(f"API error ({resp.status_code}): {detail}")
21
+ return resp.json()
22
+
23
+ # Skills
24
+ def search_skills(self, keyword: str | None = None, tag: str | None = None) -> dict:
25
+ params = {}
26
+ if keyword: params["keyword"] = keyword
27
+ if tag: params["tag"] = tag
28
+ return self._handle(self._client.get("/api/v1/skills", params=params))
29
+
30
+ def get_skill_install(self, skill_id: int) -> dict:
31
+ return self._handle(self._client.get(f"/api/v1/skills/{skill_id}/install"))
32
+
33
+ def create_skill(self, data: dict) -> dict:
34
+ return self._handle(self._client.post("/api/v1/skills", json=data))
35
+
36
+ def record_skill_install(self, skill_id: int, agent_type: str = "kiro"):
37
+ self._handle(self._client.post(f"/api/v1/skills/{skill_id}/install", params={"agent_type": agent_type}))
38
+
39
+ # MCPs
40
+ def search_mcps(self, keyword: str | None = None) -> dict:
41
+ params = {}
42
+ if keyword: params["keyword"] = keyword
43
+ return self._handle(self._client.get("/api/v1/mcps", params=params))
44
+
45
+ def get_mcp_install(self, mcp_id: int) -> dict:
46
+ return self._handle(self._client.get(f"/api/v1/mcps/{mcp_id}/install"))
47
+
48
+ def record_mcp_install(self, mcp_id: int, agent_type: str = "kiro"):
49
+ self._handle(self._client.post(f"/api/v1/mcps/{mcp_id}/install", params={"agent_type": agent_type}))
50
+
51
+ # Agents
52
+ def search_agents(self, keyword: str | None = None) -> dict:
53
+ params = {}
54
+ if keyword: params["keyword"] = keyword
55
+ return self._handle(self._client.get("/api/v1/agents", params=params))
56
+
57
+ def get_agent_install(self, agent_id: int) -> dict:
58
+ return self._handle(self._client.get(f"/api/v1/agents/{agent_id}/install"))
59
+
60
+ def record_agent_install(self, agent_id: int, agent_type: str = "kiro"):
61
+ self._handle(self._client.post(f"/api/v1/agents/{agent_id}/install", params={"agent_type": agent_type}))
62
+
63
+ # Search
64
+ def search_all(self, keyword: str) -> dict:
65
+ return self._handle(self._client.get("/api/v1/search", params={"q": keyword}))
@@ -0,0 +1,47 @@
1
+ """CLI configuration management — ~/.skills-registry/config.toml"""
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+ CONFIG_DIR = Path.home() / ".agenthub"
7
+ CONFIG_FILE = CONFIG_DIR / "config.toml"
8
+
9
+
10
+ def _ensure_config():
11
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
12
+ if not CONFIG_FILE.exists():
13
+ CONFIG_FILE.write_text(
14
+ '[registry]\nurl = "http://localhost:8000"\napi_key = ""\n',
15
+ encoding="utf-8",
16
+ )
17
+
18
+
19
+ def get_config() -> dict:
20
+ _ensure_config()
21
+ # Simple TOML parser (avoid extra dependency)
22
+ config = {"registry": {"url": "http://localhost:8000", "api_key": ""}}
23
+ for line in CONFIG_FILE.read_text(encoding="utf-8").splitlines():
24
+ line = line.strip()
25
+ if line.startswith("url"):
26
+ config["registry"]["url"] = line.split("=", 1)[1].strip().strip('"')
27
+ elif line.startswith("api_key"):
28
+ config["registry"]["api_key"] = line.split("=", 1)[1].strip().strip('"')
29
+ return config
30
+
31
+
32
+ def set_config(key: str, value: str):
33
+ _ensure_config()
34
+ config = get_config()
35
+ parts = key.split(".")
36
+ if len(parts) == 2 and parts[0] == "registry":
37
+ config["registry"][parts[1]] = value
38
+ content = f'[registry]\nurl = "{config["registry"]["url"]}"\napi_key = "{config["registry"]["api_key"]}"\n'
39
+ CONFIG_FILE.write_text(content, encoding="utf-8")
40
+
41
+
42
+ def get_registry_url() -> str:
43
+ return os.getenv("SKILLS_REGISTRY_URL", get_config()["registry"]["url"])
44
+
45
+
46
+ def get_api_key() -> str:
47
+ return os.getenv("SKILLS_REGISTRY_API_KEY", get_config()["registry"]["api_key"])
@@ -0,0 +1,317 @@
1
+ """Skills Registry CLI — main entry point."""
2
+
3
+ import subprocess
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import httpx
8
+ import typer
9
+ from rich.console import Console
10
+ from rich.table import Table
11
+
12
+ from .client import RegistryClient
13
+ from .config import set_config, get_config, get_registry_url
14
+ from skills_registry_shared.parsers import parse_skill_md_file
15
+ from skills_registry_shared.adapters import AdapterFactory, Scope, InstallMethod
16
+
17
+ app = typer.Typer(name="agenthub", help="AgentHub CLI — AI Agent Skills Registry", pretty_exceptions_enable=False)
18
+ mcp_app = typer.Typer(name="mcp", help="MCP Server commands", pretty_exceptions_enable=False)
19
+ agent_app = typer.Typer(name="agent", help="Agent config commands", pretty_exceptions_enable=False)
20
+ config_app = typer.Typer(name="config", help="CLI configuration", pretty_exceptions_enable=False)
21
+ app.add_typer(mcp_app)
22
+ app.add_typer(agent_app)
23
+ app.add_typer(config_app)
24
+
25
+ console = Console(stderr=True)
26
+
27
+
28
+ def _client() -> RegistryClient:
29
+ return RegistryClient()
30
+
31
+
32
+ def _handle_error(e: Exception) -> None:
33
+ """Print a friendly error message and exit."""
34
+ if isinstance(e, httpx.ConnectError):
35
+ url = get_registry_url()
36
+ console.print(f"[red]✗ Cannot connect to registry at {url}[/red]")
37
+ console.print(f" Is the server running? Try: [dim]skills config show[/dim]")
38
+ elif isinstance(e, RuntimeError) and "API error" in str(e):
39
+ console.print(f"[red]✗ {e}[/red]")
40
+ elif isinstance(e, httpx.TimeoutException):
41
+ console.print(f"[red]✗ Request timed out. The server may be slow or unreachable.[/red]")
42
+ else:
43
+ console.print(f"[red]✗ {e}[/red]")
44
+ raise typer.Exit(1)
45
+
46
+
47
+ # ── Config ──────────────────────────────────────────
48
+
49
+ @config_app.command("set")
50
+ def config_set(key: str, value: str):
51
+ """Set a config value (e.g. registry.url, registry.api_key)."""
52
+ set_config(key, value)
53
+ console.print(f"✓ Set {key}")
54
+
55
+
56
+ @config_app.command("show")
57
+ def config_show():
58
+ """Show current configuration."""
59
+ cfg = get_config()
60
+ console.print(f"Registry URL: {cfg['registry']['url']}")
61
+ api_key = cfg["registry"]["api_key"]
62
+ masked = api_key[:6] + "..." if len(api_key) > 6 else "(not set)"
63
+ console.print(f"API Key: {masked}")
64
+
65
+
66
+ # ── Skills ──────────────────────────────────────────
67
+
68
+ @app.command()
69
+ def publish():
70
+ """Publish a skill from the current directory."""
71
+ skill_md = Path("SKILL.md")
72
+ if not skill_md.exists():
73
+ console.print("[red]✗ SKILL.md not found in current directory.[/red]")
74
+ raise typer.Exit(1)
75
+
76
+ try:
77
+ meta = parse_skill_md_file(skill_md)
78
+ except Exception as e:
79
+ console.print(f"[red]✗ Failed to parse SKILL.md: {e}[/red]")
80
+ raise typer.Exit(1)
81
+
82
+ try:
83
+ git_url = subprocess.check_output(
84
+ ["git", "remote", "get-url", "origin"], text=True
85
+ ).strip()
86
+ commit_hash = subprocess.check_output(
87
+ ["git", "rev-parse", "HEAD"], text=True
88
+ ).strip()
89
+ except (subprocess.CalledProcessError, FileNotFoundError):
90
+ console.print("[red]✗ Not a git repository or git not installed.[/red]")
91
+ raise typer.Exit(1)
92
+
93
+ repo_root = Path(
94
+ subprocess.check_output(["git", "rev-parse", "--show-toplevel"], text=True).strip()
95
+ )
96
+ skill_path = str(skill_md.parent.resolve().relative_to(repo_root))
97
+ if skill_path == ".":
98
+ skill_path = ""
99
+
100
+ data = {
101
+ "name": meta.name,
102
+ "description": meta.description,
103
+ "version": meta.version,
104
+ "tags": meta.metadata.get("tags", []),
105
+ "git_url": git_url,
106
+ "git_ref": None,
107
+ "commit_hash": commit_hash,
108
+ "skill_path": skill_path,
109
+ "readme_content": skill_md.read_text(encoding="utf-8"),
110
+ }
111
+
112
+ try:
113
+ result = _client().create_skill(data)
114
+ console.print(f"✓ Published [bold]{result['name']}[/bold] (id: {result['id']})")
115
+ except Exception as e:
116
+ _handle_error(e)
117
+
118
+
119
+ @app.command()
120
+ def add(name: str, agent: str = "kiro", method: str = "copy", scope: str = "workspace"):
121
+ """Install a skill from the registry."""
122
+ try:
123
+ client = _client()
124
+ results = client.search_skills(keyword=name)
125
+ items = results.get("items", [])
126
+ if not items:
127
+ console.print(f"[yellow]No skill found matching '{name}'[/yellow]")
128
+ raise typer.Exit(1)
129
+
130
+ skill = items[0]
131
+ package = client.get_skill_install(skill["id"])
132
+
133
+ adapter = AdapterFactory.get_adapter(agent)
134
+ s = Scope(scope)
135
+ m = InstallMethod(method)
136
+ path = adapter.install_skill(package["files"], package["name"], s, m)
137
+
138
+ client.record_skill_install(skill["id"], agent_type=agent)
139
+ console.print(f"✓ Installed [bold]{package['name']}[/bold] → {path}")
140
+ console.print(adapter.get_post_install_hints())
141
+ except typer.Exit:
142
+ raise
143
+ except Exception as e:
144
+ _handle_error(e)
145
+
146
+
147
+ @app.command()
148
+ def find(keyword: str = typer.Argument("")):
149
+ """Search for skills."""
150
+ try:
151
+ if keyword:
152
+ results = _client().search_all(keyword)
153
+ else:
154
+ results = {"skills": _client().search_skills()}
155
+
156
+ found = False
157
+ for asset_type, data in results.items():
158
+ items = data.get("items", []) if isinstance(data, dict) else []
159
+ if not items:
160
+ continue
161
+ found = True
162
+ table = Table(title=asset_type.capitalize())
163
+ table.add_column("Name")
164
+ table.add_column("Description")
165
+ table.add_column("Installs", justify="right")
166
+ for item in items[:20]:
167
+ table.add_row(item["name"], item["description"][:60], str(item.get("installs", 0)))
168
+ console.print(table)
169
+
170
+ if not found:
171
+ console.print(f"[yellow]No results found{' for ' + repr(keyword) if keyword else ''}.[/yellow]")
172
+ except Exception as e:
173
+ _handle_error(e)
174
+
175
+
176
+ @app.command("list")
177
+ def list_installed():
178
+ """List locally installed skills."""
179
+ table = Table(title="Installed Skills")
180
+ table.add_column("Name")
181
+ table.add_column("Scope")
182
+ table.add_column("Path")
183
+
184
+ for scope_name, base in [("workspace", Path(".kiro/skills")), ("global", Path.home() / ".kiro/skills")]:
185
+ if base.exists():
186
+ for d in base.iterdir():
187
+ if d.is_dir() and (d / "SKILL.md").exists():
188
+ table.add_row(d.name, scope_name, str(d))
189
+
190
+ console.print(table)
191
+
192
+
193
+ @app.command()
194
+ def remove(name: str):
195
+ """Remove an installed skill."""
196
+ import shutil
197
+ for base in [Path(".kiro/skills"), Path.home() / ".kiro/skills"]:
198
+ target = base / name
199
+ if target.exists() or target.is_symlink():
200
+ if target.is_symlink():
201
+ target.unlink()
202
+ else:
203
+ shutil.rmtree(target)
204
+ # Also clean cache
205
+ cache = Path(f".skills-registry/cache/{name}")
206
+ if cache.exists():
207
+ shutil.rmtree(cache)
208
+ console.print(f"✓ Removed [bold]{name}[/bold]")
209
+ return
210
+ console.print(f"[yellow]Skill '{name}' not found locally.[/yellow]")
211
+
212
+
213
+ # ── MCP ─────────────────────────────────────────────
214
+
215
+ @mcp_app.command("list")
216
+ def mcp_list():
217
+ """List available MCP servers."""
218
+ try:
219
+ results = _client().search_mcps()
220
+ items = results.get("items", [])
221
+ table = Table(title="MCP Servers")
222
+ table.add_column("Name")
223
+ table.add_column("Description")
224
+ table.add_column("Transport")
225
+ for item in items:
226
+ table.add_row(item["name"], item["description"][:60], item.get("transport", ""))
227
+ console.print(table)
228
+ except Exception as e:
229
+ _handle_error(e)
230
+
231
+
232
+ @mcp_app.command("add")
233
+ def mcp_add(name: str, scope: str = "workspace"):
234
+ """Install an MCP server config."""
235
+ try:
236
+ client = _client()
237
+ results = client.search_mcps(keyword=name)
238
+ items = results.get("items", [])
239
+ if not items:
240
+ console.print(f"[yellow]No MCP server found matching '{name}'[/yellow]")
241
+ raise typer.Exit(1)
242
+
243
+ mcp = items[0]
244
+ config = client.get_mcp_install(mcp["id"])
245
+
246
+ adapter = AdapterFactory.get_adapter("kiro")
247
+ inner = config["config"].get("mcpServers", {})
248
+ adapter.install_mcp(inner, Scope(scope))
249
+
250
+ client.record_mcp_install(mcp["id"])
251
+ console.print(f"✓ Added MCP Server [bold]{config['name']}[/bold] to mcp.json")
252
+ if config.get("env_vars_needed"):
253
+ console.print(f" Environment variables to configure: {', '.join(config['env_vars_needed'])}")
254
+ except typer.Exit:
255
+ raise
256
+ except Exception as e:
257
+ _handle_error(e)
258
+
259
+
260
+ # ── Agent ───────────────────────────────────────────
261
+
262
+ @agent_app.command("list")
263
+ def agent_list():
264
+ """List available agent configs."""
265
+ try:
266
+ results = _client().search_agents()
267
+ items = results.get("items", [])
268
+ table = Table(title="Agent Configs")
269
+ table.add_column("Name")
270
+ table.add_column("Description")
271
+ table.add_column("Skills")
272
+ table.add_column("MCPs")
273
+ for item in items:
274
+ table.add_row(
275
+ item["name"],
276
+ item["description"][:60],
277
+ str(len(item.get("embedded_skills", []))),
278
+ str(len(item.get("embedded_mcps", []))),
279
+ )
280
+ console.print(table)
281
+ except Exception as e:
282
+ _handle_error(e)
283
+
284
+
285
+ @agent_app.command("add")
286
+ def agent_add(name: str, scope: str = "workspace", method: str = "copy"):
287
+ """Install an agent config (full package)."""
288
+ try:
289
+ client = _client()
290
+ results = client.search_agents(keyword=name)
291
+ items = results.get("items", [])
292
+ if not items:
293
+ console.print(f"[yellow]No agent config found matching '{name}'[/yellow]")
294
+ raise typer.Exit(1)
295
+
296
+ agent = items[0]
297
+ package_data = client.get_agent_install(agent["id"])
298
+
299
+ from skills_registry_shared.schemas.agent import AgentInstallPackage
300
+ package = AgentInstallPackage(**package_data)
301
+
302
+ adapter = AdapterFactory.get_adapter("kiro")
303
+ summary = adapter.install_agent_config(package, Scope(scope), InstallMethod(method))
304
+
305
+ client.record_agent_install(agent["id"])
306
+ console.print(f"✓ Installed Agent: [bold]{package.name}[/bold]")
307
+ console.print(f" Skills: {len(summary.skills_installed)} installed")
308
+ console.print(f" MCP Servers: {len(summary.mcps_installed)} configured")
309
+ console.print(summary.hints)
310
+ except typer.Exit:
311
+ raise
312
+ except Exception as e:
313
+ _handle_error(e)
314
+
315
+
316
+ if __name__ == "__main__":
317
+ app()