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.
@@ -0,0 +1,3 @@
1
+ """Trinity CLI — command-line interface for the Trinity Agent Platform."""
2
+
3
+ __version__ = "0.1.0"
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ trinity = trinity_cli.main:cli
@@ -0,0 +1 @@
1
+ trinity_cli