trinity-cli 0.1.0__tar.gz → 0.2.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.
- {trinity_cli-0.1.0 → trinity_cli-0.2.0}/PKG-INFO +5 -4
- {trinity_cli-0.1.0 → trinity_cli-0.2.0}/README.md +3 -3
- {trinity_cli-0.1.0 → trinity_cli-0.2.0}/pyproject.toml +2 -1
- trinity_cli-0.2.0/trinity_cli/__init__.py +8 -0
- {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli/commands/agents.py +3 -3
- {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli/commands/auth.py +114 -16
- {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli/commands/chat.py +3 -3
- trinity_cli-0.2.0/trinity_cli/commands/deploy.py +269 -0
- {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli/commands/health.py +2 -2
- {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli/commands/schedules.py +1 -1
- {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli/commands/skills.py +2 -2
- {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli/commands/tags.py +2 -2
- {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli/config.py +9 -0
- {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli/main.py +5 -0
- {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli/output.py +2 -2
- {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli.egg-info/PKG-INFO +5 -4
- {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli.egg-info/SOURCES.txt +1 -0
- {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli.egg-info/requires.txt +1 -0
- trinity_cli-0.1.0/trinity_cli/__init__.py +0 -3
- {trinity_cli-0.1.0 → trinity_cli-0.2.0}/setup.cfg +0 -0
- {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli/client.py +0 -0
- {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli/commands/__init__.py +0 -0
- {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli/commands/profiles.py +0 -0
- {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli.egg-info/dependency_links.txt +0 -0
- {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli.egg-info/entry_points.txt +0 -0
- {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: trinity-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: CLI for the Trinity Autonomous Agent Orchestration Platform
|
|
5
5
|
Author-email: Ability AI <hello@ability.ai>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -23,6 +23,7 @@ Requires-Python: >=3.10
|
|
|
23
23
|
Description-Content-Type: text/markdown
|
|
24
24
|
Requires-Dist: click>=8.0
|
|
25
25
|
Requires-Dist: httpx>=0.24
|
|
26
|
+
Requires-Dist: pyyaml>=6.0
|
|
26
27
|
Requires-Dist: rich>=13.0
|
|
27
28
|
|
|
28
29
|
# Trinity CLI
|
|
@@ -94,11 +95,11 @@ trinity profile list
|
|
|
94
95
|
## Output Formats
|
|
95
96
|
|
|
96
97
|
```bash
|
|
97
|
-
#
|
|
98
|
+
# Table (default, human-readable)
|
|
98
99
|
trinity agents list
|
|
99
100
|
|
|
100
|
-
#
|
|
101
|
-
trinity agents list --format
|
|
101
|
+
# JSON (for piping/scripting)
|
|
102
|
+
trinity agents list --format json
|
|
102
103
|
```
|
|
103
104
|
|
|
104
105
|
## Environment Variables
|
|
@@ -67,11 +67,11 @@ trinity profile list
|
|
|
67
67
|
## Output Formats
|
|
68
68
|
|
|
69
69
|
```bash
|
|
70
|
-
#
|
|
70
|
+
# Table (default, human-readable)
|
|
71
71
|
trinity agents list
|
|
72
72
|
|
|
73
|
-
#
|
|
74
|
-
trinity agents list --format
|
|
73
|
+
# JSON (for piping/scripting)
|
|
74
|
+
trinity agents list --format json
|
|
75
75
|
```
|
|
76
76
|
|
|
77
77
|
## Environment Variables
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "trinity-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "CLI for the Trinity Autonomous Agent Orchestration Platform"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -28,6 +28,7 @@ classifiers = [
|
|
|
28
28
|
dependencies = [
|
|
29
29
|
"click>=8.0",
|
|
30
30
|
"httpx>=0.24",
|
|
31
|
+
"pyyaml>=6.0",
|
|
31
32
|
"rich>=13.0",
|
|
32
33
|
]
|
|
33
34
|
|
|
@@ -13,7 +13,7 @@ def agents():
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
@agents.command("list")
|
|
16
|
-
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="
|
|
16
|
+
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="table", help="Output format")
|
|
17
17
|
def list_agents(fmt):
|
|
18
18
|
"""List all agents."""
|
|
19
19
|
client = TrinityClient()
|
|
@@ -36,7 +36,7 @@ def list_agents(fmt):
|
|
|
36
36
|
|
|
37
37
|
@agents.command("get")
|
|
38
38
|
@click.argument("name")
|
|
39
|
-
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="
|
|
39
|
+
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="table", help="Output format")
|
|
40
40
|
def get_agent(name, fmt):
|
|
41
41
|
"""Get agent details."""
|
|
42
42
|
client = TrinityClient()
|
|
@@ -47,7 +47,7 @@ def get_agent(name, fmt):
|
|
|
47
47
|
@agents.command("create")
|
|
48
48
|
@click.argument("name")
|
|
49
49
|
@click.option("--template", default=None, help="Template (e.g. github:Org/repo)")
|
|
50
|
-
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="
|
|
50
|
+
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="table", help="Output format")
|
|
51
51
|
def create_agent(name, template, fmt):
|
|
52
52
|
"""Create a new agent."""
|
|
53
53
|
client = TrinityClient()
|
|
@@ -1,14 +1,85 @@
|
|
|
1
1
|
"""Authentication commands: login, logout, status, init."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
3
6
|
import click
|
|
4
7
|
|
|
5
8
|
from ..client import TrinityClient, TrinityAPIError
|
|
6
9
|
from ..config import (
|
|
7
10
|
clear_auth, get_instance_url, get_user, load_config,
|
|
8
|
-
profile_name_from_url, set_auth, _resolve_profile_name,
|
|
11
|
+
profile_name_from_url, set_auth, set_profile_key, _resolve_profile_name,
|
|
9
12
|
)
|
|
10
13
|
|
|
11
14
|
|
|
15
|
+
def _provision_mcp_key(client: TrinityClient, profile_name: str):
|
|
16
|
+
"""Ensure the user has an MCP API key and store it in the profile."""
|
|
17
|
+
try:
|
|
18
|
+
result = client.post("/api/mcp/keys/ensure-default")
|
|
19
|
+
if result and result.get("api_key"):
|
|
20
|
+
set_profile_key("mcp_api_key", result["api_key"], profile_name)
|
|
21
|
+
click.echo(f"MCP API key provisioned and saved to profile")
|
|
22
|
+
return result["api_key"]
|
|
23
|
+
except TrinityAPIError:
|
|
24
|
+
# Non-fatal — user can still use JWT auth
|
|
25
|
+
pass
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _write_mcp_json(instance_url: str, mcp_api_key: str):
|
|
30
|
+
"""Write or merge Trinity MCP server config into .mcp.json in current directory."""
|
|
31
|
+
mcp_path = Path.cwd() / ".mcp.json"
|
|
32
|
+
config = {}
|
|
33
|
+
if mcp_path.exists():
|
|
34
|
+
try:
|
|
35
|
+
config = json.loads(mcp_path.read_text())
|
|
36
|
+
except (json.JSONDecodeError, OSError):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
servers = config.setdefault("mcpServers", {})
|
|
40
|
+
# Derive MCP endpoint from instance URL (replace backend port with MCP port)
|
|
41
|
+
mcp_url = instance_url.rstrip("/")
|
|
42
|
+
if mcp_url.endswith(":8000"):
|
|
43
|
+
mcp_url = mcp_url.replace(":8000", ":8080")
|
|
44
|
+
elif ":" not in mcp_url.split("//")[-1]:
|
|
45
|
+
# No port specified — assume /mcp path on same host
|
|
46
|
+
mcp_url = mcp_url + ":8080"
|
|
47
|
+
|
|
48
|
+
servers["trinity"] = {
|
|
49
|
+
"type": "streamable-http",
|
|
50
|
+
"url": f"{mcp_url}/mcp",
|
|
51
|
+
"headers": {
|
|
52
|
+
"Authorization": f"Bearer {mcp_api_key}"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
mcp_path.write_text(json.dumps(config, indent=2) + "\n")
|
|
57
|
+
click.echo(f"MCP server config written to {mcp_path}")
|
|
58
|
+
|
|
59
|
+
# Add .mcp.json to .gitignore if in a git repo (contains API key)
|
|
60
|
+
cwd = Path.cwd()
|
|
61
|
+
if (cwd / ".git").exists():
|
|
62
|
+
gitignore = cwd / ".gitignore"
|
|
63
|
+
marker = ".mcp.json"
|
|
64
|
+
if gitignore.exists():
|
|
65
|
+
content = gitignore.read_text()
|
|
66
|
+
if marker not in content:
|
|
67
|
+
with open(gitignore, "a") as f:
|
|
68
|
+
f.write(f"\n{marker}\n")
|
|
69
|
+
else:
|
|
70
|
+
gitignore.write_text(f"{marker}\n")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _normalize_url(url: str) -> str:
|
|
74
|
+
"""Normalize a URL: add https:// if no scheme, strip trailing slash."""
|
|
75
|
+
url = url.strip().rstrip("/")
|
|
76
|
+
if not url:
|
|
77
|
+
return url
|
|
78
|
+
if not url.startswith(("http://", "https://")):
|
|
79
|
+
url = f"https://{url}"
|
|
80
|
+
return url
|
|
81
|
+
|
|
82
|
+
|
|
12
83
|
def _get_profile_name(ctx: click.Context) -> str | None:
|
|
13
84
|
"""Extract the --profile value from the root context."""
|
|
14
85
|
root = ctx.find_root()
|
|
@@ -26,9 +97,21 @@ def login(ctx, instance, profile_opt):
|
|
|
26
97
|
url = instance or get_instance_url(profile_name)
|
|
27
98
|
if not url:
|
|
28
99
|
url = click.prompt("Trinity instance URL")
|
|
29
|
-
url = url
|
|
30
|
-
|
|
31
|
-
|
|
100
|
+
url = _normalize_url(url)
|
|
101
|
+
|
|
102
|
+
# Verify instance is reachable (with retry)
|
|
103
|
+
for attempt in range(3):
|
|
104
|
+
client = TrinityClient(base_url=url, token="none")
|
|
105
|
+
try:
|
|
106
|
+
client.get_unauthenticated("/api/auth/mode")
|
|
107
|
+
break
|
|
108
|
+
except Exception:
|
|
109
|
+
click.echo(f"Cannot reach {url}.", err=True)
|
|
110
|
+
if attempt < 2:
|
|
111
|
+
url = _normalize_url(click.prompt("Try a different URL"))
|
|
112
|
+
else:
|
|
113
|
+
click.echo("Giving up after 3 attempts.", err=True)
|
|
114
|
+
raise SystemExit(1)
|
|
32
115
|
|
|
33
116
|
email = click.prompt("Email")
|
|
34
117
|
|
|
@@ -61,6 +144,10 @@ def login(ctx, instance, profile_opt):
|
|
|
61
144
|
name = user.get("name") or user.get("email") or user.get("username") if user else email
|
|
62
145
|
click.echo(f"Logged in as {name} [profile: {target_profile}]")
|
|
63
146
|
|
|
147
|
+
# Auto-provision MCP API key
|
|
148
|
+
authed_client = TrinityClient(base_url=url, token=token)
|
|
149
|
+
_provision_mcp_key(authed_client, target_profile)
|
|
150
|
+
|
|
64
151
|
|
|
65
152
|
@click.command()
|
|
66
153
|
@click.pass_context
|
|
@@ -119,18 +206,22 @@ def init(ctx, profile_opt):
|
|
|
119
206
|
for the instance (defaults to hostname).
|
|
120
207
|
"""
|
|
121
208
|
url = click.prompt("Trinity instance URL", default="http://localhost:8000")
|
|
122
|
-
url = url
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
209
|
+
url = _normalize_url(url)
|
|
210
|
+
|
|
211
|
+
# Verify instance is reachable (with retry)
|
|
212
|
+
for attempt in range(3):
|
|
213
|
+
client = TrinityClient(base_url=url, token="none")
|
|
214
|
+
try:
|
|
215
|
+
client.get_unauthenticated("/api/auth/mode")
|
|
216
|
+
click.echo(f"Connected to {url}")
|
|
217
|
+
break
|
|
218
|
+
except Exception:
|
|
219
|
+
click.echo(f"Cannot reach {url}.", err=True)
|
|
220
|
+
if attempt < 2:
|
|
221
|
+
url = _normalize_url(click.prompt("Try a different URL"))
|
|
222
|
+
else:
|
|
223
|
+
click.echo("Giving up after 3 attempts.", err=True)
|
|
224
|
+
raise SystemExit(1)
|
|
134
225
|
|
|
135
226
|
# Determine profile name
|
|
136
227
|
profile_name = profile_opt or _get_profile_name(ctx) or profile_name_from_url(url)
|
|
@@ -174,4 +265,11 @@ def init(ctx, profile_opt):
|
|
|
174
265
|
set_auth(url, token, user, profile_name=profile_name)
|
|
175
266
|
name = user.get("name") or user.get("email") or user.get("username") if user else email
|
|
176
267
|
click.echo(f"Logged in as {name} [profile: {profile_name}]")
|
|
268
|
+
|
|
269
|
+
# Auto-provision MCP API key and write .mcp.json
|
|
270
|
+
authed_client = TrinityClient(base_url=url, token=token)
|
|
271
|
+
mcp_key = _provision_mcp_key(authed_client, profile_name)
|
|
272
|
+
if mcp_key:
|
|
273
|
+
_write_mcp_json(url, mcp_key)
|
|
274
|
+
|
|
177
275
|
click.echo(f"\nTrinity CLI is ready. Try 'trinity agents list'.")
|
|
@@ -9,7 +9,7 @@ from ..output import format_output
|
|
|
9
9
|
@click.command("chat")
|
|
10
10
|
@click.argument("agent")
|
|
11
11
|
@click.argument("message")
|
|
12
|
-
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="
|
|
12
|
+
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="table", help="Output format")
|
|
13
13
|
def chat_with_agent(agent, message, fmt):
|
|
14
14
|
"""Send a message to an agent.
|
|
15
15
|
|
|
@@ -33,7 +33,7 @@ def chat_history_group():
|
|
|
33
33
|
|
|
34
34
|
@click.command("history")
|
|
35
35
|
@click.argument("agent")
|
|
36
|
-
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="
|
|
36
|
+
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="table", help="Output format")
|
|
37
37
|
def chat_history(agent, fmt):
|
|
38
38
|
"""Get chat history for an agent."""
|
|
39
39
|
client = TrinityClient()
|
|
@@ -44,7 +44,7 @@ def chat_history(agent, fmt):
|
|
|
44
44
|
@click.command("logs")
|
|
45
45
|
@click.argument("agent")
|
|
46
46
|
@click.option("--tail", default=50, help="Number of log lines")
|
|
47
|
-
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="
|
|
47
|
+
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="table", help="Output format")
|
|
48
48
|
def logs(agent, tail, fmt):
|
|
49
49
|
"""View agent container logs.
|
|
50
50
|
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""Deploy command: package and deploy a local agent directory."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import io
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import tarfile
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
import yaml
|
|
13
|
+
|
|
14
|
+
from ..client import TrinityClient, TrinityAPIError
|
|
15
|
+
from ..config import get_instance_url, _resolve_profile_name
|
|
16
|
+
|
|
17
|
+
# Directories/files always excluded from the archive
|
|
18
|
+
ALWAYS_EXCLUDE = {
|
|
19
|
+
".git",
|
|
20
|
+
"node_modules",
|
|
21
|
+
"__pycache__",
|
|
22
|
+
".venv",
|
|
23
|
+
"venv",
|
|
24
|
+
".trinity-remote.yaml",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# File patterns excluded (matched by name)
|
|
28
|
+
EXCLUDE_PATTERNS = {".env", ".env.local", ".env.production"}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _get_gitignore_patterns(root: Path) -> list[str]:
|
|
32
|
+
"""Read .gitignore and return list of patterns."""
|
|
33
|
+
gitignore = root / ".gitignore"
|
|
34
|
+
if not gitignore.exists():
|
|
35
|
+
return []
|
|
36
|
+
patterns = []
|
|
37
|
+
for line in gitignore.read_text().splitlines():
|
|
38
|
+
line = line.strip()
|
|
39
|
+
if line and not line.startswith("#"):
|
|
40
|
+
patterns.append(line)
|
|
41
|
+
return patterns
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _is_git_repo(path: Path) -> bool:
|
|
45
|
+
return (path / ".git").exists()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _git_ls_files(root: Path) -> list[str]:
|
|
49
|
+
"""Use git ls-files to get tracked + untracked (non-ignored) files."""
|
|
50
|
+
try:
|
|
51
|
+
result = subprocess.run(
|
|
52
|
+
["git", "ls-files", "--cached", "--others", "--exclude-standard"],
|
|
53
|
+
cwd=root,
|
|
54
|
+
capture_output=True,
|
|
55
|
+
text=True,
|
|
56
|
+
timeout=30,
|
|
57
|
+
)
|
|
58
|
+
if result.returncode == 0:
|
|
59
|
+
return [f for f in result.stdout.strip().splitlines() if f]
|
|
60
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
61
|
+
pass
|
|
62
|
+
return []
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _should_exclude(rel_path: str) -> bool:
|
|
66
|
+
"""Check if a relative path should be excluded."""
|
|
67
|
+
parts = Path(rel_path).parts
|
|
68
|
+
for part in parts:
|
|
69
|
+
if part in ALWAYS_EXCLUDE:
|
|
70
|
+
return True
|
|
71
|
+
if Path(rel_path).name in EXCLUDE_PATTERNS:
|
|
72
|
+
return True
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _create_archive(root: Path) -> bytes:
|
|
77
|
+
"""Create a tar.gz archive of the agent directory."""
|
|
78
|
+
buf = io.BytesIO()
|
|
79
|
+
|
|
80
|
+
if _is_git_repo(root):
|
|
81
|
+
# Use git to determine which files to include
|
|
82
|
+
files = _git_ls_files(root)
|
|
83
|
+
if not files:
|
|
84
|
+
# Fallback to walking if git ls-files fails
|
|
85
|
+
files = None
|
|
86
|
+
else:
|
|
87
|
+
files = None
|
|
88
|
+
|
|
89
|
+
with tarfile.open(fileobj=buf, mode="w:gz") as tar:
|
|
90
|
+
if files is not None:
|
|
91
|
+
for rel in files:
|
|
92
|
+
if _should_exclude(rel):
|
|
93
|
+
continue
|
|
94
|
+
full = root / rel
|
|
95
|
+
if full.is_file():
|
|
96
|
+
tar.add(str(full), arcname=rel)
|
|
97
|
+
else:
|
|
98
|
+
# Walk directory manually
|
|
99
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
100
|
+
# Prune excluded directories in-place
|
|
101
|
+
dirnames[:] = [
|
|
102
|
+
d for d in dirnames
|
|
103
|
+
if d not in ALWAYS_EXCLUDE and not d.startswith(".")
|
|
104
|
+
]
|
|
105
|
+
for fname in filenames:
|
|
106
|
+
full = Path(dirpath) / fname
|
|
107
|
+
rel = full.relative_to(root)
|
|
108
|
+
if _should_exclude(str(rel)):
|
|
109
|
+
continue
|
|
110
|
+
tar.add(str(full), arcname=str(rel))
|
|
111
|
+
|
|
112
|
+
return buf.getvalue()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _load_tracking(root: Path) -> dict | None:
|
|
116
|
+
"""Load .trinity-remote.yaml if it exists."""
|
|
117
|
+
tracking_file = root / ".trinity-remote.yaml"
|
|
118
|
+
if not tracking_file.exists():
|
|
119
|
+
return None
|
|
120
|
+
try:
|
|
121
|
+
return yaml.safe_load(tracking_file.read_text())
|
|
122
|
+
except (yaml.YAMLError, OSError):
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _save_tracking(root: Path, instance_url: str, agent_name: str, profile_name: str):
|
|
127
|
+
"""Write .trinity-remote.yaml tracking file."""
|
|
128
|
+
tracking_file = root / ".trinity-remote.yaml"
|
|
129
|
+
data = {
|
|
130
|
+
"instance": instance_url,
|
|
131
|
+
"agent": agent_name,
|
|
132
|
+
"profile": profile_name,
|
|
133
|
+
"deployed_at": datetime.now(timezone.utc).isoformat(),
|
|
134
|
+
}
|
|
135
|
+
tracking_file.write_text(
|
|
136
|
+
"# Auto-generated by trinity deploy — do not edit\n"
|
|
137
|
+
+ yaml.dump(data, default_flow_style=False)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Add to .gitignore if in a git repo and not already listed
|
|
141
|
+
if _is_git_repo(root):
|
|
142
|
+
gitignore = root / ".gitignore"
|
|
143
|
+
marker = ".trinity-remote.yaml"
|
|
144
|
+
if gitignore.exists():
|
|
145
|
+
content = gitignore.read_text()
|
|
146
|
+
if marker not in content:
|
|
147
|
+
with open(gitignore, "a") as f:
|
|
148
|
+
f.write(f"\n{marker}\n")
|
|
149
|
+
else:
|
|
150
|
+
gitignore.write_text(f"{marker}\n")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@click.command()
|
|
154
|
+
@click.argument("path", default=".", type=click.Path(exists=True, file_okay=False, resolve_path=True))
|
|
155
|
+
@click.option("--name", default=None, help="Override agent name (default: directory name or template.yaml name)")
|
|
156
|
+
@click.option("--repo", default=None, help="Deploy from a public GitHub repo instead of local files (e.g. user/repo)")
|
|
157
|
+
@click.pass_context
|
|
158
|
+
def deploy(ctx, path, name, repo):
|
|
159
|
+
"""Deploy a local agent directory to Trinity.
|
|
160
|
+
|
|
161
|
+
Packages the directory, uploads it, and creates/updates the agent.
|
|
162
|
+
On first deploy, writes .trinity-remote.yaml for tracking.
|
|
163
|
+
Subsequent deploys update the same agent automatically.
|
|
164
|
+
|
|
165
|
+
\b
|
|
166
|
+
Examples:
|
|
167
|
+
trinity deploy . Deploy current directory
|
|
168
|
+
trinity deploy ./my-agent Deploy a specific directory
|
|
169
|
+
trinity deploy . --name bot Override agent name
|
|
170
|
+
trinity deploy --repo user/repo Deploy from GitHub
|
|
171
|
+
"""
|
|
172
|
+
profile_name = None
|
|
173
|
+
root = ctx.find_root()
|
|
174
|
+
if root.obj:
|
|
175
|
+
profile_name = root.obj.get("profile")
|
|
176
|
+
|
|
177
|
+
resolved_profile = _resolve_profile_name(profile_name)
|
|
178
|
+
instance_url = get_instance_url(profile_name)
|
|
179
|
+
|
|
180
|
+
if repo:
|
|
181
|
+
# GitHub-based deploy — use existing create agent flow
|
|
182
|
+
client = TrinityClient(profile=profile_name)
|
|
183
|
+
agent_name = name or repo.split("/")[-1]
|
|
184
|
+
click.echo(f"Creating agent '{agent_name}' from github:{repo}...")
|
|
185
|
+
try:
|
|
186
|
+
result = client.post("/api/agents", json={
|
|
187
|
+
"name": agent_name,
|
|
188
|
+
"template": f"github:{repo}",
|
|
189
|
+
})
|
|
190
|
+
click.echo(f"Agent '{result['name']}' created (status: {result['status']})")
|
|
191
|
+
except TrinityAPIError as e:
|
|
192
|
+
click.echo(f"Deploy failed: {e.detail}", err=True)
|
|
193
|
+
raise SystemExit(1)
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
# File-based deploy
|
|
197
|
+
agent_dir = Path(path)
|
|
198
|
+
|
|
199
|
+
# Check for tracking file (redeploy)
|
|
200
|
+
tracking = _load_tracking(agent_dir)
|
|
201
|
+
if tracking:
|
|
202
|
+
tracked_instance = tracking.get("instance", "")
|
|
203
|
+
if instance_url and tracked_instance and tracked_instance != instance_url:
|
|
204
|
+
click.echo(
|
|
205
|
+
f"Warning: .trinity-remote.yaml points to {tracked_instance} "
|
|
206
|
+
f"but current profile targets {instance_url}",
|
|
207
|
+
err=True,
|
|
208
|
+
)
|
|
209
|
+
if not click.confirm("Deploy to current profile instance anyway?"):
|
|
210
|
+
raise SystemExit(0)
|
|
211
|
+
# Use tracked agent name for redeploy
|
|
212
|
+
if not name:
|
|
213
|
+
name = tracking.get("agent")
|
|
214
|
+
|
|
215
|
+
# Default name from directory
|
|
216
|
+
if not name:
|
|
217
|
+
template_yaml = agent_dir / "template.yaml"
|
|
218
|
+
if template_yaml.exists():
|
|
219
|
+
try:
|
|
220
|
+
tdata = yaml.safe_load(template_yaml.read_text())
|
|
221
|
+
name = tdata.get("name")
|
|
222
|
+
except (yaml.YAMLError, OSError):
|
|
223
|
+
pass
|
|
224
|
+
if not name:
|
|
225
|
+
name = agent_dir.name
|
|
226
|
+
|
|
227
|
+
click.echo(f"Packaging '{agent_dir.name}'...")
|
|
228
|
+
archive_bytes = _create_archive(agent_dir)
|
|
229
|
+
size_mb = len(archive_bytes) / (1024 * 1024)
|
|
230
|
+
click.echo(f"Archive: {size_mb:.1f} MB")
|
|
231
|
+
|
|
232
|
+
if size_mb > 50:
|
|
233
|
+
click.echo("Error: Archive exceeds 50 MB limit", err=True)
|
|
234
|
+
raise SystemExit(1)
|
|
235
|
+
|
|
236
|
+
archive_b64 = base64.b64encode(archive_bytes).decode()
|
|
237
|
+
|
|
238
|
+
client = TrinityClient(profile=profile_name)
|
|
239
|
+
|
|
240
|
+
action = "Redeploying" if tracking else "Deploying"
|
|
241
|
+
click.echo(f"{action} '{name}'...")
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
result = client.post("/api/agents/deploy-local", json={
|
|
245
|
+
"archive": archive_b64,
|
|
246
|
+
"name": name,
|
|
247
|
+
})
|
|
248
|
+
except TrinityAPIError as e:
|
|
249
|
+
click.echo(f"Deploy failed: {e.detail}", err=True)
|
|
250
|
+
raise SystemExit(1)
|
|
251
|
+
|
|
252
|
+
if result.get("status") != "success":
|
|
253
|
+
click.echo(f"Deploy failed: {result}", err=True)
|
|
254
|
+
raise SystemExit(1)
|
|
255
|
+
|
|
256
|
+
agent = result.get("agent", {})
|
|
257
|
+
versioning = result.get("versioning", {})
|
|
258
|
+
agent_name = agent.get("name", name)
|
|
259
|
+
|
|
260
|
+
click.echo(f"Agent '{agent_name}' deployed (status: {agent.get('status', 'unknown')})")
|
|
261
|
+
|
|
262
|
+
if versioning.get("previous_version"):
|
|
263
|
+
click.echo(f" Previous version: {versioning['previous_version']} (stopped: {versioning.get('previous_version_stopped', False)})")
|
|
264
|
+
if versioning.get("new_version"):
|
|
265
|
+
click.echo(f" Version: {versioning['new_version']}")
|
|
266
|
+
|
|
267
|
+
# Write tracking file
|
|
268
|
+
_save_tracking(agent_dir, instance_url, versioning.get("base_name", name), resolved_profile)
|
|
269
|
+
click.echo(f"Tracking file written: .trinity-remote.yaml")
|
|
@@ -13,7 +13,7 @@ def health():
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
@health.command("fleet")
|
|
16
|
-
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="
|
|
16
|
+
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="table", help="Output format")
|
|
17
17
|
def fleet_health(fmt):
|
|
18
18
|
"""Show fleet-wide health status."""
|
|
19
19
|
client = TrinityClient()
|
|
@@ -23,7 +23,7 @@ def fleet_health(fmt):
|
|
|
23
23
|
|
|
24
24
|
@health.command("agent")
|
|
25
25
|
@click.argument("name")
|
|
26
|
-
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="
|
|
26
|
+
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="table", help="Output format")
|
|
27
27
|
def agent_health(name, fmt):
|
|
28
28
|
"""Show health status for a specific agent."""
|
|
29
29
|
client = TrinityClient()
|
|
@@ -14,7 +14,7 @@ def schedules():
|
|
|
14
14
|
|
|
15
15
|
@schedules.command("list")
|
|
16
16
|
@click.argument("agent")
|
|
17
|
-
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="
|
|
17
|
+
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="table", help="Output format")
|
|
18
18
|
def list_schedules(agent, fmt):
|
|
19
19
|
"""List schedules for an agent."""
|
|
20
20
|
client = TrinityClient()
|
|
@@ -13,7 +13,7 @@ def skills():
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
@skills.command("list")
|
|
16
|
-
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="
|
|
16
|
+
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="table", help="Output format")
|
|
17
17
|
def list_skills(fmt):
|
|
18
18
|
"""List all available skills."""
|
|
19
19
|
client = TrinityClient()
|
|
@@ -34,7 +34,7 @@ def list_skills(fmt):
|
|
|
34
34
|
|
|
35
35
|
@skills.command("get")
|
|
36
36
|
@click.argument("name")
|
|
37
|
-
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="
|
|
37
|
+
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="table", help="Output format")
|
|
38
38
|
def get_skill(name, fmt):
|
|
39
39
|
"""Get details for a specific skill."""
|
|
40
40
|
client = TrinityClient()
|
|
@@ -13,7 +13,7 @@ def tags():
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
@tags.command("list")
|
|
16
|
-
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="
|
|
16
|
+
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="table", help="Output format")
|
|
17
17
|
def list_tags(fmt):
|
|
18
18
|
"""List all tags in use."""
|
|
19
19
|
client = TrinityClient()
|
|
@@ -23,7 +23,7 @@ def list_tags(fmt):
|
|
|
23
23
|
|
|
24
24
|
@tags.command("get")
|
|
25
25
|
@click.argument("agent")
|
|
26
|
-
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="
|
|
26
|
+
@click.option("--format", "fmt", type=click.Choice(["json", "table"]), default="table", help="Output format")
|
|
27
27
|
def get_agent_tags(agent, fmt):
|
|
28
28
|
"""Get tags for a specific agent."""
|
|
29
29
|
client = TrinityClient()
|
|
@@ -135,6 +135,15 @@ def set_auth(instance_url: str, token: str, user: Optional[dict] = None,
|
|
|
135
135
|
save_config(config)
|
|
136
136
|
|
|
137
137
|
|
|
138
|
+
def set_profile_key(key: str, value, profile_name: Optional[str] = None):
|
|
139
|
+
"""Set an arbitrary key in a profile."""
|
|
140
|
+
config = load_config()
|
|
141
|
+
name = _resolve_profile_name(profile_name)
|
|
142
|
+
profile = config.setdefault("profiles", {}).setdefault(name, {})
|
|
143
|
+
profile[key] = value
|
|
144
|
+
save_config(config)
|
|
145
|
+
|
|
146
|
+
|
|
138
147
|
def clear_auth(profile_name: Optional[str] = None):
|
|
139
148
|
"""Clear token and user from a profile."""
|
|
140
149
|
config = load_config()
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Usage:
|
|
4
4
|
trinity init # Set up and authenticate
|
|
5
5
|
trinity login # Log in to an instance
|
|
6
|
+
trinity deploy . # Deploy local agent directory
|
|
6
7
|
trinity agents list # List agents
|
|
7
8
|
trinity chat my-agent "hello" # Chat with an agent
|
|
8
9
|
trinity logs my-agent # View agent logs
|
|
@@ -16,6 +17,7 @@ from . import __version__
|
|
|
16
17
|
from .commands.agents import agents
|
|
17
18
|
from .commands.auth import init, login, logout, status
|
|
18
19
|
from .commands.chat import chat_history, chat_with_agent, logs
|
|
20
|
+
from .commands.deploy import deploy
|
|
19
21
|
from .commands.health import health
|
|
20
22
|
from .commands.profiles import profile
|
|
21
23
|
from .commands.schedules import schedules
|
|
@@ -58,6 +60,9 @@ cli.add_command(status)
|
|
|
58
60
|
# Profile management
|
|
59
61
|
cli.add_command(profile)
|
|
60
62
|
|
|
63
|
+
# Deploy command (top-level)
|
|
64
|
+
cli.add_command(deploy)
|
|
65
|
+
|
|
61
66
|
# Resource commands (groups)
|
|
62
67
|
cli.add_command(agents)
|
|
63
68
|
cli.add_command(health)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Output formatting for Trinity CLI.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Table (human-readable) by default. Use --format json for piping/scripting.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import json
|
|
@@ -10,7 +10,7 @@ from typing import Any
|
|
|
10
10
|
import click
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
def format_output(data: Any, fmt: str = "
|
|
13
|
+
def format_output(data: Any, fmt: str = "table"):
|
|
14
14
|
"""Format and print data according to the chosen format."""
|
|
15
15
|
if fmt == "json":
|
|
16
16
|
click.echo(json.dumps(data, indent=2, default=str))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: trinity-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: CLI for the Trinity Autonomous Agent Orchestration Platform
|
|
5
5
|
Author-email: Ability AI <hello@ability.ai>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -23,6 +23,7 @@ Requires-Python: >=3.10
|
|
|
23
23
|
Description-Content-Type: text/markdown
|
|
24
24
|
Requires-Dist: click>=8.0
|
|
25
25
|
Requires-Dist: httpx>=0.24
|
|
26
|
+
Requires-Dist: pyyaml>=6.0
|
|
26
27
|
Requires-Dist: rich>=13.0
|
|
27
28
|
|
|
28
29
|
# Trinity CLI
|
|
@@ -94,11 +95,11 @@ trinity profile list
|
|
|
94
95
|
## Output Formats
|
|
95
96
|
|
|
96
97
|
```bash
|
|
97
|
-
#
|
|
98
|
+
# Table (default, human-readable)
|
|
98
99
|
trinity agents list
|
|
99
100
|
|
|
100
|
-
#
|
|
101
|
-
trinity agents list --format
|
|
101
|
+
# JSON (for piping/scripting)
|
|
102
|
+
trinity agents list --format json
|
|
102
103
|
```
|
|
103
104
|
|
|
104
105
|
## Environment Variables
|
|
@@ -15,6 +15,7 @@ trinity_cli/commands/__init__.py
|
|
|
15
15
|
trinity_cli/commands/agents.py
|
|
16
16
|
trinity_cli/commands/auth.py
|
|
17
17
|
trinity_cli/commands/chat.py
|
|
18
|
+
trinity_cli/commands/deploy.py
|
|
18
19
|
trinity_cli/commands/health.py
|
|
19
20
|
trinity_cli/commands/profiles.py
|
|
20
21
|
trinity_cli/commands/schedules.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|