teardrop-cli 0.3.2__tar.gz → 0.3.4__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.
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/PKG-INFO +4 -2
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/README.md +2 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/docs/cli-reference.md +12 -1
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/pyproject.toml +2 -2
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/src/teardrop_cli/cli.py +19 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/src/teardrop_cli/commands/auth.py +3 -1
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/src/teardrop_cli/commands/event_triggers.py +13 -1
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/src/teardrop_cli/commands/marketplace.py +74 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/src/teardrop_cli/commands/schedules.py +54 -3
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/tests/conftest.py +31 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/tests/smoke/conftest.py +1 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/tests/test_auth.py +3 -1
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/tests/test_marketplace.py +49 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/tests/test_schedules.py +111 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/.github/workflows/publish.yml +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/.github/workflows/test.yml +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/.gitignore +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/.vscode/settings.json +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/LICENSE +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/docs/cli-json-schema.md +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/src/teardrop_cli/_fixtures.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/src/teardrop_cli/_lazy.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/src/teardrop_cli/_templates.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/src/teardrop_cli/commands/__init__.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/src/teardrop_cli/commands/agent_tools.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/src/teardrop_cli/commands/balance.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/src/teardrop_cli/commands/chat.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/src/teardrop_cli/commands/config_cmd.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/src/teardrop_cli/commands/earnings.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/src/teardrop_cli/commands/llm_config.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/src/teardrop_cli/commands/mcp.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/src/teardrop_cli/commands/models.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/src/teardrop_cli/commands/quickstart.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/src/teardrop_cli/commands/run.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/src/teardrop_cli/commands/tools.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/src/teardrop_cli/commands/usage.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/src/teardrop_cli/config.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/src/teardrop_cli/formatting.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/src/teardrop_cli/pricing.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/tests/e2e/README.md +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/tests/e2e/__init__.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/tests/e2e/conftest.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/tests/e2e/test_auth_e2e.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/tests/e2e/test_billing_e2e.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/tests/e2e/test_marketplace_e2e.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/tests/e2e/test_run_e2e.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/tests/e2e/test_signup_e2e.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/tests/e2e/test_siwe_login_e2e.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/tests/smoke/__init__.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/tests/smoke/test_config_persistence.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/tests/smoke/test_keyring_integration.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/tests/smoke/test_perf_lazygroup.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/tests/smoke/test_siwe_signing.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/tests/test_billing.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/tests/test_chat.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/tests/test_config_cmd.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/tests/test_credential_priority.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/tests/test_earnings.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/tests/test_event_triggers.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/tests/test_init_and_byok.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/tests/test_llm_config.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/tests/test_mcp.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/tests/test_models.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/tests/test_quickstart.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/tests/test_run.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/tests/test_signup.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/tests/test_token_expiry_fallback.py +0 -0
- {teardrop_cli-0.3.2 → teardrop_cli-0.3.4}/tests/test_tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: teardrop-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Summary: Command-line interface for the Teardrop AI agent platform
|
|
5
5
|
Project-URL: Homepage, https://github.com/teardrop-ai/teardrop-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/teardrop-ai/teardrop-cli
|
|
@@ -45,7 +45,7 @@ Requires-Dist: httpx>=0.27
|
|
|
45
45
|
Requires-Dist: keyring>=25.0
|
|
46
46
|
Requires-Dist: platformdirs>=4.0
|
|
47
47
|
Requires-Dist: rich>=13.0
|
|
48
|
-
Requires-Dist: teardrop-sdk>=0.3.
|
|
48
|
+
Requires-Dist: teardrop-sdk>=0.3.2
|
|
49
49
|
Requires-Dist: tomli-w>=1.0
|
|
50
50
|
Requires-Dist: typer>=0.12
|
|
51
51
|
Provides-Extra: dev
|
|
@@ -315,6 +315,8 @@ New email-authenticated organizations may receive optional promotional credit af
|
|
|
315
315
|
teardrop marketplace list
|
|
316
316
|
teardrop marketplace search "weather"
|
|
317
317
|
teardrop marketplace info acme/weather
|
|
318
|
+
teardrop marketplace reputation
|
|
319
|
+
teardrop marketplace reputation acme/weather --json
|
|
318
320
|
teardrop marketplace subscribe acme/weather
|
|
319
321
|
teardrop marketplace subscriptions
|
|
320
322
|
```
|
|
@@ -255,6 +255,8 @@ New email-authenticated organizations may receive optional promotional credit af
|
|
|
255
255
|
teardrop marketplace list
|
|
256
256
|
teardrop marketplace search "weather"
|
|
257
257
|
teardrop marketplace info acme/weather
|
|
258
|
+
teardrop marketplace reputation
|
|
259
|
+
teardrop marketplace reputation acme/weather --json
|
|
258
260
|
teardrop marketplace subscribe acme/weather
|
|
259
261
|
teardrop marketplace subscriptions
|
|
260
262
|
```
|
|
@@ -133,6 +133,13 @@ teardrop schedules create \
|
|
|
133
133
|
--interval-seconds 3600 \
|
|
134
134
|
--json
|
|
135
135
|
|
|
136
|
+
# Read the prompt from a UTF-8 file (or '-' for stdin) instead of --prompt:
|
|
137
|
+
teardrop schedules create \
|
|
138
|
+
--name hourly-briefing \
|
|
139
|
+
--prompt-file ./task.md \
|
|
140
|
+
--interval-seconds 3600 \
|
|
141
|
+
--json
|
|
142
|
+
|
|
136
143
|
teardrop schedules list --json
|
|
137
144
|
teardrop schedules get <schedule-id> --json
|
|
138
145
|
teardrop schedules update <schedule-id> --enabled false --json
|
|
@@ -142,6 +149,8 @@ teardrop schedules delete <schedule-id>
|
|
|
142
149
|
teardrop schedules delete <schedule-id> --yes
|
|
143
150
|
```
|
|
144
151
|
|
|
152
|
+
Exactly one of `--prompt` or `--prompt-file` is required. `--prompt-file` reads UTF-8 (a leading BOM is stripped) and preserves newlines and emoji; pass `-` to read the prompt from stdin.
|
|
153
|
+
|
|
145
154
|
Non-JSON list output includes: `ID`, `Name`, `Enabled`, `Interval`, `Consecutive Failures`, `Last Run At`.
|
|
146
155
|
|
|
147
156
|
Run history output includes: `Run ID`, `Status`, `Cost (USDC)`, `Error Message`, `Executed At`.
|
|
@@ -192,7 +201,7 @@ teardrop usage --json
|
|
|
192
201
|
|
|
193
202
|
The CLI does not support direct top-ups. To add credits or manage your billing methods, please visit the dashboard:
|
|
194
203
|
|
|
195
|
-
[https://teardrop.dev
|
|
204
|
+
[https://teardrop.dev](https://teardrop.dev)
|
|
196
205
|
|
|
197
206
|
If you run out of credits during an agent run, the agent will pause and provide this link.
|
|
198
207
|
|
|
@@ -315,6 +324,8 @@ teardrop marketplace list
|
|
|
315
324
|
teardrop marketplace list --category data --json
|
|
316
325
|
teardrop marketplace search "weather"
|
|
317
326
|
teardrop marketplace info acme/weather
|
|
327
|
+
teardrop marketplace reputation
|
|
328
|
+
teardrop marketplace reputation acme/weather --json
|
|
318
329
|
teardrop marketplace subscribe acme/weather
|
|
319
330
|
teardrop marketplace subscribe acme/weather --yes # skip confirmation
|
|
320
331
|
teardrop marketplace subscriptions
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "teardrop-cli"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.4"
|
|
8
8
|
description = "Command-line interface for the Teardrop AI agent platform"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -24,7 +24,7 @@ classifiers = [
|
|
|
24
24
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
25
25
|
]
|
|
26
26
|
dependencies = [
|
|
27
|
-
"teardrop-sdk>=0.3.
|
|
27
|
+
"teardrop-sdk>=0.3.2",
|
|
28
28
|
"typer>=0.12",
|
|
29
29
|
"rich>=13.0",
|
|
30
30
|
"platformdirs>=4.0",
|
|
@@ -8,11 +8,30 @@ modules are imported only when the subcommand is actually invoked, keeping
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
import sys
|
|
12
|
+
from contextlib import suppress
|
|
13
|
+
|
|
11
14
|
import click
|
|
12
15
|
|
|
13
16
|
from teardrop_cli._lazy import LazyGroup
|
|
14
17
|
|
|
15
18
|
|
|
19
|
+
def _force_utf8_stdio() -> None:
|
|
20
|
+
"""Reconfigure stdout/stderr to UTF-8.
|
|
21
|
+
|
|
22
|
+
On legacy Windows consoles the default codec (e.g. cp1252) cannot encode
|
|
23
|
+
non-ASCII characters such as emoji in prompts or JSON output, raising
|
|
24
|
+
``UnicodeEncodeError``. Forcing UTF-8 keeps ``--json`` and rich output
|
|
25
|
+
working regardless of the console code page.
|
|
26
|
+
"""
|
|
27
|
+
for stream in (sys.stdout, sys.stderr):
|
|
28
|
+
with suppress(AttributeError, ValueError):
|
|
29
|
+
stream.reconfigure(encoding="utf-8") # type: ignore[union-attr]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
_force_utf8_stdio()
|
|
33
|
+
|
|
34
|
+
|
|
16
35
|
@click.group(
|
|
17
36
|
name="teardrop",
|
|
18
37
|
cls=LazyGroup,
|
|
@@ -462,7 +462,9 @@ async def _siwe_auth_async(
|
|
|
462
462
|
|
|
463
463
|
async with AsyncTeardropClient(url) as client:
|
|
464
464
|
nonce_resp = await client.get_siwe_nonce()
|
|
465
|
-
|
|
465
|
+
# get_siwe_nonce() returns a SiweNonceResponse pydantic model (nonce: str),
|
|
466
|
+
# not a dict — access the attribute directly.
|
|
467
|
+
nonce = nonce_resp.nonce
|
|
466
468
|
|
|
467
469
|
from urllib.parse import urlparse
|
|
468
470
|
|
|
@@ -23,6 +23,15 @@ def _to_dict(value: Any) -> dict[str, Any]:
|
|
|
23
23
|
return dict(value)
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
def _list_items(value: Any) -> Any:
|
|
27
|
+
if isinstance(value, dict):
|
|
28
|
+
return value.get("items") or []
|
|
29
|
+
items = getattr(value, "items", None)
|
|
30
|
+
if items is not None and not callable(items):
|
|
31
|
+
return items
|
|
32
|
+
return value
|
|
33
|
+
|
|
34
|
+
|
|
26
35
|
def _to_page_dict(value: Any) -> dict[str, Any]:
|
|
27
36
|
data = _to_dict(value)
|
|
28
37
|
data["items"] = [_to_dict(item) for item in data.get("items") or []]
|
|
@@ -223,7 +232,10 @@ def list_cmd(
|
|
|
223
232
|
operation=lambda client: client.event_triggers.list(),
|
|
224
233
|
)
|
|
225
234
|
|
|
226
|
-
|
|
235
|
+
# The SDK returns an EventTriggerListResponse model with an ``items``
|
|
236
|
+
# field (plus ``next_cursor``). Iterating the model directly would yield
|
|
237
|
+
# its field tuples, so unwrap ``.items`` when present.
|
|
238
|
+
items = [_to_dict(item) for item in _list_items(result)]
|
|
227
239
|
if as_json:
|
|
228
240
|
print_json(items)
|
|
229
241
|
return
|
|
@@ -240,6 +240,80 @@ def info(
|
|
|
240
240
|
data_console.print_json(json.dumps(schema, default=str))
|
|
241
241
|
|
|
242
242
|
|
|
243
|
+
# ---------------------------------------------------------------------------
|
|
244
|
+
# reputation
|
|
245
|
+
# ---------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@app.command()
|
|
249
|
+
def reputation(
|
|
250
|
+
qualified_name: Annotated[
|
|
251
|
+
str | None,
|
|
252
|
+
typer.Argument(metavar="ORG/TOOL", help="Optional qualified tool name filter."),
|
|
253
|
+
] = None,
|
|
254
|
+
as_json: Annotated[bool, typer.Option("--json", help="Output as JSON.")] = False,
|
|
255
|
+
base_url: Annotated[str | None, typer.Option("--base-url", hidden=True)] = None,
|
|
256
|
+
) -> None:
|
|
257
|
+
"""Show public reputation metrics for marketplace tools (no auth required)."""
|
|
258
|
+
from teardrop_cli import config
|
|
259
|
+
from teardrop_cli.formatting import data_console, print_error, print_json, print_table, spinner
|
|
260
|
+
|
|
261
|
+
client = config.get_client(base_url, require_auth=False)
|
|
262
|
+
|
|
263
|
+
async def _fetch() -> dict:
|
|
264
|
+
try:
|
|
265
|
+
response = await client.get_public_reputation()
|
|
266
|
+
return response.model_dump() if hasattr(response, "model_dump") else dict(response)
|
|
267
|
+
finally:
|
|
268
|
+
await client.close()
|
|
269
|
+
|
|
270
|
+
with spinner("Fetching public reputation…"):
|
|
271
|
+
data = asyncio.run(_fetch())
|
|
272
|
+
|
|
273
|
+
tools = data.get("tools", [])
|
|
274
|
+
if qualified_name is not None:
|
|
275
|
+
tools = [tool for tool in tools if tool.get("qualified_tool_name") == qualified_name]
|
|
276
|
+
if not tools:
|
|
277
|
+
print_error(f"No public reputation found for {qualified_name!r}.")
|
|
278
|
+
raise typer.Exit(1)
|
|
279
|
+
data["tools"] = tools
|
|
280
|
+
|
|
281
|
+
if as_json:
|
|
282
|
+
print_json(data)
|
|
283
|
+
return
|
|
284
|
+
|
|
285
|
+
if not tools:
|
|
286
|
+
data_console.print("[dim]No public reputation data available.[/dim]")
|
|
287
|
+
return
|
|
288
|
+
|
|
289
|
+
data_console.print(
|
|
290
|
+
f"Schema: {data.get('schema_version', '—')} | "
|
|
291
|
+
f"Generated: {data.get('generated_at') or '—'} | "
|
|
292
|
+
f"Methodology: {data.get('methodology_url') or '—'}"
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
rows = [
|
|
296
|
+
[
|
|
297
|
+
tool.get("qualified_tool_name", "—"),
|
|
298
|
+
(
|
|
299
|
+
f"score={tool.get('reputation_score', '—')} "
|
|
300
|
+
f"success={tool.get('success_rate', '—')} "
|
|
301
|
+
f"sample={tool.get('sample_size', '—')} "
|
|
302
|
+
f"confidence={tool.get('confidence', '—')} "
|
|
303
|
+
f"freshness={tool.get('freshness', '—')} "
|
|
304
|
+
f"latency_ms={tool.get('average_latency_ms', '—')} "
|
|
305
|
+
f"callers={tool.get('unique_caller_count', '—')}"
|
|
306
|
+
),
|
|
307
|
+
]
|
|
308
|
+
for tool in tools
|
|
309
|
+
]
|
|
310
|
+
print_table(
|
|
311
|
+
["Tool", "Metrics"],
|
|
312
|
+
rows,
|
|
313
|
+
title="Public Tool Reputation",
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
|
|
243
317
|
# ---------------------------------------------------------------------------
|
|
244
318
|
# subscribe
|
|
245
319
|
# ---------------------------------------------------------------------------
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import sys
|
|
6
7
|
from collections.abc import Awaitable, Callable
|
|
8
|
+
from pathlib import Path
|
|
7
9
|
from typing import Annotated, Any
|
|
8
10
|
|
|
9
11
|
import typer
|
|
@@ -23,6 +25,32 @@ def _to_dict(value: Any) -> dict[str, Any]:
|
|
|
23
25
|
return dict(value)
|
|
24
26
|
|
|
25
27
|
|
|
28
|
+
def _list_items(value: Any) -> Any:
|
|
29
|
+
if isinstance(value, dict):
|
|
30
|
+
return value.get("items") or []
|
|
31
|
+
items = getattr(value, "items", None)
|
|
32
|
+
if items is not None and not callable(items):
|
|
33
|
+
return items
|
|
34
|
+
return value
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _read_prompt_file(path: str) -> str:
|
|
38
|
+
"""Read prompt text from *path* (UTF-8), or stdin when *path* is ``-``."""
|
|
39
|
+
from teardrop_cli.formatting import print_error
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
if path == "-":
|
|
43
|
+
return sys.stdin.read()
|
|
44
|
+
text = Path(path).read_text(encoding="utf-8-sig")
|
|
45
|
+
return text
|
|
46
|
+
except UnicodeError as exc:
|
|
47
|
+
print_error(f"Could not decode --prompt-file {path!r} as UTF-8: {exc}")
|
|
48
|
+
raise typer.Exit(1) from None
|
|
49
|
+
except OSError as exc:
|
|
50
|
+
print_error(f"Could not read --prompt-file {path!r}: {exc}")
|
|
51
|
+
raise typer.Exit(1) from None
|
|
52
|
+
|
|
53
|
+
|
|
26
54
|
def _to_page_dict(value: Any) -> dict[str, Any]:
|
|
27
55
|
data = _to_dict(value)
|
|
28
56
|
data["items"] = [_to_dict(item) for item in data.get("items") or []]
|
|
@@ -132,11 +160,22 @@ def _validate_callback_options(
|
|
|
132
160
|
@app.command()
|
|
133
161
|
def create(
|
|
134
162
|
name: Annotated[str, typer.Option("--name", help="Schedule name.")],
|
|
135
|
-
prompt: Annotated[str, typer.Option("--prompt", help="Prompt to run.")],
|
|
136
163
|
interval_seconds: Annotated[
|
|
137
164
|
int,
|
|
138
165
|
typer.Option("--interval-seconds", min=1, help="Run interval in seconds."),
|
|
139
166
|
],
|
|
167
|
+
prompt: Annotated[
|
|
168
|
+
str | None,
|
|
169
|
+
typer.Option("--prompt", help="Prompt to run. Mutually exclusive with --prompt-file."),
|
|
170
|
+
] = None,
|
|
171
|
+
prompt_file: Annotated[
|
|
172
|
+
str | None,
|
|
173
|
+
typer.Option(
|
|
174
|
+
"--prompt-file",
|
|
175
|
+
help="Read the prompt from a UTF-8 file. Use '-' to read from stdin. "
|
|
176
|
+
"Mutually exclusive with --prompt.",
|
|
177
|
+
),
|
|
178
|
+
] = None,
|
|
140
179
|
callback_url: Annotated[
|
|
141
180
|
str | None,
|
|
142
181
|
typer.Option("--callback-url", help="Optional callback URL invoked after each run."),
|
|
@@ -147,7 +186,16 @@ def create(
|
|
|
147
186
|
"""Create a new interval schedule."""
|
|
148
187
|
from teardrop import CreateScheduleRequest
|
|
149
188
|
|
|
150
|
-
from teardrop_cli.formatting import print_json, print_success
|
|
189
|
+
from teardrop_cli.formatting import print_error, print_json, print_success
|
|
190
|
+
|
|
191
|
+
if prompt is not None and prompt_file is not None:
|
|
192
|
+
print_error("Use either --prompt or --prompt-file, not both.")
|
|
193
|
+
raise typer.Exit(1) from None
|
|
194
|
+
if prompt is None and prompt_file is None:
|
|
195
|
+
print_error("Provide either --prompt or --prompt-file.")
|
|
196
|
+
raise typer.Exit(1) from None
|
|
197
|
+
if prompt_file is not None:
|
|
198
|
+
prompt = _read_prompt_file(prompt_file)
|
|
151
199
|
|
|
152
200
|
request = CreateScheduleRequest(
|
|
153
201
|
name=name,
|
|
@@ -185,7 +233,10 @@ def list_cmd(
|
|
|
185
233
|
operation=lambda client: client.schedules.list(),
|
|
186
234
|
)
|
|
187
235
|
|
|
188
|
-
|
|
236
|
+
# The SDK returns a ScheduledRunListResponse model with an ``items``
|
|
237
|
+
# field (plus ``next_cursor``). Iterating the model directly would yield
|
|
238
|
+
# its field tuples, so unwrap ``.items`` when present.
|
|
239
|
+
items = [_to_dict(item) for item in _list_items(result)]
|
|
189
240
|
if as_json:
|
|
190
241
|
print_json(items)
|
|
191
242
|
return
|
|
@@ -159,6 +159,37 @@ def mock_client():
|
|
|
159
159
|
"next_cursor": None,
|
|
160
160
|
}
|
|
161
161
|
)
|
|
162
|
+
client.get_public_reputation = AsyncMock(
|
|
163
|
+
return_value=MagicMock(
|
|
164
|
+
model_dump=lambda: {
|
|
165
|
+
"schema_version": "1.0",
|
|
166
|
+
"generated_at": "2026-08-02T12:00:00Z",
|
|
167
|
+
"methodology_url": "https://teardrop.dev/reputation-methodology",
|
|
168
|
+
"tools": [
|
|
169
|
+
{
|
|
170
|
+
"qualified_tool_name": "acme/weather",
|
|
171
|
+
"reputation_score": 0.91,
|
|
172
|
+
"success_rate": 0.96,
|
|
173
|
+
"sample_size": 125.0,
|
|
174
|
+
"confidence": 0.88,
|
|
175
|
+
"freshness": 0.97,
|
|
176
|
+
"average_latency_ms": 142.5,
|
|
177
|
+
"unique_caller_count": 18,
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"qualified_tool_name": "acme/search",
|
|
181
|
+
"reputation_score": 0.84,
|
|
182
|
+
"success_rate": 0.9,
|
|
183
|
+
"sample_size": 80.0,
|
|
184
|
+
"confidence": 0.75,
|
|
185
|
+
"freshness": 0.92,
|
|
186
|
+
"average_latency_ms": 210.0,
|
|
187
|
+
"unique_caller_count": None,
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
}
|
|
191
|
+
)
|
|
192
|
+
)
|
|
162
193
|
client.subscribe = AsyncMock(
|
|
163
194
|
return_value=MagicMock(
|
|
164
195
|
model_dump=lambda: {"id": "sub_1", "qualified_tool_name": "acme/weather"}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from types import SimpleNamespace
|
|
5
6
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
6
7
|
|
|
7
8
|
import pytest
|
|
@@ -140,7 +141,8 @@ class TestSiweLogin:
|
|
|
140
141
|
return None
|
|
141
142
|
|
|
142
143
|
async def get_siwe_nonce(self):
|
|
143
|
-
|
|
144
|
+
# Mirrors the real SDK: returns a SiweNonceResponse pydantic model.
|
|
145
|
+
return SimpleNamespace(nonce="abc123")
|
|
144
146
|
|
|
145
147
|
async def authenticate_siwe(self, message, signature):
|
|
146
148
|
seen["message"] = message
|
|
@@ -91,6 +91,55 @@ class TestInfo:
|
|
|
91
91
|
assert result.exit_code == 1
|
|
92
92
|
|
|
93
93
|
|
|
94
|
+
class TestReputation:
|
|
95
|
+
def test_reputation_table(self, runner: CliRunner, patch_get_client):
|
|
96
|
+
result = runner.invoke(app, ["marketplace", "reputation"])
|
|
97
|
+
assert result.exit_code == 0, result.output
|
|
98
|
+
assert "Public Tool Reputation" in result.output
|
|
99
|
+
assert "Schema: 1.0" in result.output
|
|
100
|
+
assert "acme/weather" in result.output
|
|
101
|
+
assert "0.91" in result.output
|
|
102
|
+
assert "142.5" in result.output
|
|
103
|
+
assert "acme/search" in result.output
|
|
104
|
+
|
|
105
|
+
def test_reputation_json(self, runner: CliRunner, patch_get_client):
|
|
106
|
+
import json
|
|
107
|
+
|
|
108
|
+
result = runner.invoke(app, ["marketplace", "reputation", "--json"])
|
|
109
|
+
assert result.exit_code == 0, result.output
|
|
110
|
+
data = json.loads(result.output)
|
|
111
|
+
assert data["schema_version"] == "1.0"
|
|
112
|
+
assert data["methodology_url"].endswith("reputation-methodology")
|
|
113
|
+
assert len(data["tools"]) == 2
|
|
114
|
+
assert data["tools"][0]["unique_caller_count"] == 18
|
|
115
|
+
assert data["tools"][1]["unique_caller_count"] is None
|
|
116
|
+
|
|
117
|
+
def test_reputation_filters_by_qualified_name(self, runner: CliRunner, patch_get_client):
|
|
118
|
+
import json
|
|
119
|
+
|
|
120
|
+
result = runner.invoke(app, ["marketplace", "reputation", "acme/weather", "--json"])
|
|
121
|
+
assert result.exit_code == 0, result.output
|
|
122
|
+
data = json.loads(result.output)
|
|
123
|
+
assert [tool["qualified_tool_name"] for tool in data["tools"]] == ["acme/weather"]
|
|
124
|
+
|
|
125
|
+
def test_reputation_missing_tool(self, runner: CliRunner, patch_get_client):
|
|
126
|
+
result = runner.invoke(app, ["marketplace", "reputation", "nope/none"])
|
|
127
|
+
assert result.exit_code == 1
|
|
128
|
+
assert "No public reputation found" in result.output
|
|
129
|
+
|
|
130
|
+
def test_reputation_does_not_require_auth(self, runner: CliRunner, mock_client, monkeypatch):
|
|
131
|
+
calls = {}
|
|
132
|
+
|
|
133
|
+
def get_client(*args, **kwargs):
|
|
134
|
+
calls.update(kwargs)
|
|
135
|
+
return mock_client
|
|
136
|
+
|
|
137
|
+
monkeypatch.setattr("teardrop_cli.config.get_client", get_client)
|
|
138
|
+
result = runner.invoke(app, ["marketplace", "reputation"])
|
|
139
|
+
assert result.exit_code == 0, result.output
|
|
140
|
+
assert calls["require_auth"] is False
|
|
141
|
+
|
|
142
|
+
|
|
94
143
|
class TestSubscribe:
|
|
95
144
|
def test_subscribe_with_yes(self, runner: CliRunner, patch_get_client, mock_client):
|
|
96
145
|
result = runner.invoke(app, ["marketplace", "subscribe", "acme/weather", "--yes"])
|
|
@@ -70,6 +70,84 @@ class TestCreate:
|
|
|
70
70
|
assert '"name": "hourly-sync"' in result.output
|
|
71
71
|
assert '"schedule_kind": "interval"' in result.output
|
|
72
72
|
|
|
73
|
+
def test_create_prompt_file(self, runner, patch_get_client, mock_client, tmp_path):
|
|
74
|
+
prompt_file = tmp_path / "task.md"
|
|
75
|
+
prompt_file.write_text("Line one\nLine two 📊\n", encoding="utf-8")
|
|
76
|
+
mock_client.schedules.create.return_value = _schedule(name="from-file")
|
|
77
|
+
result = runner.invoke(
|
|
78
|
+
app,
|
|
79
|
+
[
|
|
80
|
+
"schedules",
|
|
81
|
+
"create",
|
|
82
|
+
"--name",
|
|
83
|
+
"from-file",
|
|
84
|
+
"--prompt-file",
|
|
85
|
+
str(prompt_file),
|
|
86
|
+
"--interval-seconds",
|
|
87
|
+
"60",
|
|
88
|
+
],
|
|
89
|
+
)
|
|
90
|
+
assert result.exit_code == 0, result.output
|
|
91
|
+
request = mock_client.schedules.create.await_args.args[0]
|
|
92
|
+
assert request.prompt == "Line one\nLine two 📊\n"
|
|
93
|
+
|
|
94
|
+
def test_create_prompt_file_mutually_exclusive(self, runner, patch_get_client, mock_client):
|
|
95
|
+
result = runner.invoke(
|
|
96
|
+
app,
|
|
97
|
+
[
|
|
98
|
+
"schedules",
|
|
99
|
+
"create",
|
|
100
|
+
"--name",
|
|
101
|
+
"x",
|
|
102
|
+
"--prompt",
|
|
103
|
+
"inline",
|
|
104
|
+
"--prompt-file",
|
|
105
|
+
"somefile.md",
|
|
106
|
+
"--interval-seconds",
|
|
107
|
+
"60",
|
|
108
|
+
],
|
|
109
|
+
)
|
|
110
|
+
assert result.exit_code == 1
|
|
111
|
+
assert "not both" in result.output
|
|
112
|
+
|
|
113
|
+
def test_create_requires_prompt_source(self, runner, patch_get_client, mock_client):
|
|
114
|
+
result = runner.invoke(
|
|
115
|
+
app,
|
|
116
|
+
[
|
|
117
|
+
"schedules",
|
|
118
|
+
"create",
|
|
119
|
+
"--name",
|
|
120
|
+
"x",
|
|
121
|
+
"--interval-seconds",
|
|
122
|
+
"60",
|
|
123
|
+
],
|
|
124
|
+
)
|
|
125
|
+
assert result.exit_code == 1
|
|
126
|
+
assert "Provide either --prompt or --prompt-file" in result.output
|
|
127
|
+
mock_client.schedules.create.assert_not_awaited()
|
|
128
|
+
|
|
129
|
+
def test_create_prompt_file_rejects_invalid_utf8(
|
|
130
|
+
self, runner, patch_get_client, mock_client, tmp_path
|
|
131
|
+
):
|
|
132
|
+
prompt_file = tmp_path / "invalid.bin"
|
|
133
|
+
prompt_file.write_bytes(b"\xff\xfe\xfd")
|
|
134
|
+
result = runner.invoke(
|
|
135
|
+
app,
|
|
136
|
+
[
|
|
137
|
+
"schedules",
|
|
138
|
+
"create",
|
|
139
|
+
"--name",
|
|
140
|
+
"x",
|
|
141
|
+
"--prompt-file",
|
|
142
|
+
str(prompt_file),
|
|
143
|
+
"--interval-seconds",
|
|
144
|
+
"60",
|
|
145
|
+
],
|
|
146
|
+
)
|
|
147
|
+
assert result.exit_code == 1
|
|
148
|
+
assert "as UTF-8" in result.output
|
|
149
|
+
mock_client.schedules.create.assert_not_awaited()
|
|
150
|
+
|
|
73
151
|
|
|
74
152
|
class TestList:
|
|
75
153
|
def test_list(self, runner: CliRunner, patch_get_client, mock_client):
|
|
@@ -91,6 +169,39 @@ class TestList:
|
|
|
91
169
|
assert result.exit_code == 0, result.output
|
|
92
170
|
assert '"name": "nightly-report"' in result.output
|
|
93
171
|
|
|
172
|
+
def test_list_handles_sdk_list_response_model(
|
|
173
|
+
self, runner: CliRunner, patch_get_client, mock_client
|
|
174
|
+
):
|
|
175
|
+
# The real SDK returns a ScheduledRunListResponse model with an
|
|
176
|
+
# ``items`` field (plus ``next_cursor``), not a plain list.
|
|
177
|
+
from teardrop.models import ScheduledRun, ScheduledRunListResponse
|
|
178
|
+
|
|
179
|
+
model = ScheduledRunListResponse(
|
|
180
|
+
items=[
|
|
181
|
+
ScheduledRun(
|
|
182
|
+
**_schedule(
|
|
183
|
+
name="model-backed",
|
|
184
|
+
org_id="org_1",
|
|
185
|
+
user_id="user_1",
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
],
|
|
189
|
+
next_cursor=None,
|
|
190
|
+
)
|
|
191
|
+
mock_client.schedules.list.return_value = model
|
|
192
|
+
result = runner.invoke(app, ["schedules", "list"])
|
|
193
|
+
assert result.exit_code == 0, result.output
|
|
194
|
+
assert "model-backed" in result.output
|
|
195
|
+
|
|
196
|
+
def test_list_handles_dict_response_envelope(self, runner, patch_get_client, mock_client):
|
|
197
|
+
mock_client.schedules.list.return_value = {
|
|
198
|
+
"items": [_schedule(name="dict-backed")],
|
|
199
|
+
"next_cursor": None,
|
|
200
|
+
}
|
|
201
|
+
result = runner.invoke(app, ["schedules", "list", "--json"])
|
|
202
|
+
assert result.exit_code == 0, result.output
|
|
203
|
+
assert '"name": "dict-backed"' in result.output
|
|
204
|
+
|
|
94
205
|
|
|
95
206
|
class TestGet:
|
|
96
207
|
def test_get_json(self, runner: CliRunner, patch_get_client, mock_client):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|