teardrop-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,192 @@
1
+ """marketplace commands: balance, earnings, withdraw, publish."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from typing import Annotated
7
+
8
+ import typer
9
+
10
+ app = typer.Typer(
11
+ name="marketplace",
12
+ help="Marketplace — balance, earnings, withdrawals, and publishing.",
13
+ no_args_is_help=True,
14
+ )
15
+
16
+
17
+ # ---------------------------------------------------------------------------
18
+ # balance
19
+ # ---------------------------------------------------------------------------
20
+
21
+
22
+ @app.command()
23
+ def balance(
24
+ as_json: Annotated[bool, typer.Option("--json", help="Output as JSON.")] = False,
25
+ base_url: Annotated[str | None, typer.Option("--base-url", hidden=True)] = None,
26
+ ) -> None:
27
+ """Show your marketplace balance."""
28
+ from teardrop_cli import config
29
+ from teardrop_cli.formatting import print_json, print_table, spinner
30
+
31
+ async def _fetch():
32
+ try:
33
+ return await client.get_marketplace_balance()
34
+ finally:
35
+ await client.close()
36
+
37
+ client = config.get_client(base_url)
38
+
39
+ with spinner("Fetching balance…"):
40
+ data = asyncio.run(_fetch())
41
+
42
+ if as_json:
43
+ print_json(data)
44
+ return
45
+
46
+ rows = [[k, v] for k, v in data.items()]
47
+ print_table(
48
+ [("Field", {"style": "bold cyan"}), "Value"],
49
+ rows,
50
+ title="Marketplace Balance",
51
+ )
52
+
53
+
54
+ # ---------------------------------------------------------------------------
55
+ # earnings
56
+ # ---------------------------------------------------------------------------
57
+
58
+
59
+ @app.command()
60
+ def earnings(
61
+ limit: Annotated[int, typer.Option("--limit", "-l", help="Max entries to return.")] = 20,
62
+ cursor: Annotated[str | None, typer.Option("--cursor", help="Pagination cursor.")] = None,
63
+ as_json: Annotated[bool, typer.Option("--json", help="Output as JSON.")] = False,
64
+ base_url: Annotated[str | None, typer.Option("--base-url", hidden=True)] = None,
65
+ ) -> None:
66
+ """List marketplace earnings."""
67
+ from teardrop_cli import config
68
+ from teardrop_cli.formatting import print_json, print_table, spinner
69
+
70
+ async def _fetch():
71
+ try:
72
+ return await client.get_earnings(limit=limit, cursor=cursor)
73
+ finally:
74
+ await client.close()
75
+
76
+ client = config.get_client(base_url)
77
+
78
+ with spinner("Fetching earnings…"):
79
+ data = asyncio.run(_fetch())
80
+
81
+ if as_json:
82
+ print_json(data)
83
+ return
84
+
85
+ items: list[dict] = data.get("items", data) if isinstance(data, dict) else data
86
+ rows = [
87
+ [
88
+ e.get("id", ""),
89
+ e.get("amount", ""),
90
+ e.get("currency", ""),
91
+ e.get("created_at", ""),
92
+ e.get("description", ""),
93
+ ]
94
+ for e in items
95
+ ]
96
+ print_table(
97
+ ["ID", ("Amount", {"justify": "right"}), "Currency", "Date", "Description"],
98
+ rows,
99
+ title="Earnings",
100
+ )
101
+
102
+
103
+ # ---------------------------------------------------------------------------
104
+ # withdraw
105
+ # ---------------------------------------------------------------------------
106
+
107
+
108
+ @app.command()
109
+ def withdraw(
110
+ amount_usdc: Annotated[
111
+ int, typer.Option("--amount-usdc", "-a", help="Amount to withdraw in USDC (integer).")
112
+ ],
113
+ payout_address: Annotated[
114
+ str, typer.Option("--payout-address", help="Wallet address to send funds to.")
115
+ ],
116
+ yes: Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation prompt.")] = False,
117
+ as_json: Annotated[bool, typer.Option("--json", help="Output as JSON.")] = False,
118
+ base_url: Annotated[str | None, typer.Option("--base-url", hidden=True)] = None,
119
+ ) -> None:
120
+ """Withdraw marketplace earnings to a wallet address."""
121
+ from teardrop import WithdrawRequest
122
+
123
+ from teardrop_cli import config
124
+ from teardrop_cli.formatting import confirm, print_json, print_success, print_table, spinner
125
+
126
+ if not yes and not confirm(f"Withdraw {amount_usdc} USDC to {payout_address}?"):
127
+ raise typer.Abort()
128
+
129
+ async def _fetch():
130
+ try:
131
+ return await client.withdraw(
132
+ WithdrawRequest(amount_usdc=amount_usdc, payout_address=payout_address)
133
+ )
134
+ finally:
135
+ await client.close()
136
+
137
+ client = config.get_client(base_url)
138
+
139
+ with spinner("Processing withdrawal…"):
140
+ result = asyncio.run(_fetch())
141
+
142
+ if as_json:
143
+ print_json(result)
144
+ return
145
+
146
+ print_success(f"Withdrawal initiated: {amount_usdc} USDC → {payout_address}")
147
+ if isinstance(result, dict):
148
+ rows = [[k, v] for k, v in result.items()]
149
+ print_table([("Field", {"style": "bold cyan"}), "Value"], rows, title="Withdrawal")
150
+
151
+
152
+ # ---------------------------------------------------------------------------
153
+ # publish
154
+ # ---------------------------------------------------------------------------
155
+
156
+
157
+ @app.command()
158
+ def publish(
159
+ payout_address: Annotated[
160
+ str,
161
+ typer.Option(
162
+ "--payout-address",
163
+ "-p",
164
+ help="Wallet address to receive marketplace earnings.",
165
+ ),
166
+ ],
167
+ as_json: Annotated[bool, typer.Option("--json", help="Output as JSON.")] = False,
168
+ base_url: Annotated[str | None, typer.Option("--base-url", hidden=True)] = None,
169
+ ) -> None:
170
+ """Set your marketplace author config (payout address)."""
171
+ from teardrop_cli import config
172
+ from teardrop_cli.formatting import print_json, print_success, print_table, spinner
173
+
174
+ async def _fetch():
175
+ try:
176
+ return await client.set_author_config(payout_address)
177
+ finally:
178
+ await client.close()
179
+
180
+ client = config.get_client(base_url)
181
+
182
+ with spinner("Updating author config…"):
183
+ result = asyncio.run(_fetch())
184
+
185
+ if as_json:
186
+ print_json(result.model_dump() if hasattr(result, "model_dump") else result)
187
+ return
188
+
189
+ print_success(f"Payout address set to [bold]{payout_address}[/bold].")
190
+ if hasattr(result, "model_dump"):
191
+ rows = [[k, v] for k, v in result.model_dump().items() if v is not None]
192
+ print_table([("Field", {"style": "bold cyan"}), "Value"], rows, title="Author Config")
@@ -0,0 +1,202 @@
1
+ """mcp commands: list, add, discover, remove."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from typing import Annotated
7
+
8
+ import typer
9
+
10
+ app = typer.Typer(
11
+ name="mcp",
12
+ help="MCP servers — list, add, discover tools, and remove.",
13
+ no_args_is_help=True,
14
+ )
15
+
16
+
17
+ # ---------------------------------------------------------------------------
18
+ # list
19
+ # ---------------------------------------------------------------------------
20
+
21
+
22
+ @app.command(name="list")
23
+ def list_servers(
24
+ as_json: Annotated[bool, typer.Option("--json", help="Output as JSON.")] = False,
25
+ base_url: Annotated[str | None, typer.Option("--base-url", hidden=True)] = None,
26
+ ) -> None:
27
+ """List connected MCP servers."""
28
+ from teardrop_cli import config
29
+ from teardrop_cli.formatting import data_console, print_json, print_table, spinner
30
+
31
+ client = config.get_client(base_url)
32
+
33
+ async def _fetch():
34
+ try:
35
+ return await client.list_mcp_servers()
36
+ finally:
37
+ await client.close()
38
+
39
+ with spinner("Fetching MCP servers…"):
40
+ servers = asyncio.run(_fetch())
41
+
42
+ if as_json:
43
+ print_json([s.model_dump() for s in servers])
44
+ return
45
+
46
+ if not servers:
47
+ data_console.print("[dim]No MCP servers configured.[/dim]")
48
+ return
49
+
50
+ rows = [
51
+ [
52
+ s.id,
53
+ s.name,
54
+ getattr(s, "url", ""),
55
+ getattr(s, "auth_type", ""),
56
+ str(len(getattr(s, "tools", []) or [])),
57
+ ]
58
+ for s in servers
59
+ ]
60
+ print_table(
61
+ ["ID", "Name", "URL", "Auth Type", "Tools"],
62
+ rows,
63
+ title="MCP Servers",
64
+ )
65
+
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # add
69
+ # ---------------------------------------------------------------------------
70
+
71
+
72
+ @app.command()
73
+ def add(
74
+ name: Annotated[str, typer.Option("--name", "-n", help="Server name.")],
75
+ url: Annotated[str, typer.Option("--url", "-u", help="Server URL.")],
76
+ auth_type: Annotated[
77
+ str | None,
78
+ typer.Option("--auth-type", help="Auth type (e.g. none, bearer, basic)."),
79
+ ] = None,
80
+ auth_token: Annotated[
81
+ str | None,
82
+ typer.Option("--auth-token", help="Auth token / secret.", hide_input=True),
83
+ ] = None,
84
+ as_json: Annotated[bool, typer.Option("--json", help="Output as JSON.")] = False,
85
+ base_url: Annotated[str | None, typer.Option("--base-url", hidden=True)] = None,
86
+ ) -> None:
87
+ """Add a new MCP server."""
88
+ from teardrop import CreateMcpServerRequest
89
+
90
+ from teardrop_cli import config
91
+ from teardrop_cli.formatting import print_json, print_success, print_table, spinner
92
+
93
+ req_kwargs: dict = {"name": name, "url": url}
94
+ if auth_type:
95
+ req_kwargs["auth_type"] = auth_type
96
+ if auth_token:
97
+ req_kwargs["auth_token"] = auth_token
98
+
99
+ client = config.get_client(base_url)
100
+
101
+ async def _fetch():
102
+ try:
103
+ return await client.create_mcp_server(CreateMcpServerRequest(**req_kwargs))
104
+ finally:
105
+ await client.close()
106
+
107
+ with spinner(f"Adding MCP server [bold]{name}[/bold]…"):
108
+ server = asyncio.run(_fetch())
109
+
110
+ if as_json:
111
+ print_json(server.model_dump())
112
+ return
113
+
114
+ print_success(f"MCP server [bold]{server.name}[/bold] added (id: {server.id}).")
115
+ rows = [[k, v] for k, v in server.model_dump().items() if v is not None]
116
+ print_table([("Field", {"style": "bold cyan"}), "Value"], rows, title="New MCP Server")
117
+
118
+
119
+ # ---------------------------------------------------------------------------
120
+ # discover
121
+ # ---------------------------------------------------------------------------
122
+
123
+
124
+ @app.command()
125
+ def discover(
126
+ server_id: Annotated[str, typer.Argument(help="MCP server ID.")],
127
+ as_json: Annotated[bool, typer.Option("--json", help="Output as JSON.")] = False,
128
+ base_url: Annotated[str | None, typer.Option("--base-url", hidden=True)] = None,
129
+ ) -> None:
130
+ """Discover tools exposed by an MCP server."""
131
+ from teardrop_cli import config
132
+ from teardrop_cli.formatting import data_console, print_json, print_table, spinner
133
+
134
+ client = config.get_client(base_url)
135
+
136
+ async def _fetch():
137
+ try:
138
+ return await client.discover_mcp_server_tools(server_id)
139
+ finally:
140
+ await client.close()
141
+
142
+ with spinner(f"Discovering tools for [bold]{server_id}[/bold]…"):
143
+ result = asyncio.run(_fetch())
144
+
145
+ tools = getattr(result, "tools", []) or []
146
+
147
+ if as_json:
148
+ data = result.model_dump() if hasattr(result, "model_dump") else {"tools": tools}
149
+ print_json(data)
150
+ return
151
+
152
+ if not tools:
153
+ data_console.print("[dim]No tools discovered.[/dim]")
154
+ return
155
+
156
+ rows = [
157
+ [
158
+ getattr(t, "name", ""),
159
+ getattr(t, "description", ""),
160
+ str(len(getattr(t, "parameters", {}).get("properties", {})))
161
+ if isinstance(getattr(t, "parameters", None), dict)
162
+ else "—",
163
+ ]
164
+ for t in tools
165
+ ]
166
+ print_table(
167
+ ["Name", "Description", "Params"],
168
+ rows,
169
+ title=f"Tools on {server_id}",
170
+ )
171
+
172
+
173
+ # ---------------------------------------------------------------------------
174
+ # remove
175
+ # ---------------------------------------------------------------------------
176
+
177
+
178
+ @app.command()
179
+ def remove(
180
+ server_id: Annotated[str, typer.Argument(help="MCP server ID to remove.")],
181
+ yes: Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation prompt.")] = False,
182
+ base_url: Annotated[str | None, typer.Option("--base-url", hidden=True)] = None,
183
+ ) -> None:
184
+ """Remove an MCP server."""
185
+ from teardrop_cli import config
186
+ from teardrop_cli.formatting import confirm, print_success, spinner
187
+
188
+ if not yes and not confirm(f"Remove MCP server [bold]{server_id}[/bold]?"):
189
+ raise typer.Abort()
190
+
191
+ client = config.get_client(base_url)
192
+
193
+ async def _fetch():
194
+ try:
195
+ await client.delete_mcp_server(server_id)
196
+ finally:
197
+ await client.close()
198
+
199
+ with spinner(f"Removing [bold]{server_id}[/bold]…"):
200
+ asyncio.run(_fetch())
201
+
202
+ print_success(f"MCP server [bold]{server_id}[/bold] removed.")
@@ -0,0 +1,193 @@
1
+ """models commands: benchmarks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from typing import Annotated
7
+
8
+ import typer
9
+
10
+ app = typer.Typer(
11
+ name="models",
12
+ help="Model catalogue and benchmarks.",
13
+ no_args_is_help=True,
14
+ )
15
+
16
+
17
+ @app.callback()
18
+ def _callback() -> None:
19
+ """Model catalogue and benchmarks."""
20
+
21
+
22
+ # ---------------------------------------------------------------------------
23
+ # benchmarks
24
+ # ---------------------------------------------------------------------------
25
+
26
+
27
+ @app.command()
28
+ def benchmarks(
29
+ org: Annotated[
30
+ str | None,
31
+ typer.Option("--org", help="Org ID for org-scoped metrics (requires auth)."),
32
+ ] = None,
33
+ no_cache: Annotated[
34
+ bool,
35
+ typer.Option("--no-cache", "--force-refresh", help="Bypass the local 10-minute cache."),
36
+ ] = False,
37
+ as_json: Annotated[bool, typer.Option("--json", help="Output as JSON.")] = False,
38
+ base_url: Annotated[str | None, typer.Option("--base-url", hidden=True)] = None,
39
+ ) -> None:
40
+ """Display model catalogue with operational metrics.
41
+
42
+ Without --org: public benchmarks (no auth required).
43
+ With --org ORG_ID: org-scoped metrics (auth required).
44
+ """
45
+ from teardrop import AuthenticationError
46
+
47
+ from teardrop_cli import config
48
+ from teardrop_cli.formatting import (
49
+ data_console,
50
+ print_error,
51
+ print_json,
52
+ spinner,
53
+ )
54
+
55
+ client = config.get_client(base_url)
56
+
57
+ try:
58
+
59
+ async def _fetch():
60
+ try:
61
+ if org:
62
+ return await client.get_org_model_benchmarks(org_id=org)
63
+ else:
64
+ return await client.get_model_benchmarks(no_cache=no_cache)
65
+ finally:
66
+ await client.close()
67
+
68
+ label = "Fetching org model benchmarks…" if org else "Fetching model benchmarks…"
69
+ with spinner(label):
70
+ response = asyncio.run(_fetch())
71
+ except AuthenticationError:
72
+ print_error(
73
+ "Not authenticated.",
74
+ hint="Run: `teardrop auth login`",
75
+ )
76
+ raise typer.Exit(1) from None
77
+ except Exception as exc:
78
+ status = getattr(exc, "status_code", None) or getattr(exc, "status", None)
79
+ if status == 401:
80
+ print_error("Not authenticated.", hint="Run: `teardrop auth login`")
81
+ raise typer.Exit(1) from None
82
+ print_error(f"Unexpected error: {exc}")
83
+ raise typer.Exit(1) from None
84
+
85
+ data = response if isinstance(response, dict) else response.model_dump()
86
+ models_list: list[dict] = data.get("models", [])
87
+
88
+ if as_json:
89
+ print_json(data)
90
+ return
91
+
92
+ if not models_list:
93
+ if org:
94
+ data_console.print(
95
+ f"[dim]No benchmark data for {org}. Run some agent tasks first.[/dim]"
96
+ )
97
+ else:
98
+ data_console.print("[dim]No benchmark data available.[/dim]")
99
+ return
100
+
101
+ if org:
102
+ _print_org_benchmarks(models_list, org_id=org)
103
+ else:
104
+ _print_public_benchmarks(models_list)
105
+
106
+
107
+ # ---------------------------------------------------------------------------
108
+ # Internal renderers
109
+ # ---------------------------------------------------------------------------
110
+
111
+
112
+ def _print_public_benchmarks(models: list[dict]) -> None:
113
+ """Render the global model catalogue table."""
114
+ from teardrop_cli.formatting import print_table
115
+
116
+ rows = []
117
+ for m in models:
118
+ benchmarks = m.get("benchmarks") or {}
119
+ pricing = m.get("pricing") or {}
120
+ p95 = benchmarks.get("p95_latency_ms")
121
+ cost_in = pricing.get("tokens_in_cost_per_1k")
122
+ cost_out = pricing.get("tokens_out_cost_per_1k")
123
+ runs = benchmarks.get("total_runs_7d")
124
+
125
+ rows.append(
126
+ [
127
+ m.get("provider", "—"),
128
+ m.get("model", "—"),
129
+ m.get("display_name", "—"),
130
+ str(m.get("quality_tier", "—")),
131
+ f"{int(p95)} ms" if p95 is not None else "—",
132
+ f"${cost_in:.4f}/1k" if cost_in is not None else "—",
133
+ f"${cost_out:.4f}/1k" if cost_out is not None else "—",
134
+ str(runs) if runs is not None else "—",
135
+ ]
136
+ )
137
+
138
+ print_table(
139
+ [
140
+ "Provider",
141
+ "Model",
142
+ "Display Name",
143
+ ("Quality", {"justify": "right"}),
144
+ ("Latency P95", {"justify": "right"}),
145
+ ("Cost (in)", {"justify": "right"}),
146
+ ("Cost (out)", {"justify": "right"}),
147
+ ("Runs 7d", {"justify": "right"}),
148
+ ],
149
+ rows,
150
+ title="Model Catalogue",
151
+ )
152
+
153
+
154
+ def _print_org_benchmarks(models: list[dict], *, org_id: str) -> None:
155
+ """Render the org-scoped benchmark table."""
156
+ from teardrop_cli.formatting import data_console, print_table
157
+
158
+ data_console.print(f"[bold]Your org's model usage (7-day aggregate) — {org_id}[/bold]")
159
+
160
+ rows = []
161
+ for m in models:
162
+ b = m.get("benchmarks") or {}
163
+ runs = b.get("total_runs_7d")
164
+ avg_cost = b.get("avg_cost_usdc_per_run")
165
+ total_cost = (runs * avg_cost) if (runs is not None and avg_cost is not None) else None
166
+ rows.append(
167
+ [
168
+ m.get("provider", "—"),
169
+ m.get("model", "—"),
170
+ str(runs) if runs is not None else "—",
171
+ f"{b.get('avg_latency_ms', '—')} ms"
172
+ if b.get("avg_latency_ms") is not None
173
+ else "—",
174
+ f"${avg_cost:.2f}" if avg_cost is not None else "—",
175
+ f"${total_cost:.2f}" if total_cost is not None else "—",
176
+ f"{b.get('avg_tokens_per_sec', '—'):.1f}"
177
+ if b.get("avg_tokens_per_sec") is not None
178
+ else "—",
179
+ ]
180
+ )
181
+
182
+ print_table(
183
+ [
184
+ "Provider",
185
+ "Model",
186
+ ("Runs", {"justify": "right"}),
187
+ ("Avg Latency", {"justify": "right"}),
188
+ ("Avg Cost/Run", {"justify": "right"}),
189
+ ("Total Cost", {"justify": "right"}),
190
+ ("Tokens/sec", {"justify": "right"}),
191
+ ],
192
+ rows,
193
+ )