trinity-cli 0.1.0__py3-none-any.whl
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.
- trinity_cli/__init__.py +3 -0
- trinity_cli/client.py +107 -0
- trinity_cli/commands/__init__.py +0 -0
- trinity_cli/commands/agents.py +96 -0
- trinity_cli/commands/auth.py +177 -0
- trinity_cli/commands/chat.py +64 -0
- trinity_cli/commands/health.py +31 -0
- trinity_cli/commands/profiles.py +57 -0
- trinity_cli/commands/schedules.py +44 -0
- trinity_cli/commands/skills.py +42 -0
- trinity_cli/commands/tags.py +31 -0
- trinity_cli/config.py +193 -0
- trinity_cli/main.py +79 -0
- trinity_cli/output.py +62 -0
- trinity_cli-0.1.0.dist-info/METADATA +119 -0
- trinity_cli-0.1.0.dist-info/RECORD +19 -0
- trinity_cli-0.1.0.dist-info/WHEEL +5 -0
- trinity_cli-0.1.0.dist-info/entry_points.txt +2 -0
- trinity_cli-0.1.0.dist-info/top_level.txt +1 -0
trinity_cli/__init__.py
ADDED
trinity_cli/client.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""HTTP client for the Trinity Backend API."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from .config import get_api_key, get_instance_url
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _profile_from_context() -> Optional[str]:
|
|
13
|
+
"""Try to get the --profile value from the current Click context."""
|
|
14
|
+
try:
|
|
15
|
+
ctx = click.get_current_context(silent=True)
|
|
16
|
+
if ctx:
|
|
17
|
+
root = ctx.find_root()
|
|
18
|
+
if root.obj and "profile" in root.obj:
|
|
19
|
+
return root.obj["profile"]
|
|
20
|
+
except RuntimeError:
|
|
21
|
+
pass
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TrinityAPIError(Exception):
|
|
26
|
+
def __init__(self, status_code: int, detail: str):
|
|
27
|
+
self.status_code = status_code
|
|
28
|
+
self.detail = detail
|
|
29
|
+
super().__init__(f"HTTP {status_code}: {detail}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TrinityClient:
|
|
33
|
+
"""Thin HTTP wrapper around the Trinity FastAPI backend."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, base_url: Optional[str] = None, token: Optional[str] = None,
|
|
36
|
+
profile: Optional[str] = None):
|
|
37
|
+
resolved_profile = profile or _profile_from_context()
|
|
38
|
+
self.base_url = base_url or get_instance_url(resolved_profile)
|
|
39
|
+
self.token = token or get_api_key(resolved_profile)
|
|
40
|
+
if not self.base_url:
|
|
41
|
+
print("Error: No Trinity instance configured. Run 'trinity init' or 'trinity login' first.", file=sys.stderr)
|
|
42
|
+
sys.exit(1)
|
|
43
|
+
|
|
44
|
+
def _headers(self) -> dict:
|
|
45
|
+
h = {"Content-Type": "application/json"}
|
|
46
|
+
if self.token:
|
|
47
|
+
h["Authorization"] = f"Bearer {self.token}"
|
|
48
|
+
return h
|
|
49
|
+
|
|
50
|
+
def _handle_response(self, resp: httpx.Response) -> Any:
|
|
51
|
+
if resp.status_code == 401:
|
|
52
|
+
print("Error: Authentication failed. Run 'trinity login' to re-authenticate.", file=sys.stderr)
|
|
53
|
+
sys.exit(1)
|
|
54
|
+
if resp.status_code >= 400:
|
|
55
|
+
try:
|
|
56
|
+
detail = resp.json().get("detail", resp.text)
|
|
57
|
+
except Exception:
|
|
58
|
+
detail = resp.text
|
|
59
|
+
raise TrinityAPIError(resp.status_code, str(detail))
|
|
60
|
+
if resp.status_code == 204:
|
|
61
|
+
return None
|
|
62
|
+
return resp.json()
|
|
63
|
+
|
|
64
|
+
def get(self, path: str, params: Optional[dict] = None) -> Any:
|
|
65
|
+
with httpx.Client(timeout=30) as c:
|
|
66
|
+
resp = c.get(f"{self.base_url}{path}", headers=self._headers(), params=params)
|
|
67
|
+
return self._handle_response(resp)
|
|
68
|
+
|
|
69
|
+
def post(self, path: str, json: Optional[dict] = None) -> Any:
|
|
70
|
+
with httpx.Client(timeout=60) as c:
|
|
71
|
+
resp = c.post(f"{self.base_url}{path}", headers=self._headers(), json=json)
|
|
72
|
+
return self._handle_response(resp)
|
|
73
|
+
|
|
74
|
+
def put(self, path: str, json: Optional[dict] = None) -> Any:
|
|
75
|
+
with httpx.Client(timeout=30) as c:
|
|
76
|
+
resp = c.put(f"{self.base_url}{path}", headers=self._headers(), json=json)
|
|
77
|
+
return self._handle_response(resp)
|
|
78
|
+
|
|
79
|
+
def delete(self, path: str) -> Any:
|
|
80
|
+
with httpx.Client(timeout=30) as c:
|
|
81
|
+
resp = c.delete(f"{self.base_url}{path}", headers=self._headers())
|
|
82
|
+
return self._handle_response(resp)
|
|
83
|
+
|
|
84
|
+
def post_form(self, path: str, data: dict) -> Any:
|
|
85
|
+
"""POST with form-encoded body (for OAuth2 token endpoint)."""
|
|
86
|
+
with httpx.Client(timeout=30) as c:
|
|
87
|
+
headers = {}
|
|
88
|
+
if self.token:
|
|
89
|
+
headers["Authorization"] = f"Bearer {self.token}"
|
|
90
|
+
resp = c.post(f"{self.base_url}{path}", data=data, headers=headers)
|
|
91
|
+
return self._handle_response(resp)
|
|
92
|
+
|
|
93
|
+
def post_unauthenticated(self, path: str, json: Optional[dict] = None) -> Any:
|
|
94
|
+
"""POST without auth header (for login/registration flows)."""
|
|
95
|
+
with httpx.Client(timeout=30) as c:
|
|
96
|
+
resp = c.post(
|
|
97
|
+
f"{self.base_url}{path}",
|
|
98
|
+
headers={"Content-Type": "application/json"},
|
|
99
|
+
json=json,
|
|
100
|
+
)
|
|
101
|
+
return self._handle_response(resp)
|
|
102
|
+
|
|
103
|
+
def get_unauthenticated(self, path: str) -> Any:
|
|
104
|
+
"""GET without auth header."""
|
|
105
|
+
with httpx.Client(timeout=30) as c:
|
|
106
|
+
resp = c.get(f"{self.base_url}{path}")
|
|
107
|
+
return self._handle_response(resp)
|
|
File without changes
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Agent management commands."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from ..client import TrinityClient, TrinityAPIError
|
|
6
|
+
from ..output import format_output
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.group()
|
|
10
|
+
def agents():
|
|
11
|
+
"""Manage agents."""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@agents.command("list")
|
|
16
|
+
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="json", help="Output format")
|
|
17
|
+
def list_agents(fmt):
|
|
18
|
+
"""List all agents."""
|
|
19
|
+
client = TrinityClient()
|
|
20
|
+
data = client.get("/api/agents")
|
|
21
|
+
if fmt == "table" and isinstance(data, list):
|
|
22
|
+
# Slim down for table view
|
|
23
|
+
rows = [
|
|
24
|
+
{
|
|
25
|
+
"name": a.get("name", ""),
|
|
26
|
+
"status": a.get("status", ""),
|
|
27
|
+
"template": a.get("template", ""),
|
|
28
|
+
"type": a.get("type", ""),
|
|
29
|
+
}
|
|
30
|
+
for a in data
|
|
31
|
+
]
|
|
32
|
+
format_output(rows, fmt)
|
|
33
|
+
else:
|
|
34
|
+
format_output(data, fmt)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@agents.command("get")
|
|
38
|
+
@click.argument("name")
|
|
39
|
+
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="json", help="Output format")
|
|
40
|
+
def get_agent(name, fmt):
|
|
41
|
+
"""Get agent details."""
|
|
42
|
+
client = TrinityClient()
|
|
43
|
+
data = client.get(f"/api/agents/{name}")
|
|
44
|
+
format_output(data, fmt)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@agents.command("create")
|
|
48
|
+
@click.argument("name")
|
|
49
|
+
@click.option("--template", default=None, help="Template (e.g. github:Org/repo)")
|
|
50
|
+
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="json", help="Output format")
|
|
51
|
+
def create_agent(name, template, fmt):
|
|
52
|
+
"""Create a new agent."""
|
|
53
|
+
client = TrinityClient()
|
|
54
|
+
payload = {"name": name}
|
|
55
|
+
if template:
|
|
56
|
+
payload["template"] = template
|
|
57
|
+
data = client.post("/api/agents", json=payload)
|
|
58
|
+
format_output(data, fmt)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@agents.command("delete")
|
|
62
|
+
@click.argument("name")
|
|
63
|
+
@click.confirmation_option(prompt="Are you sure you want to delete this agent?")
|
|
64
|
+
def delete_agent(name):
|
|
65
|
+
"""Delete an agent."""
|
|
66
|
+
client = TrinityClient()
|
|
67
|
+
client.delete(f"/api/agents/{name}")
|
|
68
|
+
click.echo(f"Deleted agent '{name}'")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@agents.command("start")
|
|
72
|
+
@click.argument("name")
|
|
73
|
+
def start_agent(name):
|
|
74
|
+
"""Start an agent container."""
|
|
75
|
+
client = TrinityClient()
|
|
76
|
+
client.post(f"/api/agents/{name}/start")
|
|
77
|
+
click.echo(f"Started agent '{name}'")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@agents.command("stop")
|
|
81
|
+
@click.argument("name")
|
|
82
|
+
def stop_agent(name):
|
|
83
|
+
"""Stop an agent container."""
|
|
84
|
+
client = TrinityClient()
|
|
85
|
+
client.post(f"/api/agents/{name}/stop")
|
|
86
|
+
click.echo(f"Stopped agent '{name}'")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@agents.command("rename")
|
|
90
|
+
@click.argument("name")
|
|
91
|
+
@click.argument("new_name")
|
|
92
|
+
def rename_agent(name, new_name):
|
|
93
|
+
"""Rename an agent."""
|
|
94
|
+
client = TrinityClient()
|
|
95
|
+
client.put(f"/api/agents/{name}/rename", json={"new_name": new_name})
|
|
96
|
+
click.echo(f"Renamed '{name}' -> '{new_name}'")
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Authentication commands: login, logout, status, init."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from ..client import TrinityClient, TrinityAPIError
|
|
6
|
+
from ..config import (
|
|
7
|
+
clear_auth, get_instance_url, get_user, load_config,
|
|
8
|
+
profile_name_from_url, set_auth, _resolve_profile_name,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _get_profile_name(ctx: click.Context) -> str | None:
|
|
13
|
+
"""Extract the --profile value from the root context."""
|
|
14
|
+
root = ctx.find_root()
|
|
15
|
+
return root.obj.get("profile") if root.obj else None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@click.command()
|
|
19
|
+
@click.option("--instance", help="Trinity instance URL (e.g. https://trinity.example.com)")
|
|
20
|
+
@click.option("--profile", "profile_opt", default=None,
|
|
21
|
+
help="Profile name to store credentials under (default: hostname)")
|
|
22
|
+
@click.pass_context
|
|
23
|
+
def login(ctx, instance, profile_opt):
|
|
24
|
+
"""Log in to a Trinity instance with email verification."""
|
|
25
|
+
profile_name = profile_opt or _get_profile_name(ctx)
|
|
26
|
+
url = instance or get_instance_url(profile_name)
|
|
27
|
+
if not url:
|
|
28
|
+
url = click.prompt("Trinity instance URL")
|
|
29
|
+
url = url.rstrip("/")
|
|
30
|
+
|
|
31
|
+
client = TrinityClient(base_url=url, token="none")
|
|
32
|
+
|
|
33
|
+
email = click.prompt("Email")
|
|
34
|
+
|
|
35
|
+
# Request verification code
|
|
36
|
+
try:
|
|
37
|
+
client.post_unauthenticated("/api/auth/email/request", {"email": email})
|
|
38
|
+
except TrinityAPIError as e:
|
|
39
|
+
click.echo(f"Error requesting code: {e.detail}", err=True)
|
|
40
|
+
raise SystemExit(1)
|
|
41
|
+
|
|
42
|
+
click.echo(f"Verification code sent to {email}")
|
|
43
|
+
code = click.prompt("Enter 6-digit code")
|
|
44
|
+
|
|
45
|
+
# Verify code and get token
|
|
46
|
+
try:
|
|
47
|
+
result = client.post_unauthenticated("/api/auth/email/verify", {
|
|
48
|
+
"email": email,
|
|
49
|
+
"code": code,
|
|
50
|
+
})
|
|
51
|
+
except TrinityAPIError as e:
|
|
52
|
+
click.echo(f"Verification failed: {e.detail}", err=True)
|
|
53
|
+
raise SystemExit(1)
|
|
54
|
+
|
|
55
|
+
token = result["access_token"]
|
|
56
|
+
user = result.get("user")
|
|
57
|
+
|
|
58
|
+
# Determine profile name: explicit > global flag > derive from URL
|
|
59
|
+
target_profile = profile_name or profile_name_from_url(url)
|
|
60
|
+
set_auth(url, token, user, profile_name=target_profile)
|
|
61
|
+
name = user.get("name") or user.get("email") or user.get("username") if user else email
|
|
62
|
+
click.echo(f"Logged in as {name} [profile: {target_profile}]")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@click.command()
|
|
66
|
+
@click.pass_context
|
|
67
|
+
def logout(ctx):
|
|
68
|
+
"""Clear stored credentials for the current profile."""
|
|
69
|
+
profile_name = _get_profile_name(ctx)
|
|
70
|
+
clear_auth(profile_name)
|
|
71
|
+
resolved = _resolve_profile_name(profile_name)
|
|
72
|
+
click.echo(f"Logged out [profile: {resolved}]")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@click.command()
|
|
76
|
+
@click.pass_context
|
|
77
|
+
def status(ctx):
|
|
78
|
+
"""Show current login status and instance info."""
|
|
79
|
+
profile_name = _get_profile_name(ctx)
|
|
80
|
+
resolved = _resolve_profile_name(profile_name)
|
|
81
|
+
url = get_instance_url(profile_name)
|
|
82
|
+
|
|
83
|
+
click.echo(f"Profile: {resolved}")
|
|
84
|
+
|
|
85
|
+
if not url:
|
|
86
|
+
click.echo("Instance: Not configured. Run 'trinity init' or 'trinity login'.")
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
user = get_user(profile_name)
|
|
90
|
+
config = load_config()
|
|
91
|
+
profile_data = config.get("profiles", {}).get(resolved, {})
|
|
92
|
+
|
|
93
|
+
click.echo(f"Instance: {url}")
|
|
94
|
+
if user:
|
|
95
|
+
click.echo(f"User: {user.get('email') or user.get('username')}")
|
|
96
|
+
click.echo(f"Role: {user.get('role', 'unknown')}")
|
|
97
|
+
elif profile_data.get("token"):
|
|
98
|
+
click.echo("User: (API key auth)")
|
|
99
|
+
else:
|
|
100
|
+
click.echo("User: Not logged in")
|
|
101
|
+
|
|
102
|
+
# Check connectivity
|
|
103
|
+
try:
|
|
104
|
+
client = TrinityClient(base_url=url, token=profile_data.get("token", "none"))
|
|
105
|
+
client.get_unauthenticated("/api/auth/mode")
|
|
106
|
+
click.echo("Status: Connected")
|
|
107
|
+
except Exception:
|
|
108
|
+
click.echo("Status: Unreachable")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@click.command()
|
|
112
|
+
@click.option("--profile", "profile_opt", default=None,
|
|
113
|
+
help="Profile name (default: derived from instance hostname)")
|
|
114
|
+
@click.pass_context
|
|
115
|
+
def init(ctx, profile_opt):
|
|
116
|
+
"""Set up Trinity CLI: configure instance, request access, and log in.
|
|
117
|
+
|
|
118
|
+
One command to go from zero to authenticated. Creates a named profile
|
|
119
|
+
for the instance (defaults to hostname).
|
|
120
|
+
"""
|
|
121
|
+
url = click.prompt("Trinity instance URL", default="http://localhost:8000")
|
|
122
|
+
url = url.rstrip("/")
|
|
123
|
+
|
|
124
|
+
client = TrinityClient(base_url=url, token="none")
|
|
125
|
+
|
|
126
|
+
# Verify instance is reachable
|
|
127
|
+
try:
|
|
128
|
+
client.get_unauthenticated("/api/auth/mode")
|
|
129
|
+
except Exception:
|
|
130
|
+
click.echo(f"Cannot reach {url}. Check the URL and try again.", err=True)
|
|
131
|
+
raise SystemExit(1)
|
|
132
|
+
|
|
133
|
+
click.echo(f"Connected to {url}")
|
|
134
|
+
|
|
135
|
+
# Determine profile name
|
|
136
|
+
profile_name = profile_opt or _get_profile_name(ctx) or profile_name_from_url(url)
|
|
137
|
+
|
|
138
|
+
email = click.prompt("Email")
|
|
139
|
+
|
|
140
|
+
# Request access (auto-approve endpoint)
|
|
141
|
+
try:
|
|
142
|
+
client.post_unauthenticated("/api/access/request", {"email": email})
|
|
143
|
+
click.echo("Access granted")
|
|
144
|
+
except TrinityAPIError as e:
|
|
145
|
+
if e.status_code == 409:
|
|
146
|
+
click.echo("Already registered")
|
|
147
|
+
else:
|
|
148
|
+
click.echo(f"Access request failed: {e.detail}", err=True)
|
|
149
|
+
raise SystemExit(1)
|
|
150
|
+
|
|
151
|
+
# Send verification code
|
|
152
|
+
try:
|
|
153
|
+
client.post_unauthenticated("/api/auth/email/request", {"email": email})
|
|
154
|
+
except TrinityAPIError as e:
|
|
155
|
+
click.echo(f"Error requesting code: {e.detail}", err=True)
|
|
156
|
+
raise SystemExit(1)
|
|
157
|
+
|
|
158
|
+
click.echo(f"Verification code sent to {email}")
|
|
159
|
+
code = click.prompt("Enter 6-digit code")
|
|
160
|
+
|
|
161
|
+
# Verify and get token
|
|
162
|
+
try:
|
|
163
|
+
result = client.post_unauthenticated("/api/auth/email/verify", {
|
|
164
|
+
"email": email,
|
|
165
|
+
"code": code,
|
|
166
|
+
})
|
|
167
|
+
except TrinityAPIError as e:
|
|
168
|
+
click.echo(f"Verification failed: {e.detail}", err=True)
|
|
169
|
+
raise SystemExit(1)
|
|
170
|
+
|
|
171
|
+
token = result["access_token"]
|
|
172
|
+
user = result.get("user")
|
|
173
|
+
|
|
174
|
+
set_auth(url, token, user, profile_name=profile_name)
|
|
175
|
+
name = user.get("name") or user.get("email") or user.get("username") if user else email
|
|
176
|
+
click.echo(f"Logged in as {name} [profile: {profile_name}]")
|
|
177
|
+
click.echo(f"\nTrinity CLI is ready. Try 'trinity agents list'.")
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Chat and log commands."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from ..client import TrinityClient
|
|
6
|
+
from ..output import format_output
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.command("chat")
|
|
10
|
+
@click.argument("agent")
|
|
11
|
+
@click.argument("message")
|
|
12
|
+
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="json", help="Output format")
|
|
13
|
+
def chat_with_agent(agent, message, fmt):
|
|
14
|
+
"""Send a message to an agent.
|
|
15
|
+
|
|
16
|
+
Example: trinity chat my-agent "What is the status?"
|
|
17
|
+
"""
|
|
18
|
+
client = TrinityClient()
|
|
19
|
+
data = client.post(f"/api/agents/{agent}/chat", json={"message": message})
|
|
20
|
+
if fmt == "json":
|
|
21
|
+
format_output(data, fmt)
|
|
22
|
+
else:
|
|
23
|
+
# In table mode, just print the response text
|
|
24
|
+
response = data.get("response", data) if isinstance(data, dict) else data
|
|
25
|
+
click.echo(response)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@click.group("chat-history")
|
|
29
|
+
def chat_history_group():
|
|
30
|
+
"""Chat history commands."""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@click.command("history")
|
|
35
|
+
@click.argument("agent")
|
|
36
|
+
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="json", help="Output format")
|
|
37
|
+
def chat_history(agent, fmt):
|
|
38
|
+
"""Get chat history for an agent."""
|
|
39
|
+
client = TrinityClient()
|
|
40
|
+
data = client.get(f"/api/agents/{agent}/chat/history")
|
|
41
|
+
format_output(data, fmt)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@click.command("logs")
|
|
45
|
+
@click.argument("agent")
|
|
46
|
+
@click.option("--tail", default=50, help="Number of log lines")
|
|
47
|
+
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="json", help="Output format")
|
|
48
|
+
def logs(agent, tail, fmt):
|
|
49
|
+
"""View agent container logs.
|
|
50
|
+
|
|
51
|
+
Example: trinity logs my-agent --tail 100
|
|
52
|
+
"""
|
|
53
|
+
client = TrinityClient()
|
|
54
|
+
data = client.get(f"/api/agents/{agent}/logs", params={"tail": tail})
|
|
55
|
+
if fmt == "json":
|
|
56
|
+
format_output(data, fmt)
|
|
57
|
+
else:
|
|
58
|
+
# Print logs as plain text
|
|
59
|
+
if isinstance(data, dict) and "logs" in data:
|
|
60
|
+
click.echo(data["logs"])
|
|
61
|
+
elif isinstance(data, str):
|
|
62
|
+
click.echo(data)
|
|
63
|
+
else:
|
|
64
|
+
format_output(data, fmt)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Health and monitoring commands."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from ..client import TrinityClient
|
|
6
|
+
from ..output import format_output
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.group()
|
|
10
|
+
def health():
|
|
11
|
+
"""Fleet and agent health monitoring."""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@health.command("fleet")
|
|
16
|
+
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="json", help="Output format")
|
|
17
|
+
def fleet_health(fmt):
|
|
18
|
+
"""Show fleet-wide health status."""
|
|
19
|
+
client = TrinityClient()
|
|
20
|
+
data = client.get("/api/monitoring/status")
|
|
21
|
+
format_output(data, fmt)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@health.command("agent")
|
|
25
|
+
@click.argument("name")
|
|
26
|
+
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="json", help="Output format")
|
|
27
|
+
def agent_health(name, fmt):
|
|
28
|
+
"""Show health status for a specific agent."""
|
|
29
|
+
client = TrinityClient()
|
|
30
|
+
data = client.get(f"/api/monitoring/agents/{name}")
|
|
31
|
+
format_output(data, fmt)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Profile management commands: list, use, remove."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from ..config import list_profiles, remove_profile, set_current_profile
|
|
6
|
+
from ..output import format_output
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.group()
|
|
10
|
+
def profile():
|
|
11
|
+
"""Manage instance profiles.
|
|
12
|
+
|
|
13
|
+
Profiles let you store credentials for multiple Trinity instances
|
|
14
|
+
and switch between them.
|
|
15
|
+
"""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@profile.command("list")
|
|
20
|
+
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="table",
|
|
21
|
+
help="Output format")
|
|
22
|
+
def profile_list(fmt):
|
|
23
|
+
"""List all configured profiles."""
|
|
24
|
+
profiles = list_profiles()
|
|
25
|
+
if not profiles:
|
|
26
|
+
click.echo("No profiles configured. Run 'trinity init' to create one.")
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
if fmt == "json":
|
|
30
|
+
format_output(profiles, "json")
|
|
31
|
+
else:
|
|
32
|
+
for p in profiles:
|
|
33
|
+
marker = "*" if p["active"] else " "
|
|
34
|
+
user_str = f" ({p['user']})" if p["user"] else ""
|
|
35
|
+
click.echo(f" {marker} {p['name']:20s} {p['instance_url']}{user_str}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@profile.command("use")
|
|
39
|
+
@click.argument("name")
|
|
40
|
+
def profile_use(name):
|
|
41
|
+
"""Switch to a different profile."""
|
|
42
|
+
if set_current_profile(name):
|
|
43
|
+
click.echo(f"Switched to profile '{name}'")
|
|
44
|
+
else:
|
|
45
|
+
click.echo(f"Profile '{name}' not found. Run 'trinity profile list' to see available profiles.", err=True)
|
|
46
|
+
raise SystemExit(1)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@profile.command("remove")
|
|
50
|
+
@click.argument("name")
|
|
51
|
+
def profile_remove(name):
|
|
52
|
+
"""Remove a profile."""
|
|
53
|
+
if remove_profile(name):
|
|
54
|
+
click.echo(f"Removed profile '{name}'")
|
|
55
|
+
else:
|
|
56
|
+
click.echo(f"Profile '{name}' not found.", err=True)
|
|
57
|
+
raise SystemExit(1)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Schedule management commands."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from ..client import TrinityClient
|
|
6
|
+
from ..output import format_output
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.group()
|
|
10
|
+
def schedules():
|
|
11
|
+
"""Manage agent schedules."""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@schedules.command("list")
|
|
16
|
+
@click.argument("agent")
|
|
17
|
+
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="json", help="Output format")
|
|
18
|
+
def list_schedules(agent, fmt):
|
|
19
|
+
"""List schedules for an agent."""
|
|
20
|
+
client = TrinityClient()
|
|
21
|
+
data = client.get(f"/api/agents/{agent}/schedules")
|
|
22
|
+
if fmt == "table" and isinstance(data, list):
|
|
23
|
+
rows = [
|
|
24
|
+
{
|
|
25
|
+
"id": s.get("id", ""),
|
|
26
|
+
"skill": s.get("skill_name", ""),
|
|
27
|
+
"cron": s.get("cron_expression", ""),
|
|
28
|
+
"enabled": s.get("enabled", ""),
|
|
29
|
+
}
|
|
30
|
+
for s in data
|
|
31
|
+
]
|
|
32
|
+
format_output(rows, fmt)
|
|
33
|
+
else:
|
|
34
|
+
format_output(data, fmt)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@schedules.command("trigger")
|
|
38
|
+
@click.argument("agent")
|
|
39
|
+
@click.argument("schedule_id")
|
|
40
|
+
def trigger_schedule(agent, schedule_id):
|
|
41
|
+
"""Trigger a schedule immediately."""
|
|
42
|
+
client = TrinityClient()
|
|
43
|
+
data = client.post(f"/api/agents/{agent}/schedules/{schedule_id}/trigger")
|
|
44
|
+
click.echo(f"Triggered schedule {schedule_id} on '{agent}'")
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Skills library commands."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from ..client import TrinityClient
|
|
6
|
+
from ..output import format_output
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.group()
|
|
10
|
+
def skills():
|
|
11
|
+
"""Browse the skills library."""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@skills.command("list")
|
|
16
|
+
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="json", help="Output format")
|
|
17
|
+
def list_skills(fmt):
|
|
18
|
+
"""List all available skills."""
|
|
19
|
+
client = TrinityClient()
|
|
20
|
+
data = client.get("/api/skills/library")
|
|
21
|
+
if fmt == "table" and isinstance(data, list):
|
|
22
|
+
rows = [
|
|
23
|
+
{
|
|
24
|
+
"name": s.get("name", ""),
|
|
25
|
+
"description": (s.get("description", "") or "")[:60],
|
|
26
|
+
"category": s.get("category", ""),
|
|
27
|
+
}
|
|
28
|
+
for s in data
|
|
29
|
+
]
|
|
30
|
+
format_output(rows, fmt)
|
|
31
|
+
else:
|
|
32
|
+
format_output(data, fmt)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@skills.command("get")
|
|
36
|
+
@click.argument("name")
|
|
37
|
+
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="json", help="Output format")
|
|
38
|
+
def get_skill(name, fmt):
|
|
39
|
+
"""Get details for a specific skill."""
|
|
40
|
+
client = TrinityClient()
|
|
41
|
+
data = client.get(f"/api/skills/library/{name}")
|
|
42
|
+
format_output(data, fmt)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Tag management commands."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from ..client import TrinityClient
|
|
6
|
+
from ..output import format_output
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.group()
|
|
10
|
+
def tags():
|
|
11
|
+
"""Manage agent tags."""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@tags.command("list")
|
|
16
|
+
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="json", help="Output format")
|
|
17
|
+
def list_tags(fmt):
|
|
18
|
+
"""List all tags in use."""
|
|
19
|
+
client = TrinityClient()
|
|
20
|
+
data = client.get("/api/tags")
|
|
21
|
+
format_output(data, fmt)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@tags.command("get")
|
|
25
|
+
@click.argument("agent")
|
|
26
|
+
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="json", help="Output format")
|
|
27
|
+
def get_agent_tags(agent, fmt):
|
|
28
|
+
"""Get tags for a specific agent."""
|
|
29
|
+
client = TrinityClient()
|
|
30
|
+
data = client.get(f"/api/agents/{agent}/tags")
|
|
31
|
+
format_output(data, fmt)
|
trinity_cli/config.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Configuration management for Trinity CLI.
|
|
2
|
+
|
|
3
|
+
Supports named profiles for managing multiple Trinity instances.
|
|
4
|
+
Stores config in ~/.trinity/config.json with 0600 permissions.
|
|
5
|
+
|
|
6
|
+
Config format:
|
|
7
|
+
{
|
|
8
|
+
"current_profile": "local",
|
|
9
|
+
"profiles": {
|
|
10
|
+
"local": {
|
|
11
|
+
"instance_url": "http://localhost:8000",
|
|
12
|
+
"token": "eyJ...",
|
|
13
|
+
"user": {"email": "admin@example.com"}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
Legacy flat configs are auto-migrated to a "default" profile on first access.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import stat
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Optional
|
|
26
|
+
from urllib.parse import urlparse
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
CONFIG_DIR = Path.home() / ".trinity"
|
|
30
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _ensure_config_dir():
|
|
34
|
+
CONFIG_DIR.mkdir(mode=0o700, exist_ok=True)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _is_legacy_config(config: dict) -> bool:
|
|
38
|
+
"""Check if config uses the old flat format (no profiles key)."""
|
|
39
|
+
return "profiles" not in config and ("instance_url" in config or "token" in config)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _migrate_legacy_config(config: dict) -> dict:
|
|
43
|
+
"""Migrate flat config to profile-based format."""
|
|
44
|
+
profile_data = {}
|
|
45
|
+
for key in ("instance_url", "token", "user"):
|
|
46
|
+
if key in config:
|
|
47
|
+
profile_data[key] = config[key]
|
|
48
|
+
|
|
49
|
+
if not profile_data:
|
|
50
|
+
return {"current_profile": "default", "profiles": {}}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
"current_profile": "default",
|
|
54
|
+
"profiles": {
|
|
55
|
+
"default": profile_data,
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def load_config() -> dict:
|
|
61
|
+
"""Load config, auto-migrating legacy flat format if needed."""
|
|
62
|
+
if not CONFIG_FILE.exists():
|
|
63
|
+
return {"current_profile": "default", "profiles": {}}
|
|
64
|
+
config = json.loads(CONFIG_FILE.read_text())
|
|
65
|
+
if _is_legacy_config(config):
|
|
66
|
+
config = _migrate_legacy_config(config)
|
|
67
|
+
save_config(config)
|
|
68
|
+
return config
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def save_config(config: dict):
|
|
72
|
+
_ensure_config_dir()
|
|
73
|
+
CONFIG_FILE.write_text(json.dumps(config, indent=2) + "\n")
|
|
74
|
+
os.chmod(CONFIG_FILE, stat.S_IRUSR | stat.S_IWUSR) # 0600
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _resolve_profile_name(explicit_profile: Optional[str] = None) -> str:
|
|
78
|
+
"""Resolve which profile to use.
|
|
79
|
+
|
|
80
|
+
Priority: explicit_profile arg > TRINITY_PROFILE env var > current_profile in config.
|
|
81
|
+
"""
|
|
82
|
+
if explicit_profile:
|
|
83
|
+
return explicit_profile
|
|
84
|
+
env_profile = os.environ.get("TRINITY_PROFILE")
|
|
85
|
+
if env_profile:
|
|
86
|
+
return env_profile
|
|
87
|
+
config = load_config()
|
|
88
|
+
return config.get("current_profile", "default")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_profile(profile_name: Optional[str] = None) -> dict:
|
|
92
|
+
"""Get the data for a specific profile (or the active one)."""
|
|
93
|
+
name = _resolve_profile_name(profile_name)
|
|
94
|
+
config = load_config()
|
|
95
|
+
return config.get("profiles", {}).get(name, {})
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def get_instance_url(profile_name: Optional[str] = None) -> Optional[str]:
|
|
99
|
+
"""Get configured instance URL. Env var TRINITY_URL always wins."""
|
|
100
|
+
url = os.environ.get("TRINITY_URL")
|
|
101
|
+
if url:
|
|
102
|
+
return url.rstrip("/")
|
|
103
|
+
profile = get_profile(profile_name)
|
|
104
|
+
url = profile.get("instance_url")
|
|
105
|
+
return url.rstrip("/") if url else None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def get_api_key(profile_name: Optional[str] = None) -> Optional[str]:
|
|
109
|
+
"""Get API key/token. Env var TRINITY_API_KEY always wins."""
|
|
110
|
+
key = os.environ.get("TRINITY_API_KEY")
|
|
111
|
+
if key:
|
|
112
|
+
return key
|
|
113
|
+
profile = get_profile(profile_name)
|
|
114
|
+
return profile.get("token")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def get_user(profile_name: Optional[str] = None) -> Optional[dict]:
|
|
118
|
+
"""Get user info from the active profile."""
|
|
119
|
+
profile = get_profile(profile_name)
|
|
120
|
+
return profile.get("user")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def set_auth(instance_url: str, token: str, user: Optional[dict] = None,
|
|
124
|
+
profile_name: Optional[str] = None):
|
|
125
|
+
"""Store auth credentials in a profile."""
|
|
126
|
+
config = load_config()
|
|
127
|
+
name = _resolve_profile_name(profile_name)
|
|
128
|
+
profiles = config.setdefault("profiles", {})
|
|
129
|
+
profile = profiles.setdefault(name, {})
|
|
130
|
+
profile["instance_url"] = instance_url.rstrip("/")
|
|
131
|
+
profile["token"] = token
|
|
132
|
+
if user:
|
|
133
|
+
profile["user"] = user
|
|
134
|
+
config["current_profile"] = name
|
|
135
|
+
save_config(config)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def clear_auth(profile_name: Optional[str] = None):
|
|
139
|
+
"""Clear token and user from a profile."""
|
|
140
|
+
config = load_config()
|
|
141
|
+
name = _resolve_profile_name(profile_name)
|
|
142
|
+
profile = config.get("profiles", {}).get(name, {})
|
|
143
|
+
profile.pop("token", None)
|
|
144
|
+
profile.pop("user", None)
|
|
145
|
+
save_config(config)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def list_profiles() -> list[dict]:
|
|
149
|
+
"""List all profiles with metadata."""
|
|
150
|
+
config = load_config()
|
|
151
|
+
current = config.get("current_profile", "default")
|
|
152
|
+
profiles = config.get("profiles", {})
|
|
153
|
+
result = []
|
|
154
|
+
for name, data in profiles.items():
|
|
155
|
+
result.append({
|
|
156
|
+
"name": name,
|
|
157
|
+
"instance_url": data.get("instance_url", ""),
|
|
158
|
+
"user": (data.get("user", {}) or {}).get("email", ""),
|
|
159
|
+
"active": name == current,
|
|
160
|
+
})
|
|
161
|
+
return result
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def set_current_profile(name: str) -> bool:
|
|
165
|
+
"""Switch to a different profile. Returns False if profile doesn't exist."""
|
|
166
|
+
config = load_config()
|
|
167
|
+
if name not in config.get("profiles", {}):
|
|
168
|
+
return False
|
|
169
|
+
config["current_profile"] = name
|
|
170
|
+
save_config(config)
|
|
171
|
+
return True
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def remove_profile(name: str) -> bool:
|
|
175
|
+
"""Remove a profile. Returns False if it doesn't exist."""
|
|
176
|
+
config = load_config()
|
|
177
|
+
profiles = config.get("profiles", {})
|
|
178
|
+
if name not in profiles:
|
|
179
|
+
return False
|
|
180
|
+
del profiles[name]
|
|
181
|
+
# If we removed the active profile, switch to first remaining (or clear)
|
|
182
|
+
if config.get("current_profile") == name:
|
|
183
|
+
config["current_profile"] = next(iter(profiles), "default")
|
|
184
|
+
save_config(config)
|
|
185
|
+
return True
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def profile_name_from_url(url: str) -> str:
|
|
189
|
+
"""Derive a profile name from an instance URL (uses hostname)."""
|
|
190
|
+
parsed = urlparse(url)
|
|
191
|
+
hostname = parsed.hostname or "default"
|
|
192
|
+
# Use just hostname, stripping port
|
|
193
|
+
return hostname
|
trinity_cli/main.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Trinity CLI — main entry point.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
trinity init # Set up and authenticate
|
|
5
|
+
trinity login # Log in to an instance
|
|
6
|
+
trinity agents list # List agents
|
|
7
|
+
trinity chat my-agent "hello" # Chat with an agent
|
|
8
|
+
trinity logs my-agent # View agent logs
|
|
9
|
+
trinity profile list # Show all profiles
|
|
10
|
+
trinity profile use prod # Switch to a profile
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
|
|
15
|
+
from . import __version__
|
|
16
|
+
from .commands.agents import agents
|
|
17
|
+
from .commands.auth import init, login, logout, status
|
|
18
|
+
from .commands.chat import chat_history, chat_with_agent, logs
|
|
19
|
+
from .commands.health import health
|
|
20
|
+
from .commands.profiles import profile
|
|
21
|
+
from .commands.schedules import schedules
|
|
22
|
+
from .commands.skills import skills
|
|
23
|
+
from .commands.tags import tags
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@click.group()
|
|
27
|
+
@click.version_option(version=__version__, prog_name="trinity")
|
|
28
|
+
@click.option("--profile", "profile_name", envvar="TRINITY_PROFILE", default=None,
|
|
29
|
+
help="Profile to use (overrides TRINITY_PROFILE env var)")
|
|
30
|
+
@click.pass_context
|
|
31
|
+
def cli(ctx, profile_name):
|
|
32
|
+
"""Trinity — Autonomous Agent Orchestration Platform CLI.
|
|
33
|
+
|
|
34
|
+
Get started:
|
|
35
|
+
|
|
36
|
+
trinity init Configure instance and log in
|
|
37
|
+
|
|
38
|
+
trinity agents list List your agents
|
|
39
|
+
|
|
40
|
+
trinity chat <agent> "message" Chat with an agent
|
|
41
|
+
|
|
42
|
+
Manage multiple instances with profiles:
|
|
43
|
+
|
|
44
|
+
trinity profile list Show configured profiles
|
|
45
|
+
|
|
46
|
+
trinity profile use <name> Switch active profile
|
|
47
|
+
"""
|
|
48
|
+
ctx.ensure_object(dict)
|
|
49
|
+
ctx.obj["profile"] = profile_name
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Auth commands (top-level)
|
|
53
|
+
cli.add_command(init)
|
|
54
|
+
cli.add_command(login)
|
|
55
|
+
cli.add_command(logout)
|
|
56
|
+
cli.add_command(status)
|
|
57
|
+
|
|
58
|
+
# Profile management
|
|
59
|
+
cli.add_command(profile)
|
|
60
|
+
|
|
61
|
+
# Resource commands (groups)
|
|
62
|
+
cli.add_command(agents)
|
|
63
|
+
cli.add_command(health)
|
|
64
|
+
cli.add_command(skills)
|
|
65
|
+
cli.add_command(schedules)
|
|
66
|
+
cli.add_command(tags)
|
|
67
|
+
|
|
68
|
+
# Standalone commands
|
|
69
|
+
cli.add_command(chat_with_agent)
|
|
70
|
+
cli.add_command(chat_history, name="history")
|
|
71
|
+
cli.add_command(logs)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def main():
|
|
75
|
+
cli()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
main()
|
trinity_cli/output.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Output formatting for Trinity CLI.
|
|
2
|
+
|
|
3
|
+
JSON by default (for piping/scripting). --format table for humans.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def format_output(data: Any, fmt: str = "json"):
|
|
14
|
+
"""Format and print data according to the chosen format."""
|
|
15
|
+
if fmt == "json":
|
|
16
|
+
click.echo(json.dumps(data, indent=2, default=str))
|
|
17
|
+
elif fmt == "table":
|
|
18
|
+
if isinstance(data, list):
|
|
19
|
+
_print_table(data)
|
|
20
|
+
elif isinstance(data, dict):
|
|
21
|
+
_print_dict(data)
|
|
22
|
+
else:
|
|
23
|
+
click.echo(str(data))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _print_table(rows: list[dict]):
|
|
27
|
+
"""Print a list of dicts as a table."""
|
|
28
|
+
if not rows:
|
|
29
|
+
click.echo("(no results)")
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
# Use rich for nice tables
|
|
33
|
+
from rich.console import Console
|
|
34
|
+
from rich.table import Table
|
|
35
|
+
|
|
36
|
+
console = Console(file=sys.stdout)
|
|
37
|
+
table = Table(show_header=True, header_style="bold")
|
|
38
|
+
|
|
39
|
+
keys = list(rows[0].keys())
|
|
40
|
+
for key in keys:
|
|
41
|
+
table.add_column(key)
|
|
42
|
+
|
|
43
|
+
for row in rows:
|
|
44
|
+
table.add_row(*[str(row.get(k, "")) for k in keys])
|
|
45
|
+
|
|
46
|
+
console.print(table)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _print_dict(data: dict):
|
|
50
|
+
"""Print a single dict as key-value pairs."""
|
|
51
|
+
from rich.console import Console
|
|
52
|
+
from rich.table import Table
|
|
53
|
+
|
|
54
|
+
console = Console(file=sys.stdout)
|
|
55
|
+
table = Table(show_header=True, header_style="bold")
|
|
56
|
+
table.add_column("Field")
|
|
57
|
+
table.add_column("Value")
|
|
58
|
+
|
|
59
|
+
for key, value in data.items():
|
|
60
|
+
table.add_row(str(key), str(value))
|
|
61
|
+
|
|
62
|
+
console.print(table)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: trinity-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI for the Trinity Autonomous Agent Orchestration Platform
|
|
5
|
+
Author-email: Ability AI <hello@ability.ai>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/abilityai/trinity
|
|
8
|
+
Project-URL: Documentation, https://github.com/abilityai/trinity/blob/main/docs/CLI.md
|
|
9
|
+
Project-URL: Repository, https://github.com/abilityai/trinity
|
|
10
|
+
Project-URL: Issues, https://github.com/abilityai/trinity/issues
|
|
11
|
+
Keywords: trinity,ai,agents,orchestration,cli
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Classifier: Topic :: System :: Systems Administration
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
Requires-Dist: click>=8.0
|
|
25
|
+
Requires-Dist: httpx>=0.24
|
|
26
|
+
Requires-Dist: rich>=13.0
|
|
27
|
+
|
|
28
|
+
# Trinity CLI
|
|
29
|
+
|
|
30
|
+
Command-line interface for the [Trinity](https://github.com/abilityai/trinity) Autonomous Agent Orchestration Platform.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# With pip
|
|
36
|
+
pip install trinity-cli
|
|
37
|
+
|
|
38
|
+
# With pipx (recommended — isolated environment)
|
|
39
|
+
pipx install trinity-cli
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Connect to your Trinity instance
|
|
46
|
+
trinity init
|
|
47
|
+
|
|
48
|
+
# List your agents
|
|
49
|
+
trinity agents list
|
|
50
|
+
|
|
51
|
+
# Chat with an agent
|
|
52
|
+
trinity chat my-agent "Hello, what can you do?"
|
|
53
|
+
|
|
54
|
+
# Check fleet health
|
|
55
|
+
trinity health fleet
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Multi-Instance Profiles
|
|
59
|
+
|
|
60
|
+
Manage multiple Trinity instances (local dev, staging, production):
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# First instance (created during init)
|
|
64
|
+
trinity init
|
|
65
|
+
|
|
66
|
+
# Add another instance
|
|
67
|
+
trinity init --profile production
|
|
68
|
+
|
|
69
|
+
# Switch between instances
|
|
70
|
+
trinity profile use production
|
|
71
|
+
trinity profile list
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Commands
|
|
75
|
+
|
|
76
|
+
| Command | Description |
|
|
77
|
+
|---------|-------------|
|
|
78
|
+
| `trinity init` | Connect to a Trinity instance |
|
|
79
|
+
| `trinity login` | Re-authenticate with stored instance |
|
|
80
|
+
| `trinity agents list` | List all agents |
|
|
81
|
+
| `trinity agents create <name>` | Create a new agent |
|
|
82
|
+
| `trinity agents start <name>` | Start an agent |
|
|
83
|
+
| `trinity agents stop <name>` | Stop an agent |
|
|
84
|
+
| `trinity chat <agent> "msg"` | Chat with an agent |
|
|
85
|
+
| `trinity history <agent>` | View chat history |
|
|
86
|
+
| `trinity logs <agent>` | View agent logs |
|
|
87
|
+
| `trinity health fleet` | Fleet health overview |
|
|
88
|
+
| `trinity health agent <name>` | Single agent health |
|
|
89
|
+
| `trinity skills list` | Browse skill library |
|
|
90
|
+
| `trinity schedules list <agent>` | View agent schedules |
|
|
91
|
+
| `trinity profile list` | List configured profiles |
|
|
92
|
+
| `trinity profile use <name>` | Switch active profile |
|
|
93
|
+
|
|
94
|
+
## Output Formats
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# JSON (default)
|
|
98
|
+
trinity agents list
|
|
99
|
+
|
|
100
|
+
# Table
|
|
101
|
+
trinity agents list --format table
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Environment Variables
|
|
105
|
+
|
|
106
|
+
| Variable | Description |
|
|
107
|
+
|----------|-------------|
|
|
108
|
+
| `TRINITY_URL` | Override instance URL |
|
|
109
|
+
| `TRINITY_API_KEY` | Override auth token |
|
|
110
|
+
| `TRINITY_PROFILE` | Override active profile |
|
|
111
|
+
|
|
112
|
+
## Documentation
|
|
113
|
+
|
|
114
|
+
- [Full CLI docs](https://github.com/abilityai/trinity/blob/main/docs/CLI.md)
|
|
115
|
+
- [Trinity Platform](https://github.com/abilityai/trinity)
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
MIT
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
trinity_cli/__init__.py,sha256=v-Zb6iFOT2o-v4fgNbQ2Lz6Cu5LTOsrBddEGZOwRVUA,100
|
|
2
|
+
trinity_cli/client.py,sha256=CsttCmygZ6xxDw5GUrF9RjeXTeYEWmL5eQdKE9RRc5k,4122
|
|
3
|
+
trinity_cli/config.py,sha256=G7WZJiWlYlT9ujWMetm5MCZ2pIZqxpCqSe3g-CgP-Bw,5941
|
|
4
|
+
trinity_cli/main.py,sha256=BIKI4kVMHUQbVH7WpSfTu0Lutf-5891I-_YBSnn2lKE,2107
|
|
5
|
+
trinity_cli/output.py,sha256=E247ugQw6VMD5p7iOu9BxcuIUe0q4xByXiZdkpTFJhU,1529
|
|
6
|
+
trinity_cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
trinity_cli/commands/agents.py,sha256=opvgFbpLTTxurdGcgVo-ojKKA3XA0XJ_ogCwuNpY9Hg,2739
|
|
8
|
+
trinity_cli/commands/auth.py,sha256=d2Uq05PPdPrh4XlNyg7IDEn5vfI0Wo5EG-g8v_Dj-Yc,6032
|
|
9
|
+
trinity_cli/commands/chat.py,sha256=CCIwR4CbqKGUfY0HecAOez9Sj6mYOW-a7iIbvezTW9k,1980
|
|
10
|
+
trinity_cli/commands/health.py,sha256=RaiNVGLESZfjpXhoadiKHret8wLcFIIlxIdGXaevKNQ,868
|
|
11
|
+
trinity_cli/commands/profiles.py,sha256=rXvSWF9KnLP10-1fd5DdCKpDBLXHxNZWcXCwMyEmB2A,1650
|
|
12
|
+
trinity_cli/commands/schedules.py,sha256=pMyp172bGsKgaPP8eyLjBndv8TmA1FWGldPUwE2mBKM,1261
|
|
13
|
+
trinity_cli/commands/skills.py,sha256=lDcNtFDXaG45ecnUgzl_D95sRwbrRwfBrKJuKepgelk,1171
|
|
14
|
+
trinity_cli/commands/tags.py,sha256=KBM1b_2af7cAO0D1xQHgXRkV_F-XDjJM_gP795Yn-hU,800
|
|
15
|
+
trinity_cli-0.1.0.dist-info/METADATA,sha256=88XFwWQBwv6a51gbhDVWHv1lk4WCLiPdqK_NZgBVFbw,3295
|
|
16
|
+
trinity_cli-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
17
|
+
trinity_cli-0.1.0.dist-info/entry_points.txt,sha256=Soep8vtpg25_4TTBoU-69WQKtjBUVn2r2KQNY_kpkws,49
|
|
18
|
+
trinity_cli-0.1.0.dist-info/top_level.txt,sha256=xlhMZagfk4_iC3gfgPDXnPiM5dqnBScErFvzSF02KIA,12
|
|
19
|
+
trinity_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
trinity_cli
|