seabay-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.
- seabay_cli-0.1.0/.gitignore +55 -0
- seabay_cli-0.1.0/PKG-INFO +12 -0
- seabay_cli-0.1.0/pyproject.toml +26 -0
- seabay_cli-0.1.0/seabay/__init__.py +0 -0
- seabay_cli-0.1.0/seabay/cli.py +297 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.egg-info/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
.eggs/
|
|
9
|
+
*.egg
|
|
10
|
+
|
|
11
|
+
# Virtual environments
|
|
12
|
+
.venv/
|
|
13
|
+
venv/
|
|
14
|
+
env/
|
|
15
|
+
|
|
16
|
+
# IDE
|
|
17
|
+
.vscode/
|
|
18
|
+
.idea/
|
|
19
|
+
*.swp
|
|
20
|
+
*.swo
|
|
21
|
+
*~
|
|
22
|
+
|
|
23
|
+
# Environment
|
|
24
|
+
.env
|
|
25
|
+
.env.*
|
|
26
|
+
*.env.local
|
|
27
|
+
*.env.prod*
|
|
28
|
+
*.env.production
|
|
29
|
+
|
|
30
|
+
# OS
|
|
31
|
+
.DS_Store
|
|
32
|
+
Thumbs.db
|
|
33
|
+
|
|
34
|
+
# Node
|
|
35
|
+
node_modules/
|
|
36
|
+
npm-debug.log*
|
|
37
|
+
|
|
38
|
+
# Docker
|
|
39
|
+
docker-compose.override.yml
|
|
40
|
+
|
|
41
|
+
# Database
|
|
42
|
+
*.sqlite3
|
|
43
|
+
*.db
|
|
44
|
+
|
|
45
|
+
# Logs
|
|
46
|
+
*.log
|
|
47
|
+
logs/
|
|
48
|
+
|
|
49
|
+
# Coverage
|
|
50
|
+
.coverage
|
|
51
|
+
htmlcov/
|
|
52
|
+
.pytest_cache/
|
|
53
|
+
|
|
54
|
+
# Alembic
|
|
55
|
+
alembic/versions/*.pyc
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: seabay-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Seabay CLI — init, demo, doctor commands
|
|
5
|
+
Project-URL: Homepage, https://seabay.ai
|
|
6
|
+
Project-URL: Repository, https://github.com/seabayai/seabay
|
|
7
|
+
Author: Avan Mo
|
|
8
|
+
License: Apache-2.0
|
|
9
|
+
Keywords: agents,ai,cli,seabay
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Requires-Dist: click>=8.1.0
|
|
12
|
+
Requires-Dist: httpx>=0.28.0
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "seabay-cli"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Seabay CLI — init, demo, doctor commands"
|
|
5
|
+
requires-python = ">=3.10"
|
|
6
|
+
license = { text = "Apache-2.0" }
|
|
7
|
+
authors = [{ name = "Avan Mo" }]
|
|
8
|
+
keywords = ["ai", "agents", "seabay", "cli"]
|
|
9
|
+
dependencies = [
|
|
10
|
+
"httpx>=0.28.0",
|
|
11
|
+
"click>=8.1.0",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[project.urls]
|
|
15
|
+
Homepage = "https://seabay.ai"
|
|
16
|
+
Repository = "https://github.com/seabayai/seabay"
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
seabay = "seabay.cli:cli"
|
|
20
|
+
|
|
21
|
+
[tool.hatch.build.targets.wheel]
|
|
22
|
+
packages = ["seabay"]
|
|
23
|
+
|
|
24
|
+
[build-system]
|
|
25
|
+
requires = ["hatchling"]
|
|
26
|
+
build-backend = "hatchling.build"
|
|
File without changes
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""Seabay CLI — seabay init | demo | doctor | status | inbox | task | listen."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
DEFAULT_API_URL = "http://localhost:8000/v1"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _load_config() -> dict:
|
|
14
|
+
"""Load .seabay.json config file."""
|
|
15
|
+
config_path = Path(".seabay.json")
|
|
16
|
+
if not config_path.exists():
|
|
17
|
+
click.echo("Error: .seabay.json not found. Run 'seabay init' first.", err=True)
|
|
18
|
+
sys.exit(1)
|
|
19
|
+
with open(config_path) as f:
|
|
20
|
+
return json.load(f)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _headers(config: dict) -> dict:
|
|
24
|
+
return {"Authorization": f"Bearer {config['api_key']}"}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@click.group()
|
|
28
|
+
@click.version_option(version="0.1.0", prog_name="seabay")
|
|
29
|
+
def cli():
|
|
30
|
+
"""Seabay CLI — manage your Seabay development environment."""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@cli.command()
|
|
35
|
+
@click.option("--slug", prompt="Agent slug", help="Unique agent identifier (lowercase, hyphens, underscores)")
|
|
36
|
+
@click.option("--name", prompt="Display name", help="Human-readable agent name")
|
|
37
|
+
@click.option("--type", "agent_type", type=click.Choice(["service", "personal"]), default="personal")
|
|
38
|
+
@click.option("--api-url", default=DEFAULT_API_URL, help="Seabay API URL")
|
|
39
|
+
def init(slug: str, name: str, agent_type: str, api_url: str):
|
|
40
|
+
"""Register a new agent and save credentials."""
|
|
41
|
+
click.echo(f"Registering agent '{slug}' ({agent_type})...")
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
resp = httpx.post(
|
|
45
|
+
f"{api_url}/agents/register",
|
|
46
|
+
json={"slug": slug, "display_name": name, "agent_type": agent_type},
|
|
47
|
+
timeout=30,
|
|
48
|
+
)
|
|
49
|
+
resp.raise_for_status()
|
|
50
|
+
data = resp.json()
|
|
51
|
+
|
|
52
|
+
click.echo("\nAgent registered successfully!")
|
|
53
|
+
click.echo(f" ID: {data['id']}")
|
|
54
|
+
click.echo(f" Slug: {data['slug']}")
|
|
55
|
+
click.echo(f" API Key: {data['api_key']}")
|
|
56
|
+
click.echo("\nSave your API key — it will NOT be shown again.")
|
|
57
|
+
|
|
58
|
+
config = {
|
|
59
|
+
"agent_id": data["id"],
|
|
60
|
+
"slug": data["slug"],
|
|
61
|
+
"api_key": data["api_key"],
|
|
62
|
+
"api_url": api_url,
|
|
63
|
+
}
|
|
64
|
+
with open(".seabay.json", "w") as f:
|
|
65
|
+
json.dump(config, f, indent=2)
|
|
66
|
+
click.echo("\nCredentials saved to .seabay.json")
|
|
67
|
+
|
|
68
|
+
except httpx.HTTPStatusError as e:
|
|
69
|
+
click.echo(f"Error: {e.response.text}", err=True)
|
|
70
|
+
sys.exit(1)
|
|
71
|
+
except httpx.ConnectError:
|
|
72
|
+
click.echo(f"Error: Cannot connect to {api_url}. Is the server running?", err=True)
|
|
73
|
+
sys.exit(1)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@cli.command()
|
|
77
|
+
def status():
|
|
78
|
+
"""Show current agent status and connection info."""
|
|
79
|
+
config = _load_config()
|
|
80
|
+
api_url = config.get("api_url", DEFAULT_API_URL)
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
resp = httpx.get(
|
|
84
|
+
f"{api_url}/agents/{config['agent_id']}",
|
|
85
|
+
headers=_headers(config),
|
|
86
|
+
timeout=10,
|
|
87
|
+
)
|
|
88
|
+
resp.raise_for_status()
|
|
89
|
+
agent = resp.json()
|
|
90
|
+
|
|
91
|
+
click.echo(f"\nAgent: {agent['display_name']} (@{agent['slug']})")
|
|
92
|
+
click.echo(f" ID: {agent['id']}")
|
|
93
|
+
click.echo(f" Type: {agent['agent_type']}")
|
|
94
|
+
click.echo(f" Status: {agent['status']}")
|
|
95
|
+
click.echo(f" Verification: {agent['verification_level']}")
|
|
96
|
+
click.echo(f" Visibility: {agent['visibility_scope']}")
|
|
97
|
+
click.echo(f" Contact Policy: {agent['contact_policy']}")
|
|
98
|
+
if agent.get('last_seen_at'):
|
|
99
|
+
click.echo(f" Last Seen: {agent['last_seen_at']}")
|
|
100
|
+
click.echo()
|
|
101
|
+
|
|
102
|
+
except httpx.HTTPStatusError as e:
|
|
103
|
+
click.echo(f"Error: {e.response.text}", err=True)
|
|
104
|
+
sys.exit(1)
|
|
105
|
+
except httpx.ConnectError:
|
|
106
|
+
click.echo(f"Error: Cannot connect to {api_url}", err=True)
|
|
107
|
+
sys.exit(1)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@cli.command()
|
|
111
|
+
@click.option("--status", "task_status", default=None, help="Filter by status")
|
|
112
|
+
@click.option("--limit", default=20, help="Max items to return")
|
|
113
|
+
def inbox(task_status: str, limit: int):
|
|
114
|
+
"""Show task inbox."""
|
|
115
|
+
config = _load_config()
|
|
116
|
+
api_url = config.get("api_url", DEFAULT_API_URL)
|
|
117
|
+
|
|
118
|
+
params = {"limit": str(limit)}
|
|
119
|
+
if task_status:
|
|
120
|
+
params["status"] = task_status
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
resp = httpx.get(
|
|
124
|
+
f"{api_url}/tasks/inbox",
|
|
125
|
+
headers=_headers(config),
|
|
126
|
+
params=params,
|
|
127
|
+
timeout=10,
|
|
128
|
+
)
|
|
129
|
+
resp.raise_for_status()
|
|
130
|
+
result = resp.json()
|
|
131
|
+
|
|
132
|
+
tasks = result.get("data", [])
|
|
133
|
+
if not tasks:
|
|
134
|
+
click.echo("\nInbox is empty.")
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
click.echo(f"\nInbox ({len(tasks)} tasks):\n")
|
|
138
|
+
for t in tasks:
|
|
139
|
+
risk_badge = f" [{t['risk_level']}]" if t.get('risk_level', 'R0') != 'R0' else ""
|
|
140
|
+
click.echo(f" {t['id']} {t['status']:20s} {t['task_type']:15s}{risk_badge}")
|
|
141
|
+
if t.get('description'):
|
|
142
|
+
click.echo(f" {t['description'][:80]}")
|
|
143
|
+
|
|
144
|
+
if result.get("has_more"):
|
|
145
|
+
click.echo(f"\n ... more tasks available (cursor: {result['next_cursor']})")
|
|
146
|
+
click.echo()
|
|
147
|
+
|
|
148
|
+
except httpx.HTTPStatusError as e:
|
|
149
|
+
click.echo(f"Error: {e.response.text}", err=True)
|
|
150
|
+
sys.exit(1)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@cli.command()
|
|
154
|
+
@click.argument("task_id")
|
|
155
|
+
@click.argument("action", type=click.Choice(["accept", "decline", "complete", "cancel"]))
|
|
156
|
+
@click.option("--reason", default=None, help="Reason (for decline/cancel)")
|
|
157
|
+
@click.option("--rating", default=None, type=float, help="Rating (for complete)")
|
|
158
|
+
def task(task_id: str, action: str, reason: str, rating: float):
|
|
159
|
+
"""Manage a task: accept, decline, complete, or cancel."""
|
|
160
|
+
config = _load_config()
|
|
161
|
+
api_url = config.get("api_url", DEFAULT_API_URL)
|
|
162
|
+
|
|
163
|
+
body = {}
|
|
164
|
+
if reason:
|
|
165
|
+
body["reason"] = reason
|
|
166
|
+
if rating is not None:
|
|
167
|
+
body["rating"] = rating
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
resp = httpx.post(
|
|
171
|
+
f"{api_url}/tasks/{task_id}/{action}",
|
|
172
|
+
headers=_headers(config),
|
|
173
|
+
json=body,
|
|
174
|
+
timeout=10,
|
|
175
|
+
)
|
|
176
|
+
resp.raise_for_status()
|
|
177
|
+
result = resp.json()
|
|
178
|
+
click.echo(f"\nTask {task_id}: {action} -> {result.get('status', 'done')}")
|
|
179
|
+
|
|
180
|
+
except httpx.HTTPStatusError as e:
|
|
181
|
+
click.echo(f"Error: {e.response.text}", err=True)
|
|
182
|
+
sys.exit(1)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@cli.command()
|
|
186
|
+
def listen():
|
|
187
|
+
"""Listen for real-time events via SSE."""
|
|
188
|
+
config = _load_config()
|
|
189
|
+
api_url = config.get("api_url", DEFAULT_API_URL)
|
|
190
|
+
|
|
191
|
+
click.echo("Connecting to event stream...")
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
with httpx.stream(
|
|
195
|
+
"GET",
|
|
196
|
+
f"{api_url}/events/stream",
|
|
197
|
+
headers={**_headers(config), "Accept": "text/event-stream"},
|
|
198
|
+
timeout=None,
|
|
199
|
+
) as resp:
|
|
200
|
+
resp.raise_for_status()
|
|
201
|
+
click.echo("Connected. Listening for events (Ctrl+C to stop)...\n")
|
|
202
|
+
|
|
203
|
+
event_type = None
|
|
204
|
+
data_lines = []
|
|
205
|
+
|
|
206
|
+
for line in resp.iter_lines():
|
|
207
|
+
if line.startswith("event: "):
|
|
208
|
+
event_type = line[7:]
|
|
209
|
+
elif line.startswith("data: "):
|
|
210
|
+
data_lines.append(line[6:])
|
|
211
|
+
elif line == "" and event_type:
|
|
212
|
+
raw_data = "\n".join(data_lines)
|
|
213
|
+
if event_type == "heartbeat":
|
|
214
|
+
pass # silent
|
|
215
|
+
else:
|
|
216
|
+
click.echo(f"[{event_type}] {raw_data}")
|
|
217
|
+
event_type = None
|
|
218
|
+
data_lines = []
|
|
219
|
+
|
|
220
|
+
except KeyboardInterrupt:
|
|
221
|
+
click.echo("\nDisconnected.")
|
|
222
|
+
except httpx.ConnectError:
|
|
223
|
+
click.echo(f"Error: Cannot connect to {api_url}", err=True)
|
|
224
|
+
sys.exit(1)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@cli.command()
|
|
228
|
+
@click.option("--api-url", default=DEFAULT_API_URL, help="Seabay API URL")
|
|
229
|
+
def demo(api_url: str):
|
|
230
|
+
"""Run a quick demo: register, search, create task."""
|
|
231
|
+
click.echo("Running Seabay demo...\n")
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
health = httpx.get(f"{api_url}/health", timeout=10).json()
|
|
235
|
+
click.echo(f"Server: {health.get('service')} v{health.get('version')} ({health.get('region')})")
|
|
236
|
+
|
|
237
|
+
svc = httpx.post(f"{api_url}/agents/register", json={
|
|
238
|
+
"slug": "demo-svc",
|
|
239
|
+
"display_name": "Demo Service",
|
|
240
|
+
"agent_type": "service",
|
|
241
|
+
"skills": ["translation", "writing"],
|
|
242
|
+
}, timeout=30).json()
|
|
243
|
+
click.echo(f"\nService agent: {svc['id']}")
|
|
244
|
+
|
|
245
|
+
user = httpx.post(f"{api_url}/agents/register", json={
|
|
246
|
+
"slug": "demo-user",
|
|
247
|
+
"display_name": "Demo User",
|
|
248
|
+
"agent_type": "personal",
|
|
249
|
+
}, timeout=30).json()
|
|
250
|
+
click.echo(f"User agent: {user['id']}")
|
|
251
|
+
|
|
252
|
+
headers = {"Authorization": f"Bearer {user['api_key']}"}
|
|
253
|
+
task_resp = httpx.post(f"{api_url}/tasks", json={
|
|
254
|
+
"to_agent_id": svc["id"],
|
|
255
|
+
"task_type": "service_request",
|
|
256
|
+
"description": "Translate this document",
|
|
257
|
+
}, headers=headers, timeout=30).json()
|
|
258
|
+
click.echo(f"\nTask created: {task_resp['id']} (status: {task_resp['status']})")
|
|
259
|
+
|
|
260
|
+
click.echo("\nDemo complete!")
|
|
261
|
+
|
|
262
|
+
except Exception as e:
|
|
263
|
+
click.echo(f"Error: {e}", err=True)
|
|
264
|
+
sys.exit(1)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@cli.command()
|
|
268
|
+
@click.option("--api-url", default=DEFAULT_API_URL, help="Seabay API URL")
|
|
269
|
+
def doctor(api_url: str):
|
|
270
|
+
"""Check Seabay development environment health."""
|
|
271
|
+
checks = []
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
resp = httpx.get(f"{api_url}/health", timeout=5)
|
|
275
|
+
if resp.status_code == 200:
|
|
276
|
+
checks.append(("API Server", "OK", resp.json().get("version", "unknown")))
|
|
277
|
+
else:
|
|
278
|
+
checks.append(("API Server", "ERROR", f"Status {resp.status_code}"))
|
|
279
|
+
except httpx.ConnectError:
|
|
280
|
+
checks.append(("API Server", "FAIL", f"Cannot connect to {api_url}"))
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
with open(".seabay.json") as f:
|
|
284
|
+
config = json.load(f)
|
|
285
|
+
checks.append(("Local Config", "OK", f"Agent: {config.get('slug', 'unknown')}"))
|
|
286
|
+
except FileNotFoundError:
|
|
287
|
+
checks.append(("Local Config", "MISSING", "Run 'seabay init' first"))
|
|
288
|
+
|
|
289
|
+
click.echo("\nSeabay Doctor\n")
|
|
290
|
+
for name, check_status, detail in checks:
|
|
291
|
+
icon = {"OK": "v", "ERROR": "x", "FAIL": "x", "MISSING": "?"}.get(check_status, "?")
|
|
292
|
+
click.echo(f" {icon} {name}: {check_status} — {detail}")
|
|
293
|
+
click.echo()
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
if __name__ == "__main__":
|
|
297
|
+
cli()
|