agentpowers 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.
- agentpowers-0.1.0/.gitignore +28 -0
- agentpowers-0.1.0/LICENSE +21 -0
- agentpowers-0.1.0/PKG-INFO +61 -0
- agentpowers-0.1.0/README.md +31 -0
- agentpowers-0.1.0/agentpowers_cli/__init__.py +0 -0
- agentpowers-0.1.0/agentpowers_cli/banner.py +56 -0
- agentpowers-0.1.0/agentpowers_cli/commands/__init__.py +0 -0
- agentpowers-0.1.0/agentpowers_cli/commands/auth.py +274 -0
- agentpowers-0.1.0/agentpowers_cli/commands/claim.py +69 -0
- agentpowers-0.1.0/agentpowers_cli/commands/detail.py +388 -0
- agentpowers-0.1.0/agentpowers_cli/commands/install.py +392 -0
- agentpowers-0.1.0/agentpowers_cli/commands/profile.py +103 -0
- agentpowers-0.1.0/agentpowers_cli/commands/publish.py +239 -0
- agentpowers-0.1.0/agentpowers_cli/commands/republish.py +76 -0
- agentpowers-0.1.0/agentpowers_cli/commands/scan.py +364 -0
- agentpowers-0.1.0/agentpowers_cli/commands/search.py +253 -0
- agentpowers-0.1.0/agentpowers_cli/commands/status.py +134 -0
- agentpowers-0.1.0/agentpowers_cli/commands/uninstall.py +96 -0
- agentpowers-0.1.0/agentpowers_cli/commands/unpublish.py +85 -0
- agentpowers-0.1.0/agentpowers_cli/commands/update.py +553 -0
- agentpowers-0.1.0/agentpowers_cli/commands/verify.py +50 -0
- agentpowers-0.1.0/agentpowers_cli/main.py +46 -0
- agentpowers-0.1.0/agentpowers_cli/services/__init__.py +0 -0
- agentpowers-0.1.0/agentpowers_cli/services/api_client.py +221 -0
- agentpowers-0.1.0/agentpowers_cli/services/auth_store.py +70 -0
- agentpowers-0.1.0/agentpowers_cli/services/config.py +14 -0
- agentpowers-0.1.0/agentpowers_cli/services/content_hasher.py +83 -0
- agentpowers-0.1.0/agentpowers_cli/services/external_installer.py +207 -0
- agentpowers-0.1.0/agentpowers_cli/services/installer.py +183 -0
- agentpowers-0.1.0/agentpowers_cli/services/packager.py +122 -0
- agentpowers-0.1.0/agentpowers_cli/services/pin_manager.py +89 -0
- agentpowers-0.1.0/pyproject.toml +59 -0
- agentpowers-0.1.0/skills/find-skills/SKILL.md +85 -0
- agentpowers-0.1.0/tests/__init__.py +0 -0
- agentpowers-0.1.0/tests/conftest.py +17 -0
- agentpowers-0.1.0/tests/test_api_client.py +180 -0
- agentpowers-0.1.0/tests/test_auth_commands.py +402 -0
- agentpowers-0.1.0/tests/test_auth_store.py +131 -0
- agentpowers-0.1.0/tests/test_banner.py +81 -0
- agentpowers-0.1.0/tests/test_claim.py +223 -0
- agentpowers-0.1.0/tests/test_content_hasher.py +85 -0
- agentpowers-0.1.0/tests/test_detail_command.py +468 -0
- agentpowers-0.1.0/tests/test_external_install.py +384 -0
- agentpowers-0.1.0/tests/test_install_command.py +623 -0
- agentpowers-0.1.0/tests/test_installer_zipslip.py +111 -0
- agentpowers-0.1.0/tests/test_pin_manager.py +104 -0
- agentpowers-0.1.0/tests/test_profile.py +104 -0
- agentpowers-0.1.0/tests/test_publish_command.py +207 -0
- agentpowers-0.1.0/tests/test_publish_display_name_required.py +166 -0
- agentpowers-0.1.0/tests/test_publish_update.py +566 -0
- agentpowers-0.1.0/tests/test_republish_command.py +147 -0
- agentpowers-0.1.0/tests/test_scan_command.py +234 -0
- agentpowers-0.1.0/tests/test_search_command.py +584 -0
- agentpowers-0.1.0/tests/test_status_command.py +163 -0
- agentpowers-0.1.0/tests/test_uninstall_command.py +156 -0
- agentpowers-0.1.0/tests/test_unpublish_command.py +147 -0
- agentpowers-0.1.0/tests/test_update_command.py +1588 -0
- agentpowers-0.1.0/tests/test_verify.py +94 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
.venv/
|
|
8
|
+
|
|
9
|
+
# Environment
|
|
10
|
+
.env
|
|
11
|
+
*.db
|
|
12
|
+
|
|
13
|
+
# IDE
|
|
14
|
+
.vscode/
|
|
15
|
+
.idea/
|
|
16
|
+
|
|
17
|
+
# OS
|
|
18
|
+
.DS_Store
|
|
19
|
+
|
|
20
|
+
# Testing
|
|
21
|
+
.pytest_cache/
|
|
22
|
+
.coverage
|
|
23
|
+
htmlcov/
|
|
24
|
+
|
|
25
|
+
# Ruff
|
|
26
|
+
.ruff_cache/
|
|
27
|
+
.gstack/
|
|
28
|
+
.claude/skills/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AgentPowers
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentpowers
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AgentPowers CLI — discover, install, and publish marketplace skills
|
|
5
|
+
Project-URL: Homepage, https://agentpowers.ai
|
|
6
|
+
Project-URL: Repository, https://github.com/AgentPowers-AI/agentpowers-app
|
|
7
|
+
Project-URL: Documentation, https://docs.agentpowers.ai
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/AgentPowers-AI/agentpowers-app/issues
|
|
9
|
+
Author: Nate Ritter
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agents,claude,cli,marketplace,skills
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Requires-Dist: httpx>=0.28.0
|
|
24
|
+
Requires-Dist: rich>=13.0
|
|
25
|
+
Requires-Dist: typer>=0.15.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: ruff>=0.9.0; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# AgentPowers CLI
|
|
32
|
+
|
|
33
|
+
The `ap` command-line tool for the AgentPowers marketplace.
|
|
34
|
+
|
|
35
|
+
## Status
|
|
36
|
+
|
|
37
|
+
**Phase 1E: Complete** -- 73 passing tests. All core commands implemented including `ap claim`.
|
|
38
|
+
|
|
39
|
+
## Commands
|
|
40
|
+
|
|
41
|
+
- `ap login` -- Open browser for Clerk auth, store JWT locally
|
|
42
|
+
- `ap logout` -- Remove stored credentials
|
|
43
|
+
- `ap whoami` -- Show current user info
|
|
44
|
+
- `ap search <query>` -- Search the marketplace (Rich table output)
|
|
45
|
+
- `ap install <slug> [--code XXXX]` -- Install a skill or agent (with optional license code)
|
|
46
|
+
- `ap publish [--price N] [--dir .] [--category dev]` -- Package and publish a skill or agent
|
|
47
|
+
- `ap claim <slug>` -- Claim ownership of a ClawHub-imported skill
|
|
48
|
+
|
|
49
|
+
## Development
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
cd agentpowers-cli
|
|
53
|
+
uv venv .venv && source .venv/bin/activate
|
|
54
|
+
uv pip install -e ".[dev]"
|
|
55
|
+
ap --help
|
|
56
|
+
pytest tests/ -v # Run tests
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Auth
|
|
60
|
+
|
|
61
|
+
Credentials stored at `~/.agentpowers/auth.json` (permissions: 600). Shared with the Claude Code plugin's MCP server.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# AgentPowers CLI
|
|
2
|
+
|
|
3
|
+
The `ap` command-line tool for the AgentPowers marketplace.
|
|
4
|
+
|
|
5
|
+
## Status
|
|
6
|
+
|
|
7
|
+
**Phase 1E: Complete** -- 73 passing tests. All core commands implemented including `ap claim`.
|
|
8
|
+
|
|
9
|
+
## Commands
|
|
10
|
+
|
|
11
|
+
- `ap login` -- Open browser for Clerk auth, store JWT locally
|
|
12
|
+
- `ap logout` -- Remove stored credentials
|
|
13
|
+
- `ap whoami` -- Show current user info
|
|
14
|
+
- `ap search <query>` -- Search the marketplace (Rich table output)
|
|
15
|
+
- `ap install <slug> [--code XXXX]` -- Install a skill or agent (with optional license code)
|
|
16
|
+
- `ap publish [--price N] [--dir .] [--category dev]` -- Package and publish a skill or agent
|
|
17
|
+
- `ap claim <slug>` -- Claim ownership of a ClawHub-imported skill
|
|
18
|
+
|
|
19
|
+
## Development
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
cd agentpowers-cli
|
|
23
|
+
uv venv .venv && source .venv/bin/activate
|
|
24
|
+
uv pip install -e ".[dev]"
|
|
25
|
+
ap --help
|
|
26
|
+
pytest tests/ -v # Run tests
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Auth
|
|
30
|
+
|
|
31
|
+
Credentials stored at `~/.agentpowers/auth.json` (permissions: 600). Shared with the Claude Code plugin's MCP server.
|
|
File without changes
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""AgentPowers CLI banner — brand mark and version display."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib.metadata import version as pkg_version
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
# Brand teal: #5fbab8
|
|
10
|
+
TEAL = "#5fbab8"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _get_version() -> str:
|
|
14
|
+
"""Return the installed CLI version, falling back to 0.1.0."""
|
|
15
|
+
try:
|
|
16
|
+
return pkg_version("agentpowers")
|
|
17
|
+
except Exception:
|
|
18
|
+
return "0.1.0"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def print_banner(console: Console | None = None) -> None:
|
|
22
|
+
"""Print the AgentPowers brand banner to the console.
|
|
23
|
+
|
|
24
|
+
Three geometric teal shapes forming a stylized 'A' mark.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
console: Rich Console instance. Creates one if not provided.
|
|
28
|
+
"""
|
|
29
|
+
if console is None:
|
|
30
|
+
console = Console()
|
|
31
|
+
|
|
32
|
+
ver = _get_version()
|
|
33
|
+
|
|
34
|
+
console.print()
|
|
35
|
+
console.print(f" [{TEAL}] ████[/{TEAL}]")
|
|
36
|
+
console.print(
|
|
37
|
+
f" [{TEAL}] ████[/{TEAL}]"
|
|
38
|
+
f" [bold]AgentPowers[/bold] [dim]v{ver}[/dim]"
|
|
39
|
+
)
|
|
40
|
+
console.print(
|
|
41
|
+
f" [{TEAL}] ████[/{TEAL}]"
|
|
42
|
+
f" [dim]Premium Claude skills & agents[/dim]"
|
|
43
|
+
)
|
|
44
|
+
console.print(
|
|
45
|
+
f" [{TEAL}] █[/{TEAL}]"
|
|
46
|
+
f" [{TEAL}]████[/{TEAL}]"
|
|
47
|
+
f" [dim]agentpowers.ai[/dim]"
|
|
48
|
+
)
|
|
49
|
+
console.print(
|
|
50
|
+
f" [{TEAL}]███[/{TEAL}]"
|
|
51
|
+
f" [{TEAL}]████[/{TEAL}]"
|
|
52
|
+
)
|
|
53
|
+
console.print(f" [{TEAL}] ██████[/{TEAL}]")
|
|
54
|
+
console.print(f" [{TEAL}] ████[/{TEAL}]")
|
|
55
|
+
console.print(f" [{TEAL}] ██[/{TEAL}]")
|
|
56
|
+
console.print()
|
|
File without changes
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""Authentication commands for the AgentPowers CLI.
|
|
2
|
+
|
|
3
|
+
Provides login (browser-based OAuth via Clerk), logout, and whoami commands.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import socket
|
|
9
|
+
import threading
|
|
10
|
+
import webbrowser
|
|
11
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
12
|
+
from typing import Any
|
|
13
|
+
from urllib.parse import parse_qs, urlparse
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
|
|
18
|
+
from agentpowers_cli.banner import print_banner
|
|
19
|
+
from agentpowers_cli.services.api_client import APIClient, APIError
|
|
20
|
+
from agentpowers_cli.services.auth_store import delete_token, save_token
|
|
21
|
+
from agentpowers_cli.services.config import (
|
|
22
|
+
AUTH_CALLBACK_HOST,
|
|
23
|
+
CLERK_FRONTEND_URL,
|
|
24
|
+
SITE_URL,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
_PAGE_STYLE = """\
|
|
30
|
+
<style>
|
|
31
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
32
|
+
body {
|
|
33
|
+
font-family: 'DM Sans', system-ui, -apple-system, sans-serif;
|
|
34
|
+
background: #0a0f1a;
|
|
35
|
+
color: #e2e8f0;
|
|
36
|
+
display: flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
justify-content: center;
|
|
39
|
+
min-height: 100vh;
|
|
40
|
+
}
|
|
41
|
+
.card {
|
|
42
|
+
text-align: center;
|
|
43
|
+
padding: 3rem 2.5rem;
|
|
44
|
+
max-width: 420px;
|
|
45
|
+
border: 1px solid rgba(56, 189, 184, 0.2);
|
|
46
|
+
border-radius: 16px;
|
|
47
|
+
background: rgba(15, 23, 42, 0.8);
|
|
48
|
+
box-shadow: 0 0 40px rgba(56, 189, 184, 0.08);
|
|
49
|
+
}
|
|
50
|
+
.icon {
|
|
51
|
+
width: 56px; height: 56px; margin: 0 auto 1.5rem;
|
|
52
|
+
border-radius: 50%;
|
|
53
|
+
display: flex; align-items: center; justify-content: center;
|
|
54
|
+
}
|
|
55
|
+
.icon-success { background: rgba(56, 189, 184, 0.15); }
|
|
56
|
+
.icon-error { background: rgba(239, 68, 68, 0.15); }
|
|
57
|
+
.icon svg { width: 28px; height: 28px; }
|
|
58
|
+
h1 {
|
|
59
|
+
font-family: 'Space Grotesk', 'DM Sans', system-ui, sans-serif;
|
|
60
|
+
font-size: 1.5rem;
|
|
61
|
+
font-weight: 600;
|
|
62
|
+
margin-bottom: 0.75rem;
|
|
63
|
+
}
|
|
64
|
+
p {
|
|
65
|
+
font-size: 0.95rem;
|
|
66
|
+
color: #94a3b8;
|
|
67
|
+
line-height: 1.5;
|
|
68
|
+
}
|
|
69
|
+
.brand {
|
|
70
|
+
margin-top: 2rem;
|
|
71
|
+
font-size: 0.8rem;
|
|
72
|
+
color: #475569;
|
|
73
|
+
letter-spacing: 0.05em;
|
|
74
|
+
}
|
|
75
|
+
.brand span { color: #38bdb8; }
|
|
76
|
+
</style>
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
_SUCCESS_HTML = (
|
|
80
|
+
"<!DOCTYPE html>"
|
|
81
|
+
"<html lang='en'><head><meta charset='utf-8'>"
|
|
82
|
+
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
|
|
83
|
+
"<title>Logged in to AgentPowers!</title>"
|
|
84
|
+
f"{_PAGE_STYLE}</head><body>"
|
|
85
|
+
"<div class='card'>"
|
|
86
|
+
"<div class='icon icon-success'>"
|
|
87
|
+
"<svg viewBox='0 0 24 24' fill='none' stroke='#38bdb8' stroke-width='2.5'"
|
|
88
|
+
" stroke-linecap='round' stroke-linejoin='round'>"
|
|
89
|
+
"<polyline points='20 6 9 17 4 12'/></svg></div>"
|
|
90
|
+
"<h1>Logged in to AgentPowers!</h1>"
|
|
91
|
+
"<p>You can close this tab and return to the terminal.</p>"
|
|
92
|
+
"<div class='brand'><span>agent</span>powers</div>"
|
|
93
|
+
"</div></body></html>"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
_ERROR_HTML = (
|
|
97
|
+
"<!DOCTYPE html>"
|
|
98
|
+
"<html lang='en'><head><meta charset='utf-8'>"
|
|
99
|
+
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
|
|
100
|
+
"<title>Login Error</title>"
|
|
101
|
+
f"{_PAGE_STYLE}</head><body>"
|
|
102
|
+
"<div class='card'>"
|
|
103
|
+
"<div class='icon icon-error'>"
|
|
104
|
+
"<svg viewBox='0 0 24 24' fill='none' stroke='#ef4444' stroke-width='2.5'"
|
|
105
|
+
" stroke-linecap='round' stroke-linejoin='round'>"
|
|
106
|
+
"<line x1='18' y1='6' x2='6' y2='18'/>"
|
|
107
|
+
"<line x1='6' y1='6' x2='18' y2='18'/></svg></div>"
|
|
108
|
+
"<h1>Login Failed</h1>"
|
|
109
|
+
"<p>Missing token. Please try logging in again from the terminal.</p>"
|
|
110
|
+
"<div class='brand'><span>agent</span>powers</div>"
|
|
111
|
+
"</div></body></html>"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def get_api_client() -> APIClient:
|
|
116
|
+
"""Create and return an APIClient instance.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
A configured APIClient.
|
|
120
|
+
"""
|
|
121
|
+
return APIClient()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _find_free_port() -> int:
|
|
125
|
+
"""Find an available TCP port on localhost.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
An available port number.
|
|
129
|
+
"""
|
|
130
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
131
|
+
sock.bind(("", 0))
|
|
132
|
+
return sock.getsockname()[1]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def open_browser(url: str) -> None:
|
|
136
|
+
"""Open a URL in the user's default browser.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
url: The URL to open.
|
|
140
|
+
"""
|
|
141
|
+
webbrowser.open(url)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class _CallbackHandler(BaseHTTPRequestHandler):
|
|
145
|
+
"""HTTP request handler for the OAuth callback.
|
|
146
|
+
|
|
147
|
+
Expects a GET request to /callback?token=XXX and stores the
|
|
148
|
+
token on the server instance.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
def do_GET(self) -> None:
|
|
152
|
+
"""Handle the GET callback from Clerk auth redirect."""
|
|
153
|
+
parsed = urlparse(self.path)
|
|
154
|
+
if parsed.path != "/callback":
|
|
155
|
+
self.send_response(404)
|
|
156
|
+
self.end_headers()
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
params = parse_qs(parsed.query)
|
|
160
|
+
token_values = params.get("token")
|
|
161
|
+
|
|
162
|
+
if token_values:
|
|
163
|
+
server: Any = self.server
|
|
164
|
+
server.received_token = token_values[0]
|
|
165
|
+
self.send_response(200)
|
|
166
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
167
|
+
self.end_headers()
|
|
168
|
+
self.wfile.write(_SUCCESS_HTML.encode())
|
|
169
|
+
else:
|
|
170
|
+
self.send_response(400)
|
|
171
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
172
|
+
self.end_headers()
|
|
173
|
+
self.wfile.write(_ERROR_HTML.encode())
|
|
174
|
+
|
|
175
|
+
def log_message(self, format: str, *args: Any) -> None:
|
|
176
|
+
"""Suppress default HTTP server logging."""
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def start_auth_server(port: int, timeout: int = 120) -> str | None:
|
|
180
|
+
"""Start a temporary HTTP server and wait for the auth callback.
|
|
181
|
+
|
|
182
|
+
Blocks until a token is received or the timeout expires.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
port: The port to listen on.
|
|
186
|
+
timeout: Maximum seconds to wait for the callback.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
The received JWT token, or None if the timeout expired.
|
|
190
|
+
"""
|
|
191
|
+
server = HTTPServer((AUTH_CALLBACK_HOST, port), _CallbackHandler)
|
|
192
|
+
server.received_token = None # type: ignore[attr-defined]
|
|
193
|
+
server.timeout = timeout
|
|
194
|
+
|
|
195
|
+
# Run in a thread so we can enforce the overall timeout
|
|
196
|
+
thread = threading.Thread(target=server.handle_request, daemon=True)
|
|
197
|
+
thread.start()
|
|
198
|
+
thread.join(timeout=timeout)
|
|
199
|
+
|
|
200
|
+
server.server_close()
|
|
201
|
+
return server.received_token # type: ignore[attr-defined]
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def login() -> None:
|
|
205
|
+
"""Authenticate with AgentPowers via browser-based Clerk login.
|
|
206
|
+
|
|
207
|
+
Opens the default browser to the Clerk sign-in page. A temporary
|
|
208
|
+
local HTTP server receives the callback with the JWT token. The
|
|
209
|
+
short-lived JWT is exchanged for a long-lived CLI token.
|
|
210
|
+
"""
|
|
211
|
+
port = _find_free_port()
|
|
212
|
+
callback_url = f"http://{AUTH_CALLBACK_HOST}:{port}/callback"
|
|
213
|
+
bridge_url = f"{SITE_URL}/auth/cli-callback?redirect_to={callback_url}"
|
|
214
|
+
auth_url = f"{CLERK_FRONTEND_URL}/sign-in?redirect_url={bridge_url}"
|
|
215
|
+
|
|
216
|
+
console.print(f"Opening browser to sign in...\n [dim]{auth_url}[/dim]")
|
|
217
|
+
open_browser(auth_url)
|
|
218
|
+
|
|
219
|
+
console.print(f"Waiting for authentication (listening on port {port})...")
|
|
220
|
+
clerk_jwt = start_auth_server(port)
|
|
221
|
+
|
|
222
|
+
if clerk_jwt is None:
|
|
223
|
+
console.print("[red]Login timed out.[/red] Please try again.")
|
|
224
|
+
raise typer.Exit(code=1)
|
|
225
|
+
|
|
226
|
+
# Exchange short-lived Clerk JWT for long-lived CLI token
|
|
227
|
+
try:
|
|
228
|
+
client = get_api_client()
|
|
229
|
+
# Temporarily save the Clerk JWT so the client can use it
|
|
230
|
+
save_token(clerk_jwt)
|
|
231
|
+
result = client.post("/v1/auth/cli-token", auth=True)
|
|
232
|
+
cli_token = result["token"]
|
|
233
|
+
save_token(cli_token)
|
|
234
|
+
console.print("[green]Logged in successfully![/green]")
|
|
235
|
+
except (APIError, Exception):
|
|
236
|
+
# Fallback: save the raw JWT (will expire, but login succeeded)
|
|
237
|
+
save_token(clerk_jwt)
|
|
238
|
+
console.print(
|
|
239
|
+
"[green]Logged in successfully![/green] "
|
|
240
|
+
"[dim](session token — may expire sooner)[/dim]"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def logout() -> None:
|
|
245
|
+
"""Log out by revoking server-side token and removing local credentials."""
|
|
246
|
+
try:
|
|
247
|
+
client = get_api_client()
|
|
248
|
+
client.post("/v1/auth/revoke-cli-token", auth=True)
|
|
249
|
+
except (APIError, Exception):
|
|
250
|
+
pass # Best-effort revocation; always delete locally
|
|
251
|
+
delete_token()
|
|
252
|
+
console.print("Logged out.")
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def whoami() -> None:
|
|
256
|
+
"""Display the currently authenticated user's information."""
|
|
257
|
+
print_banner(console)
|
|
258
|
+
|
|
259
|
+
client = get_api_client()
|
|
260
|
+
try:
|
|
261
|
+
user = client.get("/v1/auth/me", auth=True)
|
|
262
|
+
except APIError as exc:
|
|
263
|
+
if exc.status_code == 401:
|
|
264
|
+
console.print("[red]Not logged in.[/red] Run `ap login` first.")
|
|
265
|
+
else:
|
|
266
|
+
console.print(f"[red]Error:[/red] {exc}")
|
|
267
|
+
raise typer.Exit(code=1)
|
|
268
|
+
|
|
269
|
+
email = user.get("email", "unknown")
|
|
270
|
+
name = user.get("name", "")
|
|
271
|
+
|
|
272
|
+
console.print(f"[bold]Email:[/bold] {email}")
|
|
273
|
+
if name:
|
|
274
|
+
console.print(f"[bold]Name:[/bold] {name}")
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Claim command — submit ownership claim for a ClawHub skill."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from agentpowers_cli.services.api_client import APIClient, APIError
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_api_client() -> APIClient:
|
|
14
|
+
"""Create and return an APIClient instance.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
A configured APIClient.
|
|
18
|
+
"""
|
|
19
|
+
return APIClient()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def claim(
|
|
23
|
+
slug: str = typer.Argument(help="Slug of the ClawHub skill to claim"),
|
|
24
|
+
) -> None:
|
|
25
|
+
"""Claim ownership of a ClawHub-imported skill.
|
|
26
|
+
|
|
27
|
+
Submits a POST request to /v1/skills/{slug}/claim.
|
|
28
|
+
The API verifies GitHub identity and returns an approval status,
|
|
29
|
+
sandbox (manual review), or rejection with a verification score.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
slug: The URL slug identifying the ClawHub skill to claim.
|
|
33
|
+
"""
|
|
34
|
+
client = get_api_client()
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
result = client.post(
|
|
38
|
+
f"/v1/skills/{slug}/claim",
|
|
39
|
+
auth=True,
|
|
40
|
+
)
|
|
41
|
+
except APIError as exc:
|
|
42
|
+
if exc.status_code == 401:
|
|
43
|
+
console.print("[red]Not logged in.[/red] Run `ap login` first.")
|
|
44
|
+
elif exc.status_code == 404:
|
|
45
|
+
console.print(f"[red]Skill '{slug}' not found.[/red]")
|
|
46
|
+
elif exc.status_code == 409:
|
|
47
|
+
console.print(
|
|
48
|
+
f"[yellow]Skill '{slug}' is already claimed.[/yellow]",
|
|
49
|
+
)
|
|
50
|
+
else:
|
|
51
|
+
console.print(f"[red]Error:[/red] {exc}")
|
|
52
|
+
raise typer.Exit(code=1)
|
|
53
|
+
|
|
54
|
+
status = result.get("status", "unknown")
|
|
55
|
+
score = result.get("verification_score", 0)
|
|
56
|
+
message = result.get("message", "")
|
|
57
|
+
|
|
58
|
+
if status == "approved":
|
|
59
|
+
console.print(f"[green]Approved![/green] {message}")
|
|
60
|
+
console.print(f" Score: {score}")
|
|
61
|
+
elif status == "sandbox":
|
|
62
|
+
console.print(f"[yellow]Under review.[/yellow] {message}")
|
|
63
|
+
console.print(f" Score: {score}")
|
|
64
|
+
console.print(" An admin will review your claim shortly.")
|
|
65
|
+
elif status == "rejected":
|
|
66
|
+
console.print(f"[red]Rejected.[/red] {message}")
|
|
67
|
+
console.print(f" Score: {score}")
|
|
68
|
+
else:
|
|
69
|
+
console.print(f"Status: {status} — {message}")
|