abrade-cli 0.1.2__tar.gz → 0.1.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: abrade-cli
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Abrade's self-hosted CLI — run the same injection suite entirely offline, no SaaS required.
5
5
  Keywords: ai-agents,security-testing,prompt-injection,red-teaming,llm-security
6
6
  Author: Abrade
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "abrade-cli"
3
- version = "0.1.2"
3
+ version = "0.1.3"
4
4
  description = "Abrade's self-hosted CLI — run the same injection suite entirely offline, no SaaS required."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "abrade-cli"
3
- version = "0.1.2"
3
+ version = "0.1.3"
4
4
  description = "Abrade's self-hosted CLI — run the same injection suite entirely offline, no SaaS required."
5
5
  readme = "README.md"
6
6
  # The project, not a personal handle. PyPI renders this as "Author" on
@@ -23,7 +23,16 @@ from typing import TYPE_CHECKING
23
23
  if TYPE_CHECKING:
24
24
  from abrade_cli.models import RunRecord
25
25
 
26
- DEFAULT_API_URL = "http://localhost:8123"
26
+ # The hosted console. This defaulted to http://localhost:8123, which is
27
+ # where the API runs under docker-compose — correct for someone developing
28
+ # Abrade, wrong for everyone who installs it. `abrade login` from a clean
29
+ # machine tried to reach a port on the user's own laptop and failed with a
30
+ # connection error that named localhost, which reads like the CLI is
31
+ # broken rather than misconfigured.
32
+ #
33
+ # ABRADE_API_URL still overrides, which is how local development points it
34
+ # back at compose.
35
+ DEFAULT_API_URL = "https://abrade.ai"
27
36
 
28
37
 
29
38
  class AbradeApiError(Exception):
@@ -16,7 +16,7 @@ from abrade_cli.execution import MissingCredentialError
16
16
  from abrade_cli.models import SuiteMode
17
17
  from abrade_cli.report import render_pdf
18
18
  from abrade_cli.runner import run_suite
19
- from abrade_cli.storage import list_runs, load_run, save_run
19
+ from abrade_cli.storage import latest_run_id, list_runs, load_run, save_run
20
20
 
21
21
  app = typer.Typer(
22
22
  name="abrade",
@@ -236,7 +236,12 @@ def report(
236
236
 
237
237
  @app.command()
238
238
  def push(
239
- run_id: str = typer.Option(..., "--run"),
239
+ run_id: str | None = typer.Option(None, "--run"),
240
+ # For CI, which has just produced a run and has no way to learn its id
241
+ # without parsing stdout. Ordered by write time, not by id — see
242
+ # storage.latest_run_id, because run ids are UUIDs and sorting them
243
+ # would upload an arbitrary run.
244
+ latest: bool = typer.Option(False, "--latest"),
240
245
  agent: Path = typer.Option(config_module.DEFAULT_CONFIG_PATH, "--agent"),
241
246
  api_url: str | None = typer.Option(None, "--api-url"),
242
247
  ) -> None:
@@ -245,6 +250,20 @@ def push(
245
250
  Optional — abrade run/list/report never require this. Nothing is sent
246
251
  unless you run this command explicitly (see `abrade login --help`).
247
252
  """
253
+ if latest and run_id:
254
+ console.print("[red]Pass either --run or --latest, not both.[/red]")
255
+ raise typer.Exit(code=2)
256
+ if latest:
257
+ run_id = latest_run_id()
258
+ if run_id is None:
259
+ console.print("[red]No local runs found — run `abrade run` first.[/red]")
260
+ raise typer.Exit(code=1)
261
+ if not run_id:
262
+ console.print(
263
+ "[red]Which run? Pass --run <id>, or --latest for the most recent.[/red]"
264
+ )
265
+ raise typer.Exit(code=2)
266
+
248
267
  try:
249
268
  record = load_run(run_id)
250
269
  except FileNotFoundError as exc:
@@ -109,6 +109,22 @@ def load_run(run_id: str, base: Path | None = None) -> RunRecord:
109
109
  )
110
110
 
111
111
 
112
+ def latest_run_id(base: Path | None = None) -> str | None:
113
+ """The most recently written run, or None if there are none.
114
+
115
+ Ordered by write time rather than by id. Run ids are UUIDs, so sorting
116
+ them alphabetically returns an arbitrary run — and `abrade push
117
+ --latest` picking the wrong one would upload the wrong evidence
118
+ silently, which is worse than failing.
119
+ """
120
+ root = base or Path.cwd()
121
+ runs_dir = root / RUNS_DIR
122
+ if not runs_dir.exists():
123
+ return None
124
+ runs = sorted(runs_dir.glob("*.json"), key=lambda p: p.stat().st_mtime)
125
+ return runs[-1].stem if runs else None
126
+
127
+
112
128
  def list_runs(base: Path | None = None) -> list[str]:
113
129
  root = base or Path.cwd()
114
130
  runs_dir = root / RUNS_DIR
File without changes