valkyrie-cli 2026.11.9__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.
Files changed (60) hide show
  1. valkyrie/cli/agent/__init__.py +27 -0
  2. valkyrie/cli/agent/lifecycle.py +130 -0
  3. valkyrie/cli/benchmark/__init__.py +29 -0
  4. valkyrie/cli/benchmark/definitions.py +73 -0
  5. valkyrie/cli/benchmark/services.py +147 -0
  6. valkyrie/cli/benchmark/tasks.py +65 -0
  7. valkyrie/cli/client.py +50 -0
  8. valkyrie/cli/dataset.py +182 -0
  9. valkyrie/cli/display.py +158 -0
  10. valkyrie/cli/entry.py +12 -0
  11. valkyrie/cli/lazy.py +38 -0
  12. valkyrie/cli/logging.py +20 -0
  13. valkyrie/cli/login.py +115 -0
  14. valkyrie/cli/main.py +62 -0
  15. valkyrie/cli/org.py +121 -0
  16. valkyrie/cli/project.py +339 -0
  17. valkyrie/cli/provider.py +68 -0
  18. valkyrie/cli/queue.py +105 -0
  19. valkyrie/cli/resource.py +65 -0
  20. valkyrie/cli/run/__init__.py +50 -0
  21. valkyrie/cli/run/analyze.py +81 -0
  22. valkyrie/cli/run/errors.py +113 -0
  23. valkyrie/cli/run/fetch.py +46 -0
  24. valkyrie/cli/run/filter_options.py +18 -0
  25. valkyrie/cli/run/list_runs.py +217 -0
  26. valkyrie/cli/run/logs.py +109 -0
  27. valkyrie/cli/run/outputs.py +111 -0
  28. valkyrie/cli/run/progress.py +264 -0
  29. valkyrie/cli/run/results.py +127 -0
  30. valkyrie/cli/run/resume.py +168 -0
  31. valkyrie/cli/run/retag.py +28 -0
  32. valkyrie/cli/run/shed.py +48 -0
  33. valkyrie/cli/run/snapshot.py +142 -0
  34. valkyrie/cli/run/start.py +386 -0
  35. valkyrie/cli/run/status.py +88 -0
  36. valkyrie/cli/run/stop.py +59 -0
  37. valkyrie/cli/run/task_ids.py +51 -0
  38. valkyrie/cli/run/tasks.py +98 -0
  39. valkyrie/cli/run/update.py +25 -0
  40. valkyrie/cli/secret.py +65 -0
  41. valkyrie/cli/ui.py +20 -0
  42. valkyrie/display.py +36 -0
  43. valkyrie/tui/__init__.py +1 -0
  44. valkyrie/tui/app.py +97 -0
  45. valkyrie/tui/browse.py +167 -0
  46. valkyrie/tui/datasets.py +219 -0
  47. valkyrie/tui/errors.py +128 -0
  48. valkyrie/tui/fake.py +452 -0
  49. valkyrie/tui/fixtures/runs.json +128 -0
  50. valkyrie/tui/run.py +692 -0
  51. valkyrie/tui/runs.py +310 -0
  52. valkyrie/tui/source.py +137 -0
  53. valkyrie/tui/theme.py +126 -0
  54. valkyrie/tui/valkyrie.tcss +185 -0
  55. valkyrie/tui/widgets.py +214 -0
  56. valkyrie_cli-2026.11.9.dist-info/METADATA +105 -0
  57. valkyrie_cli-2026.11.9.dist-info/RECORD +60 -0
  58. valkyrie_cli-2026.11.9.dist-info/WHEEL +4 -0
  59. valkyrie_cli-2026.11.9.dist-info/entry_points.txt +3 -0
  60. valkyrie_cli-2026.11.9.dist-info/licenses/LICENSE +661 -0
@@ -0,0 +1,27 @@
1
+ import click
2
+
3
+ from valkyrie.cli.lazy import LazyGroup
4
+
5
+
6
+ @click.group(
7
+ cls=LazyGroup,
8
+ lazy_commands={
9
+ "install": (
10
+ "valkyrie.cli.agent.lifecycle:install",
11
+ "Install a GitHub agent through Tracker, replacing an existing alias.",
12
+ ),
13
+ "push": (
14
+ "valkyrie.cli.agent.lifecycle:push",
15
+ "Upload a local agent through Tracker, replacing an existing alias.",
16
+ ),
17
+ "remove": ("valkyrie.cli.agent.lifecycle:agent_remove", "Remove an installed agent."),
18
+ "download": ("valkyrie.cli.agent.lifecycle:download", "Download an installed agent."),
19
+ "list": ("valkyrie.cli.agent.lifecycle:list_installed_agents", "List installed agents."),
20
+ },
21
+ help="Install, upload, list, download, and remove agents.",
22
+ )
23
+ def agent():
24
+ pass
25
+
26
+
27
+ __all__ = ["agent"]
@@ -0,0 +1,130 @@
1
+ import json
2
+ from datetime import datetime
3
+ from pathlib import Path
4
+
5
+ import click
6
+ from valkyrie.sdk import ValkyrieClient
7
+
8
+ from valkyrie.display import terminal_safe
9
+
10
+ from valkyrie.cli.client import call
11
+ from valkyrie.cli.display import format_option, format_table, local_time, paginate_cli_pages
12
+
13
+
14
+ @click.command(name="install", help="Install a GitHub agent through Tracker, replacing an existing alias.")
15
+ @click.argument("github_url", type=str)
16
+ @click.option(
17
+ "--name",
18
+ "-n",
19
+ type=str,
20
+ required=False,
21
+ help="Agent name (defaults to the contract name).",
22
+ )
23
+ def install(github_url: str, name: str | None):
24
+ entry = call(lambda client: client.agents.install(github_url, name=name))
25
+ click.echo(click.style(f"✓ Agent '{entry.name}' installed successfully!", fg="green", bold=True))
26
+
27
+
28
+ @click.command(name="push", help="Upload a local agent through Tracker, replacing an existing alias.")
29
+ @click.argument("agent_path", type=click.Path(exists=True, path_type=Path, file_okay=False, dir_okay=True))
30
+ @click.option(
31
+ "--name",
32
+ "-n",
33
+ type=str,
34
+ required=False,
35
+ help="Agent name (defaults to the contract name).",
36
+ )
37
+ def push(agent_path: Path, name: str | None):
38
+ entry = call(lambda client: client.agents.push(agent_path, name=name or None))
39
+ click.echo(click.style(f"✓ Agent '{entry.name}' pushed successfully!", fg="green", bold=True))
40
+
41
+
42
+ @click.command(name="remove", help="Remove an installed agent.")
43
+ @click.argument("agent_name", type=str)
44
+ @click.option("--yes", "-y", is_flag=True, help="Skip the confirmation prompt.")
45
+ def agent_remove(agent_name: str, yes: bool):
46
+ if not yes and not click.confirm(f"Are you sure you want to remove agent '{agent_name}'?"):
47
+ click.echo("Cancelled.")
48
+ return
49
+
50
+ call(lambda client: client.agents.remove(agent_name))
51
+ click.echo(click.style(f"✓ Agent '{agent_name}' removed successfully!", fg="green", bold=True))
52
+
53
+
54
+ @click.command(name="download", help="Download an installed agent.")
55
+ @click.argument("agent_name", type=str)
56
+ @click.option(
57
+ "--output-dir",
58
+ "-o",
59
+ type=click.Path(path_type=Path, file_okay=False, dir_okay=True),
60
+ default=None,
61
+ required=False,
62
+ help="Output directory for downloaded agent (default: current directory).",
63
+ )
64
+ @click.option("--overwrite", is_flag=True, help="Replace an existing agent directory after validating the download.")
65
+ def download(agent_name: str, output_dir: Path | None, overwrite: bool):
66
+ call(lambda client: client.agents.download(agent_name, output_dir, overwrite=overwrite))
67
+ click.echo(click.style(f"✓ Agent '{agent_name}' downloaded successfully!", fg="green", bold=True))
68
+
69
+
70
+ @click.command(name="list", help="List installed agents.")
71
+ @click.option("--all", "all_agents", is_flag=True, help="Show every agent without interactive paging.")
72
+ @format_option("Output format. JSON includes every agent without interactive paging.")
73
+ def list_installed_agents(all_agents: bool, output_format: str):
74
+
75
+ async def list_agents(client: ValkyrieClient) -> list[tuple[str, datetime | None]]:
76
+ response = await client.agents.list()
77
+ try:
78
+ return [
79
+ (agent.name, datetime.fromisoformat(agent.last_modified) if agent.last_modified else None)
80
+ for agent in response.agents
81
+ ]
82
+ except ValueError as error:
83
+ raise click.ClickException(terminal_safe(str(error), preserve_newlines=True)) from error
84
+
85
+ agents = call(list_agents)
86
+
87
+ if output_format == "json":
88
+ click.echo(
89
+ json.dumps(
90
+ {
91
+ "agents": [
92
+ {"name": name, "last_modified": modified.isoformat() if modified else None}
93
+ for name, modified in agents
94
+ ]
95
+ }
96
+ )
97
+ )
98
+ return
99
+
100
+ if not agents:
101
+ click.echo(click.style("No agents found.", fg="yellow"))
102
+ return
103
+
104
+ if all_agents:
105
+ _format_agents_response(agents, 1, 1, len(agents))
106
+ else:
107
+ paginate_agents(agents)
108
+
109
+
110
+ def _format_agents_response(
111
+ agents: list[tuple[str, datetime | None]],
112
+ current_page: int,
113
+ total_pages: int,
114
+ total_count: int,
115
+ ) -> None:
116
+ rows = [
117
+ {"Agent": name, "Last Modified": local_time(last_modified) if last_modified else "-"}
118
+ for name, last_modified in agents
119
+ ]
120
+
121
+ format_table(rows, ["Agent", "Last Modified"], current_page, total_pages, total_count, "agent")
122
+
123
+
124
+ def paginate_agents(agents: list[tuple[str, datetime | None]], limit: int = 10) -> None:
125
+ paginate_cli_pages(
126
+ lambda offset, page_limit: (len(agents), agents[offset : offset + page_limit]),
127
+ _format_agents_response,
128
+ limit=limit,
129
+ render_empty=lambda: click.echo(click.style("No agents found.", fg="yellow")),
130
+ )
@@ -0,0 +1,29 @@
1
+ import click
2
+
3
+ from valkyrie.cli.lazy import LazyGroup
4
+
5
+
6
+ @click.group(
7
+ cls=LazyGroup,
8
+ lazy_commands={
9
+ "tasks": ("valkyrie.cli.benchmark.tasks:tasks", "Export the task IDs of a benchmark dataset."),
10
+ "service": (
11
+ "valkyrie.cli.benchmark.services:service",
12
+ "Save benchmark service URLs that your runs use instead of the hosted services.",
13
+ ),
14
+ },
15
+ hidden_commands={
16
+ "create": "valkyrie.cli.benchmark.definitions:create_benchmark",
17
+ "list": "valkyrie.cli.benchmark.definitions:list_benchmarks",
18
+ "show": "valkyrie.cli.benchmark.definitions:show_benchmark",
19
+ "update": "valkyrie.cli.benchmark.definitions:update_benchmark",
20
+ "remove": "valkyrie.cli.benchmark.definitions:remove_benchmark",
21
+ "restore": "valkyrie.cli.benchmark.definitions:restore_benchmark",
22
+ },
23
+ help="Export the task IDs in a benchmark dataset, and save benchmark services.",
24
+ )
25
+ def benchmark():
26
+ pass
27
+
28
+
29
+ __all__ = ["benchmark"]
@@ -0,0 +1,73 @@
1
+ import click
2
+
3
+ from valkyrie.cli.client import call
4
+ from valkyrie.cli.resource import INCLUDE_ARCHIVED, RESOURCE_NAME, list_resources, show_resource
5
+ from valkyrie.cli.display import format_option, one_line
6
+
7
+
8
+ @click.command(
9
+ name="create",
10
+ help=(
11
+ "Create a benchmark that datasets can belong to.\n\n"
12
+ "NAME is lowercase letters and digits with single '.', '_' or '-' separators, such as swe-bench.\n\n"
13
+ "Example:\n"
14
+ ' valkyrie benchmark create swe-bench --description "SWE-bench tasks"'
15
+ ),
16
+ )
17
+ @click.argument("name", metavar="NAME", type=RESOURCE_NAME)
18
+ @click.option("--description", default="", help="What the benchmark covers.")
19
+ def create_benchmark(name: str, description: str) -> None:
20
+ created = call(lambda client: client.benchmark_definitions.create(name, description=description))
21
+ click.echo(click.style(f"✓ Benchmark created: {one_line(created.name)}", fg="green", bold=True))
22
+
23
+
24
+ @click.command(name="list", help="List your organization's benchmarks.")
25
+ @INCLUDE_ARCHIVED
26
+ @format_option()
27
+ def list_benchmarks(include_archived: bool, output_format: str) -> None:
28
+ response = call(lambda client: client.benchmark_definitions.list(include_archived=include_archived))
29
+ list_resources(response, response.benchmarks, output_format, "benchmark")
30
+
31
+
32
+ @click.command(name="show", help="Show one benchmark, archived or not.")
33
+ @click.argument("name", metavar="NAME", type=RESOURCE_NAME)
34
+ @format_option()
35
+ def show_benchmark(name: str, output_format: str) -> None:
36
+ found = call(lambda client: client.benchmark_definitions.get(name))
37
+ show_resource(found, output_format)
38
+
39
+
40
+ @click.command(
41
+ name="update",
42
+ help=(
43
+ "Change a benchmark's description. Its name never changes.\n\n"
44
+ "Example:\n"
45
+ ' valkyrie benchmark update swe-bench --description "SWE-bench family"'
46
+ ),
47
+ )
48
+ @click.argument("name", metavar="NAME", type=RESOURCE_NAME)
49
+ @click.option("--description", required=True, help="The description the benchmark now has.")
50
+ def update_benchmark(name: str, description: str) -> None:
51
+ updated = call(lambda client: client.benchmark_definitions.update(name, description=description))
52
+ click.echo(click.style(f"✓ Benchmark updated: {one_line(updated.name)}", fg="green", bold=True))
53
+
54
+
55
+ @click.command(
56
+ name="remove",
57
+ help="Archive a benchmark once it has no active datasets. It stays readable, keeps its name, and can be restored.",
58
+ )
59
+ @click.argument("name", metavar="NAME", type=RESOURCE_NAME)
60
+ @click.option("--yes", "-y", is_flag=True, help="Skip the confirmation prompt.")
61
+ def remove_benchmark(name: str, yes: bool) -> None:
62
+ if not yes and not click.confirm(f"Archive benchmark '{name}'?"):
63
+ click.echo("Cancelled.")
64
+ return
65
+ call(lambda client: client.benchmark_definitions.remove(name))
66
+ click.echo(click.style(f"✓ Benchmark '{name}' archived.", fg="green", bold=True))
67
+
68
+
69
+ @click.command(name="restore", help="Restore an archived benchmark so it can change again.")
70
+ @click.argument("name", metavar="NAME", type=RESOURCE_NAME)
71
+ def restore_benchmark(name: str) -> None:
72
+ restored = call(lambda client: client.benchmark_definitions.restore(name))
73
+ click.echo(click.style(f"✓ Benchmark restored: {one_line(restored.name)}", fg="green", bold=True))
@@ -0,0 +1,147 @@
1
+ from collections.abc import Callable, Sequence
2
+ from urllib.parse import urlparse
3
+
4
+ import click
5
+ from valkyrie.sdk import BenchmarkServiceEntry, BenchmarkServiceHealth
6
+
7
+ from valkyrie.cli.client import call
8
+ from valkyrie.cli.display import format_table, paginate_cli_pages
9
+
10
+
11
+ @click.group(help="Save benchmark service URLs that your runs use instead of the hosted services.")
12
+ def service() -> None:
13
+ pass
14
+
15
+
16
+ @service.command(
17
+ "set",
18
+ help=(
19
+ "Use a service URL for your runs of a benchmark.\n"
20
+ "\n"
21
+ "Replaces the URL and credential saved before. The Tracker must be able to reach the URL.\n"
22
+ "\n"
23
+ "Example:\n"
24
+ " valkyrie benchmark service set swebench https://my-tunnel.ngrok.io --auth-secret swebench-key"
25
+ ),
26
+ )
27
+ @click.argument("benchmark")
28
+ @click.argument("url")
29
+ @click.option(
30
+ "--auth-secret",
31
+ metavar="SECRET",
32
+ help="Organization secret whose value the Tracker sends to the service. Create it with `valkyrie secret set`.",
33
+ )
34
+ @click.option("--auth-header", metavar="HEADER", help="Header that carries the secret. Defaults to Authorization.")
35
+ def service_set(benchmark: str, url: str, auth_secret: str | None, auth_header: str | None) -> None:
36
+ settings = call(
37
+ lambda client: client.settings.set_benchmark_service(
38
+ benchmark, url, auth_secret=auth_secret, auth_header=auth_header
39
+ )
40
+ )
41
+ saved = settings.benchmark_services[benchmark]
42
+ message = f"Runs of {benchmark} now use {saved.url}"
43
+ if saved.auth_secret_name:
44
+ message += f", sending the secret {saved.auth_secret_name} as {saved.auth_header_name}"
45
+ click.echo(f"{message}.")
46
+
47
+
48
+ @service.command(
49
+ "remove",
50
+ help=(
51
+ "Go back to the hosted service for your runs of a benchmark.\n"
52
+ "\n"
53
+ "Example:\n"
54
+ " valkyrie benchmark service remove swebench"
55
+ ),
56
+ )
57
+ @click.argument("benchmark")
58
+ def service_remove(benchmark: str) -> None:
59
+ call(lambda client: client.settings.remove_benchmark_service(benchmark))
60
+ click.echo(f"Runs of {benchmark} now use the hosted service.")
61
+
62
+
63
+ @service.command(
64
+ "list",
65
+ help=(
66
+ "List hosted benchmark services and your saved URLs, with their latency.\n"
67
+ "\n"
68
+ "A URL you saved replaces the hosted service with the same name."
69
+ ),
70
+ )
71
+ def service_list() -> None:
72
+ saved = call(lambda client: client.settings.get()).benchmark_services
73
+
74
+ catalog = call(lambda client: client.services.catalog())
75
+ services_by_name = {service.name: service for service in catalog.services}
76
+ services_by_name.update({name: BenchmarkServiceEntry(name=name, url=entry.url) for name, entry in saved.items()})
77
+ if not services_by_name:
78
+ click.echo(click.style("No benchmark services configured.", fg="yellow"))
79
+ return
80
+
81
+ paginate_services(
82
+ list(services_by_name.values()),
83
+ check_services=lambda entries: call(lambda client: client.services.check(entries)).services,
84
+ )
85
+
86
+
87
+ def _service_source_domain(service: BenchmarkServiceHealth) -> str:
88
+ host = urlparse(service.url).netloc or service.url
89
+ return host.removeprefix(f"{service.name}.")
90
+
91
+
92
+ def _health_checked_page(
93
+ services: Sequence[BenchmarkServiceEntry],
94
+ cache: dict[str, BenchmarkServiceHealth],
95
+ check_services: Callable[[list[BenchmarkServiceEntry]], list[BenchmarkServiceHealth]],
96
+ ) -> list[BenchmarkServiceHealth]:
97
+ missing = [service for service in services if service.name not in cache]
98
+ if missing:
99
+ cache.update({service.name: service for service in check_services(missing)})
100
+ for service in missing:
101
+ cache.setdefault(
102
+ service.name, BenchmarkServiceHealth(name=service.name, url=service.url, healthy=False, latency_ms=None)
103
+ )
104
+ return [cache[service.name] for service in services]
105
+
106
+
107
+ def paginate_services(
108
+ services: Sequence[BenchmarkServiceEntry],
109
+ check_services: Callable[[list[BenchmarkServiceEntry]], list[BenchmarkServiceHealth]],
110
+ limit: int = 10,
111
+ ) -> None:
112
+ health_cache: dict[str, BenchmarkServiceHealth] = {}
113
+
114
+ def load_page(offset: int, page_limit: int) -> tuple[int, list[BenchmarkServiceHealth]]:
115
+ page_services = _health_checked_page(services[offset : offset + page_limit], health_cache, check_services)
116
+ return len(services), page_services
117
+
118
+ def render_page(
119
+ page_services: list[BenchmarkServiceHealth],
120
+ current_page: int,
121
+ total_pages: int,
122
+ total_count: int,
123
+ ) -> None:
124
+ rows = [
125
+ {
126
+ "Benchmark": service.name,
127
+ "Service URL": service.url,
128
+ "Source": _service_source_domain(service),
129
+ "Latency": f"{service.latency_ms} ms" if service.latency_ms is not None else "-",
130
+ }
131
+ for service in page_services
132
+ ]
133
+ format_table(
134
+ rows,
135
+ ["Benchmark", "Service URL", "Source", "Latency"],
136
+ current_page,
137
+ total_pages,
138
+ total_count,
139
+ "service",
140
+ )
141
+
142
+ paginate_cli_pages(
143
+ load_page,
144
+ render_page,
145
+ limit=limit,
146
+ render_empty=lambda: click.echo(click.style("No benchmark services found.", fg="yellow")),
147
+ )
@@ -0,0 +1,65 @@
1
+ from pathlib import Path
2
+
3
+ import click
4
+
5
+ from valkyrie.cli.client import call
6
+
7
+
8
+ @click.command(
9
+ name="tasks",
10
+ help=(
11
+ "Export the task IDs of a benchmark dataset.\n"
12
+ "\n"
13
+ "Example:\n"
14
+ " valkyrie benchmark tasks swebench --dataset default"
15
+ ),
16
+ )
17
+ @click.argument("benchmark_name", type=str)
18
+ @click.option(
19
+ "--dataset",
20
+ type=str,
21
+ required=False,
22
+ default=None,
23
+ help="Dataset name to fetch from the benchmark service (defaults to 'default').",
24
+ )
25
+ @click.option(
26
+ "--header",
27
+ "-H",
28
+ "headers",
29
+ multiple=True,
30
+ nargs=2,
31
+ type=(str, str),
32
+ help="Custom header for benchmark service requests (e.g., -H Authorization my-credential).",
33
+ )
34
+ @click.option(
35
+ "--ignore-custom-services",
36
+ "--ics",
37
+ is_flag=True,
38
+ help="Use the hosted benchmark service instead of the URL saved with `valkyrie benchmark service set`.",
39
+ )
40
+ @click.option(
41
+ "--output",
42
+ "-o",
43
+ type=click.Path(path_type=Path, file_okay=True, dir_okay=False),
44
+ required=False,
45
+ default=None,
46
+ help="Path to save task IDs, one per line. Defaults to <benchmark>-<dataset>-tasks.txt.",
47
+ )
48
+ def tasks(
49
+ benchmark_name: str,
50
+ dataset: str | None,
51
+ headers: tuple[tuple[str, str]],
52
+ ignore_custom_services: bool,
53
+ output: Path | None,
54
+ ):
55
+ task_ids = call(
56
+ lambda client: client.services.task_ids(
57
+ benchmark_name,
58
+ dataset=dataset,
59
+ service_headers=dict(headers),
60
+ ignore_custom_services=ignore_custom_services,
61
+ )
62
+ )
63
+ output_path = output or Path(f"{benchmark_name}-{dataset or 'default'}-tasks.txt")
64
+ output_path.write_text("\n".join(task_ids) + "\n", encoding="utf-8")
65
+ click.echo(f"Wrote {len(task_ids)} task IDs to {output_path}")
valkyrie/cli/client.py ADDED
@@ -0,0 +1,50 @@
1
+ import asyncio
2
+ import traceback
3
+ from collections.abc import Awaitable, Callable
4
+ from typing import Any, TypeVar
5
+
6
+ import click
7
+ from valkyrie.sdk import SessionAuth, ValkyrieAPIError, ValkyrieClient, ValkyrieConfig, ValkyrieSDKError
8
+
9
+ from valkyrie.display import terminal_safe
10
+
11
+ Result = TypeVar("Result")
12
+
13
+
14
+ def call(operation: Callable[[ValkyrieClient], Awaitable[Result]], auth: SessionAuth | None = None) -> Result:
15
+
16
+ async def run() -> Result:
17
+ async with ValkyrieClient(auth=auth) as client:
18
+ return await operation(client)
19
+
20
+ try:
21
+ return asyncio.run(run())
22
+ except ValkyrieAPIError as error:
23
+ if error.status_code == 401:
24
+ raise click.ClickException(
25
+ _unauthorized(auth is not None or ValkyrieConfig().http_auth() is not None)
26
+ ) from error
27
+ message = f"{_describe(error.detail)} ({error.status_code})" if isinstance(error.detail, list) else str(error)
28
+ raise click.ClickException(terminal_safe(message, preserve_newlines=True)) from error
29
+ except (ValkyrieSDKError, ValueError, OSError) as error:
30
+ if isinstance(error, ValueError) and not _rejected_by_sdk(error):
31
+ raise
32
+ raise click.ClickException(terminal_safe(str(error), preserve_newlines=True)) from error
33
+
34
+
35
+ def _rejected_by_sdk(error: ValueError) -> bool:
36
+ return isinstance(error, ValkyrieSDKError) or any(
37
+ frame.f_globals.get("__name__", "").startswith("valkyrie.sdk")
38
+ for frame, _ in traceback.walk_tb(error.__traceback__)
39
+ )
40
+
41
+
42
+ def _unauthorized(has_credentials: bool) -> str:
43
+ problem = "The Tracker rejected your credentials." if has_credentials else "You are not logged in."
44
+ return f"{problem} Run `valkyrie login`."
45
+
46
+
47
+ def _describe(detail: Any) -> Any:
48
+ if isinstance(detail, list):
49
+ return "; ".join(f"{error['loc'][-1]}: {error['msg']}" for error in detail)
50
+ return detail