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.
Files changed (26) hide show
  1. {trinity_cli-0.1.0 → trinity_cli-0.2.0}/PKG-INFO +5 -4
  2. {trinity_cli-0.1.0 → trinity_cli-0.2.0}/README.md +3 -3
  3. {trinity_cli-0.1.0 → trinity_cli-0.2.0}/pyproject.toml +2 -1
  4. trinity_cli-0.2.0/trinity_cli/__init__.py +8 -0
  5. {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli/commands/agents.py +3 -3
  6. {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli/commands/auth.py +114 -16
  7. {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli/commands/chat.py +3 -3
  8. trinity_cli-0.2.0/trinity_cli/commands/deploy.py +269 -0
  9. {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli/commands/health.py +2 -2
  10. {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli/commands/schedules.py +1 -1
  11. {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli/commands/skills.py +2 -2
  12. {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli/commands/tags.py +2 -2
  13. {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli/config.py +9 -0
  14. {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli/main.py +5 -0
  15. {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli/output.py +2 -2
  16. {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli.egg-info/PKG-INFO +5 -4
  17. {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli.egg-info/SOURCES.txt +1 -0
  18. {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli.egg-info/requires.txt +1 -0
  19. trinity_cli-0.1.0/trinity_cli/__init__.py +0 -3
  20. {trinity_cli-0.1.0 → trinity_cli-0.2.0}/setup.cfg +0 -0
  21. {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli/client.py +0 -0
  22. {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli/commands/__init__.py +0 -0
  23. {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli/commands/profiles.py +0 -0
  24. {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli.egg-info/dependency_links.txt +0 -0
  25. {trinity_cli-0.1.0 → trinity_cli-0.2.0}/trinity_cli.egg-info/entry_points.txt +0 -0
  26. {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.1.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
- # JSON (default)
98
+ # Table (default, human-readable)
98
99
  trinity agents list
99
100
 
100
- # Table
101
- trinity agents list --format table
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
- # JSON (default)
70
+ # Table (default, human-readable)
71
71
  trinity agents list
72
72
 
73
- # Table
74
- trinity agents list --format table
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.1.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
 
@@ -0,0 +1,8 @@
1
+ """Trinity CLI — command-line interface for the Trinity Agent Platform."""
2
+
3
+ from importlib.metadata import version, PackageNotFoundError
4
+
5
+ try:
6
+ __version__ = version("trinity-cli")
7
+ except PackageNotFoundError:
8
+ __version__ = "dev"
@@ -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="json", help="Output format")
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="json", help="Output format")
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="json", help="Output format")
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.rstrip("/")
30
-
31
- client = TrinityClient(base_url=url, token="none")
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.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}")
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="json", help="Output format")
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="json", help="Output format")
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="json", help="Output format")
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="json", help="Output format")
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="json", help="Output format")
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="json", help="Output format")
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="json", help="Output format")
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="json", help="Output format")
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="json", help="Output format")
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="json", help="Output format")
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
- JSON by default (for piping/scripting). --format table for humans.
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 = "json"):
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.1.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
- # JSON (default)
98
+ # Table (default, human-readable)
98
99
  trinity agents list
99
100
 
100
- # Table
101
- trinity agents list --format table
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
@@ -1,3 +1,4 @@
1
1
  click>=8.0
2
2
  httpx>=0.24
3
+ pyyaml>=6.0
3
4
  rich>=13.0
@@ -1,3 +0,0 @@
1
- """Trinity CLI — command-line interface for the Trinity Agent Platform."""
2
-
3
- __version__ = "0.1.0"
File without changes