vidbyte-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.
- vidbyte_cli/__init__.py +11 -0
- vidbyte_cli/__main__.py +6 -0
- vidbyte_cli/cli.py +18 -0
- vidbyte_cli/commands/__init__.py +86 -0
- vidbyte_cli/commands/agent_options.py +53 -0
- vidbyte_cli/commands/agents/__init__.py +31 -0
- vidbyte_cli/commands/agents/suggestion/__init__.py +38 -0
- vidbyte_cli/commands/agents/suggestion/prompts/__init__.py +3 -0
- vidbyte_cli/commands/agents/suggestion/prompts/all_categories.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/approaches.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/avoid.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/blockers.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/capability.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/categories.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/category.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/completed.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/constraint.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/context.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/count.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/critic_model.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/decision.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/dry_run.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/extra_compute.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/feedback_accept.md +7 -0
- vidbyte_cli/commands/agents/suggestion/prompts/feedback_group.md +7 -0
- vidbyte_cli/commands/agents/suggestion/prompts/feedback_project.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/feedback_reason.md +98 -0
- vidbyte_cli/commands/agents/suggestion/prompts/feedback_reject.md +7 -0
- vidbyte_cli/commands/agents/suggestion/prompts/feedback_suggestion.md +101 -0
- vidbyte_cli/commands/agents/suggestion/prompts/files.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/forbidden.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/goal.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/handoff.md +7 -0
- vidbyte_cli/commands/agents/suggestion/prompts/horizon.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/hypotheses.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/idea.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/in_progress.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/input.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/library.py +29 -0
- vidbyte_cli/commands/agents/suggestion/prompts/max_messages.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/max_output_tokens.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/max_total_tokens.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/mistakes.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/outcomes.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/project.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/project_create.md +7 -0
- vidbyte_cli/commands/agents/suggestion/prompts/project_description.md +98 -0
- vidbyte_cli/commands/agents/suggestion/prompts/project_group.md +7 -0
- vidbyte_cli/commands/agents/suggestion/prompts/project_key.md +96 -0
- vidbyte_cli/commands/agents/suggestion/prompts/project_list.md +7 -0
- vidbyte_cli/commands/agents/suggestion/prompts/project_title.md +99 -0
- vidbyte_cli/commands/agents/suggestion/prompts/provider.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/question.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/risks.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/rounds.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/run.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/success.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/timeout.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/trajectory.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/view.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/view_all.md +5 -0
- vidbyte_cli/commands/agents/suggestion/render.py +77 -0
- vidbyte_cli/commands/agents/suggestion/request_builder.py +240 -0
- vidbyte_cli/commands/agents/suggestion/suggest.py +154 -0
- vidbyte_cli/commands/agents/suggestion/suggestion_categories.py +77 -0
- vidbyte_cli/commands/agents/suggestion/suggestion_feedback.py +85 -0
- vidbyte_cli/commands/agents/suggestion/suggestion_handoff.py +67 -0
- vidbyte_cli/commands/agents/suggestion/suggestion_projects.py +95 -0
- vidbyte_cli/commands/auth/__init__.py +0 -0
- vidbyte_cli/commands/auth/login.py +100 -0
- vidbyte_cli/commands/auth/logout.py +41 -0
- vidbyte_cli/commands/auth/whoami.py +51 -0
- vidbyte_cli/commands/billing/__init__.py +20 -0
- vidbyte_cli/commands/billing/prompts/__init__.py +1 -0
- vidbyte_cli/commands/billing/prompts/confirm.md +16 -0
- vidbyte_cli/commands/billing/prompts/group.md +13 -0
- vidbyte_cli/commands/billing/prompts/idempotency_key.md +16 -0
- vidbyte_cli/commands/billing/prompts/library.py +18 -0
- vidbyte_cli/commands/billing/prompts/top_up.md +16 -0
- vidbyte_cli/commands/billing/top_up.py +65 -0
- vidbyte_cli/commands/config/__init__.py +0 -0
- vidbyte_cli/commands/config/get.py +45 -0
- vidbyte_cli/commands/config/set.py +39 -0
- vidbyte_cli/commands/provider/__init__.py +9 -0
- vidbyte_cli/commands/provider/login.py +102 -0
- vidbyte_cli/commands/provider/logout.py +45 -0
- vidbyte_cli/commands/provider/whoami.py +49 -0
- vidbyte_cli/commands/research/__init__.py +5 -0
- vidbyte_cli/commands/research/add.py +41 -0
- vidbyte_cli/commands/research/options.py +134 -0
- vidbyte_cli/commands/research/render.py +157 -0
- vidbyte_cli/commands/research/resume.py +37 -0
- vidbyte_cli/commands/research/start.py +37 -0
- vidbyte_cli/commands/research/status.py +32 -0
- vidbyte_cli/commands/research/thread.py +30 -0
- vidbyte_cli/commands/research/threads.py +35 -0
- vidbyte_cli/commands/research/watch.py +93 -0
- vidbyte_cli/commands/runtime/__init__.py +23 -0
- vidbyte_cli/commands/runtime/adversarial_team.py +41 -0
- vidbyte_cli/commands/runtime/doctor.py +44 -0
- vidbyte_cli/commands/runtime/list.py +47 -0
- vidbyte_cli/commands/runtime/persistence.py +141 -0
- vidbyte_cli/commands/runtime/same_host_ensemble.py +221 -0
- vidbyte_cli/commands/runtime/stages.py +553 -0
- vidbyte_cli/commands/runtime/task_board.py +1342 -0
- vidbyte_cli/commands/setup/__init__.py +0 -0
- vidbyte_cli/commands/setup/doctor.py +64 -0
- vidbyte_cli/commands/setup/prompts/__init__.py +1 -0
- vidbyte_cli/commands/setup/prompts/browser.md +7 -0
- vidbyte_cli/commands/setup/prompts/library.py +18 -0
- vidbyte_cli/commands/setup/prompts/star.md +13 -0
- vidbyte_cli/commands/setup/star.py +132 -0
- vidbyte_cli/lib/__init__.py +0 -0
- vidbyte_cli/lib/api/__init__.py +0 -0
- vidbyte_cli/lib/api/client.py +236 -0
- vidbyte_cli/lib/api/endpoints/__init__.py +0 -0
- vidbyte_cli/lib/api/endpoints/auth.py +24 -0
- vidbyte_cli/lib/api/endpoints/billing.py +36 -0
- vidbyte_cli/lib/api/endpoints/research.py +103 -0
- vidbyte_cli/lib/api/endpoints/runtime.py +113 -0
- vidbyte_cli/lib/api/problem.py +86 -0
- vidbyte_cli/lib/api/response.py +101 -0
- vidbyte_cli/lib/api/retry.py +116 -0
- vidbyte_cli/lib/api/runtime_payment.py +89 -0
- vidbyte_cli/lib/auth/__init__.py +16 -0
- vidbyte_cli/lib/auth/credentials.py +223 -0
- vidbyte_cli/lib/auth/input.py +59 -0
- vidbyte_cli/lib/auth/keyring_store.py +126 -0
- vidbyte_cli/lib/auth/provider_credentials.py +157 -0
- vidbyte_cli/lib/auth/provider_input.py +54 -0
- vidbyte_cli/lib/auth/provider_resolver.py +89 -0
- vidbyte_cli/lib/auth/provider_store.py +149 -0
- vidbyte_cli/lib/auth/provider_verifier.py +219 -0
- vidbyte_cli/lib/auth/resolver.py +63 -0
- vidbyte_cli/lib/auth/verifier.py +38 -0
- vidbyte_cli/lib/config/__init__.py +25 -0
- vidbyte_cli/lib/config/atomic.py +47 -0
- vidbyte_cli/lib/config/config.py +141 -0
- vidbyte_cli/lib/config/migration.py +81 -0
- vidbyte_cli/lib/config/models.py +145 -0
- vidbyte_cli/lib/config/paths.py +66 -0
- vidbyte_cli/lib/config/resolver.py +137 -0
- vidbyte_cli/lib/constants/__init__.py +1 -0
- vidbyte_cli/lib/constants/runtime.py +186 -0
- vidbyte_cli/lib/errors/__init__.py +12 -0
- vidbyte_cli/lib/errors/cli_error.py +86 -0
- vidbyte_cli/lib/errors/codes.py +43 -0
- vidbyte_cli/lib/errors/failures.py +2910 -0
- vidbyte_cli/lib/errors/handler.py +72 -0
- vidbyte_cli/lib/files/__init__.py +6 -0
- vidbyte_cli/lib/files/documents.py +94 -0
- vidbyte_cli/lib/files/store.py +156 -0
- vidbyte_cli/lib/io/__init__.py +16 -0
- vidbyte_cli/lib/io/attachments.py +129 -0
- vidbyte_cli/lib/io/codex_attachments.py +47 -0
- vidbyte_cli/lib/io/prompt.py +71 -0
- vidbyte_cli/lib/io/streams.py +37 -0
- vidbyte_cli/lib/io/terminal.py +66 -0
- vidbyte_cli/lib/output/__init__.py +7 -0
- vidbyte_cli/lib/output/formats.py +29 -0
- vidbyte_cli/lib/output/manager.py +95 -0
- vidbyte_cli/lib/output/models.py +49 -0
- vidbyte_cli/lib/runtime/__init__.py +9 -0
- vidbyte_cli/lib/runtime/application.py +134 -0
- vidbyte_cli/lib/runtime/context.py +250 -0
- vidbyte_cli/lib/runtime/options.py +159 -0
- vidbyte_cli/lib/runtime/version.py +30 -0
- vidbyte_cli/lib/runtime_primitives/__init__.py +28 -0
- vidbyte_cli/lib/runtime_primitives/executor.py +54 -0
- vidbyte_cli/lib/runtime_primitives/gate.py +105 -0
- vidbyte_cli/lib/runtime_primitives/hosts.py +45 -0
- vidbyte_cli/lib/runtime_primitives/planner.py +79 -0
- vidbyte_cli/lib/runtime_primitives/stages.py +338 -0
- vidbyte_cli/lib/runtime_primitives/task_board.py +982 -0
- vidbyte_cli/lib/runtime_primitives/task_board_checkpoints.py +473 -0
- vidbyte_cli/lib/runtime_primitives/task_board_system.md +33 -0
- vidbyte_cli/lib/runtime_primitives/task_board_tasks.py +111 -0
- vidbyte_cli/lib/runtime_primitives/verification.py +68 -0
- vidbyte_cli/services/__init__.py +5 -0
- vidbyte_cli/services/ensemble/__init__.py +7 -0
- vidbyte_cli/services/ensemble/prompts/__init__.py +6 -0
- vidbyte_cli/services/ensemble/prompts/implementer_system.md +546 -0
- vidbyte_cli/services/ensemble/prompts/implementer_turn.md +49 -0
- vidbyte_cli/services/ensemble/prompts/library.py +188 -0
- vidbyte_cli/services/ensemble/prompts/planner_system.md +728 -0
- vidbyte_cli/services/ensemble/prompts/planner_turn.md +6 -0
- vidbyte_cli/services/ensemble/prompts/role_system.md +104 -0
- vidbyte_cli/services/ensemble/prompts/role_turn.md +19 -0
- vidbyte_cli/services/ensemble/prompts/selector_final_turn.md +19 -0
- vidbyte_cli/services/ensemble/prompts/selector_round_turn.md +17 -0
- vidbyte_cli/services/ensemble/prompts/selector_system.md +89 -0
- vidbyte_cli/services/ensemble/runner.py +30 -0
- vidbyte_cli/services/ensemble/sdk.py +134 -0
- vidbyte_cli/services/ensemble/service.py +233 -0
- vidbyte_cli/services/ensemble/settings.py +60 -0
- vidbyte_cli/services/persistence/__init__.py +6 -0
- vidbyte_cli/services/persistence/prompts/__init__.py +6 -0
- vidbyte_cli/services/persistence/prompts/library.py +46 -0
- vidbyte_cli/services/persistence/prompts/persistence_system.md +1 -0
- vidbyte_cli/services/persistence/prompts/persistence_turn.md +3 -0
- vidbyte_cli/services/persistence/runner.py +35 -0
- vidbyte_cli/services/persistence/session.py +116 -0
- vidbyte_cli/services/suggestions/__init__.py +7 -0
- vidbyte_cli/services/suggestions/categories.py +270 -0
- vidbyte_cli/services/suggestions/context.py +222 -0
- vidbyte_cli/services/suggestions/extra_compute.py +53 -0
- vidbyte_cli/services/suggestions/handoff.py +173 -0
- vidbyte_cli/services/suggestions/message_tools.py +47 -0
- vidbyte_cli/services/suggestions/project.py +253 -0
- vidbyte_cli/services/suggestions/prompts/__init__.py +1 -0
- vidbyte_cli/services/suggestions/prompts/categories/__init__.py +3 -0
- vidbyte_cli/services/suggestions/prompts/categories/adjacent_opportunity.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/alternative.md +38 -0
- vidbyte_cli/services/suggestions/prompts/categories/big_bets.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/bottleneck.md +38 -0
- vidbyte_cli/services/suggestions/prompts/categories/business_growth.md +38 -0
- vidbyte_cli/services/suggestions/prompts/categories/completion.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/continuation.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/coordination.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/creative_exploration.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/cross_domain.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/delegation.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/experiment.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/feedback.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/goal_clarification.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/immediate_next_steps.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/investigation.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/learning.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/leverage.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/long_term_directions.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/preparation.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/prerequisite.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/prioritization.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/product_experience.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/quick_wins.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/reframing.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/risk_prevention.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/simplification.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/stop_or_defer.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/strategy.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/technical_possibilities.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/verification.md +37 -0
- vidbyte_cli/services/suggestions/prompts/critic.md +209 -0
- vidbyte_cli/services/suggestions/prompts/critic_message.md +8 -0
- vidbyte_cli/services/suggestions/prompts/generator.md +64 -0
- vidbyte_cli/services/suggestions/prompts/library.py +72 -0
- vidbyte_cli/services/suggestions/prompts/message_generator_argument.md +1 -0
- vidbyte_cli/services/suggestions/prompts/message_generator_tool.md +1 -0
- vidbyte_cli/services/suggestions/prompts/message_parent_argument.md +1 -0
- vidbyte_cli/services/suggestions/prompts/message_parent_tool.md +1 -0
- vidbyte_cli/services/suggestions/prompts/message_receipt.md +1 -0
- vidbyte_cli/services/suggestions/prompts/revision.md +8 -0
- vidbyte_cli/services/suggestions/sdk.py +228 -0
- vidbyte_cli/services/suggestions/selection.py +78 -0
- vidbyte_cli/services/suggestions/service.py +540 -0
- vidbyte_cli/types/__init__.py +0 -0
- vidbyte_cli/types/api.py +101 -0
- vidbyte_cli/types/attachments.py +79 -0
- vidbyte_cli/types/ensemble.py +189 -0
- vidbyte_cli/types/provider.py +62 -0
- vidbyte_cli/types/research.py +260 -0
- vidbyte_cli/types/runtime.py +1098 -0
- vidbyte_cli/types/suggestions.py +518 -0
- vidbyte_cli-0.1.0.dist-info/METADATA +335 -0
- vidbyte_cli-0.1.0.dist-info/RECORD +269 -0
- vidbyte_cli-0.1.0.dist-info/WHEEL +5 -0
- vidbyte_cli-0.1.0.dist-info/entry_points.txt +2 -0
- vidbyte_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- vidbyte_cli-0.1.0.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""`vidbyte-cli doctor` — checks the CLI environment and credentials.
|
|
2
|
+
|
|
3
|
+
Diagnosis is read-only: doctor never migrates or repairs, because a user running it to find
|
|
4
|
+
out what is wrong should not have the answer change underneath them. It reports whether a
|
|
5
|
+
credential exists and where it came from, never its value. API identity and repository
|
|
6
|
+
checks arrive with the HTTP platform.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
from pydantic import JsonValue
|
|
13
|
+
|
|
14
|
+
from ...lib.auth.credentials import Credentials
|
|
15
|
+
from ...lib.output import OutputDocument
|
|
16
|
+
from ...lib.runtime.context import ApplicationContext
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DoctorCommand:
|
|
20
|
+
def register(self, parent: click.Group) -> None:
|
|
21
|
+
@parent.command(name="doctor", help="Diagnose local CLI configuration and credentials")
|
|
22
|
+
@click.pass_obj
|
|
23
|
+
def _run(context: ApplicationContext) -> None:
|
|
24
|
+
self.execute(context)
|
|
25
|
+
|
|
26
|
+
def execute(self, context: ApplicationContext) -> None:
|
|
27
|
+
config = context.resolved_config()
|
|
28
|
+
store = context.credential_store()
|
|
29
|
+
credential = context.credential_resolver().resolve(config.profile, config.api_url)
|
|
30
|
+
keyring_available = store.keyring.available()
|
|
31
|
+
legacy_present = context.paths().legacy_root.exists()
|
|
32
|
+
# A credential written by hand rather than by login never crossed the input guard, so
|
|
33
|
+
# the format is reported: the backend ignores a non-live key, which otherwise looks
|
|
34
|
+
# exactly like being logged out.
|
|
35
|
+
live_format = (
|
|
36
|
+
Credentials.is_live_format(credential.credentials.secret_value())
|
|
37
|
+
if credential is not None
|
|
38
|
+
else None
|
|
39
|
+
)
|
|
40
|
+
data: dict[str, JsonValue] = {
|
|
41
|
+
"profile": config.profile,
|
|
42
|
+
"api_url": config.api_url,
|
|
43
|
+
"config_path": config.config_path,
|
|
44
|
+
"credential_present": credential is not None,
|
|
45
|
+
"credential_source": credential.source.value if credential is not None else None,
|
|
46
|
+
"credential_live_format": live_format,
|
|
47
|
+
"keyring_available": keyring_available,
|
|
48
|
+
"legacy_state_present": legacy_present,
|
|
49
|
+
}
|
|
50
|
+
credential_status = (
|
|
51
|
+
f"present ({credential.source.value})" if credential is not None else "not found"
|
|
52
|
+
)
|
|
53
|
+
if live_format is False:
|
|
54
|
+
credential_status += " - not a live key, the backend will ignore it"
|
|
55
|
+
human = "\n".join(
|
|
56
|
+
(
|
|
57
|
+
f"Profile: {config.profile}",
|
|
58
|
+
f"API URL: {config.api_url}",
|
|
59
|
+
f"Credentials: {credential_status}",
|
|
60
|
+
f"OS keyring: {'available' if keyring_available else 'unavailable'}",
|
|
61
|
+
f"Legacy state: {'present' if legacy_present else 'not found'}",
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
context.output().result(OutputDocument(kind="doctor.local", data=data), human)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Markdown assets used by setup command help."""
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# `--browser`
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
This flag skips the GitHub CLI and opens the public repository page in the default browser instead. Use it when you prefer to click Star yourself, or when the GitHub CLI is logged in to an account you do not want to star from. With this flag, the command never invokes the GitHub CLI, so no GitHub API request is made. The repository URL is printed as well, so it still works on a machine without a browser. The resulting record reports the method as `browser` and the star as unconfirmed.
|
|
5
|
+
|
|
6
|
+
## Default
|
|
7
|
+
The flag is off by default, which tries the GitHub CLI first and falls back to the browser only when that fails. Leaving it off is the faster path for people who already use the GitHub CLI. Turning it on is the right choice in shared or borrowed environments where the logged-in GitHub account is not your own. The flag takes no value and has no environment-variable equivalent.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Loads caller-facing setup help from packaged Markdown assets."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib import resources
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SetupHelpLibrary:
|
|
9
|
+
"""Reads each setup help description from its named Markdown asset."""
|
|
10
|
+
|
|
11
|
+
def __init__(self) -> None:
|
|
12
|
+
self._cache: dict[str, str] = {}
|
|
13
|
+
|
|
14
|
+
def load(self, name: str) -> str:
|
|
15
|
+
if name not in self._cache:
|
|
16
|
+
asset = resources.files(__package__).joinpath(f"{name}.md")
|
|
17
|
+
self._cache[name] = asset.read_text(encoding="utf-8").strip()
|
|
18
|
+
return self._cache[name]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Star the Vidbyte CLI
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
This command stars the public Vidbyte CLI repository on GitHub for the person running it. It exists so a user who finds the CLI useful can show support without leaving the terminal. It never runs on its own: no other command, hint, or error path invokes it. An agent driving this CLI must never run it on a user's behalf, because starring is a personal endorsement and GitHub forbids inauthentic interactions. Run it only when the user has asked to star the project.
|
|
5
|
+
|
|
6
|
+
## How it stars
|
|
7
|
+
When the GitHub CLI (`gh`) is installed and logged in, the command asks it to star the repository through the GitHub API. The GitHub CLI owns the login, so this package never sees, stores, or prompts for a GitHub token. Starring is idempotent, so running the command again on an already-starred repository succeeds without any change. If the GitHub CLI is missing, logged out, or fails, the command opens the repository page in the default browser instead. The repository URL is always printed so it can be opened by hand when no browser is available.
|
|
8
|
+
|
|
9
|
+
## Output
|
|
10
|
+
On success, the human result names the starred repository, and machine formats emit one `setup.star` record on stdout. That record carries the repository, its URL, the method used (`gh` or `browser`), and whether the star was confirmed. A star made through the browser cannot be confirmed by the CLI, so that record reports it as unconfirmed. When the GitHub CLI path fails, the reason is written to stderr as a diagnostic before the browser fallback runs. Neither path changes local CLI configuration or Vidbyte credentials.
|
|
11
|
+
|
|
12
|
+
## Examples
|
|
13
|
+
Run `vidbyte-cli star` to star through the GitHub CLI when it is logged in. Run `vidbyte-cli star --browser` to skip the GitHub CLI and click Star on the repository page yourself. Run `gh auth login` first if you want the terminal path and the GitHub CLI is not yet logged in. Removing a star is done on the repository page, since this command only ever adds one.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""`vidbyte-cli star` — stars the public Vidbyte CLI repository on GitHub.
|
|
2
|
+
|
|
3
|
+
It runs only because a person typed it: starring on someone's behalf breaks GitHub's rules on
|
|
4
|
+
inauthentic interactions, so no other command, hint, or error path may call it. The GitHub CLI
|
|
5
|
+
owns authentication when it is present, so this package never holds a GitHub token; without
|
|
6
|
+
it, the repository page opens and the person clicks Star themselves.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
import click
|
|
18
|
+
from pydantic import JsonValue
|
|
19
|
+
|
|
20
|
+
from ...lib.output import OutputDocument
|
|
21
|
+
from .prompts.library import SetupHelpLibrary
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from ...lib.runtime.context import ApplicationContext
|
|
25
|
+
|
|
26
|
+
# The public front-door repository, not the private source repository. It must name the same
|
|
27
|
+
# repository as `Repository` under [project.urls]; scripts/test-star-command.py enforces that.
|
|
28
|
+
REPOSITORY = "cerredz/vidbyte-cli"
|
|
29
|
+
REPOSITORY_URL = f"https://github.com/{REPOSITORY}"
|
|
30
|
+
_GH_TIMEOUT_SECONDS = 30
|
|
31
|
+
|
|
32
|
+
_STAR_HELP = SetupHelpLibrary().load("star")
|
|
33
|
+
_BROWSER_HELP = SetupHelpLibrary().load("browser")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True, slots=True)
|
|
37
|
+
class GhAttempt:
|
|
38
|
+
"""Whether the GitHub CLI starred the repository, and why not when it did not."""
|
|
39
|
+
|
|
40
|
+
starred: bool
|
|
41
|
+
detail: str
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class GitHubCliStarrer:
|
|
45
|
+
"""Stars a repository through the GitHub CLI's own login."""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
which: Callable[[str], str | None] = shutil.which,
|
|
50
|
+
run: Callable[..., subprocess.CompletedProcess[str]] = subprocess.run,
|
|
51
|
+
) -> None:
|
|
52
|
+
self._which = which
|
|
53
|
+
self._run = run
|
|
54
|
+
|
|
55
|
+
def star(self, repository: str) -> GhAttempt:
|
|
56
|
+
executable = self._which("gh")
|
|
57
|
+
if executable is None:
|
|
58
|
+
return GhAttempt(starred=False, detail="the GitHub CLI (gh) is not installed")
|
|
59
|
+
# stdin is closed so a logged-out gh fails fast instead of waiting on a prompt.
|
|
60
|
+
command = [executable, "api", "--method", "PUT", f"/user/starred/{repository}", "--silent"]
|
|
61
|
+
try:
|
|
62
|
+
completed = self._run(
|
|
63
|
+
command,
|
|
64
|
+
stdin=subprocess.DEVNULL,
|
|
65
|
+
capture_output=True,
|
|
66
|
+
text=True,
|
|
67
|
+
check=False,
|
|
68
|
+
timeout=_GH_TIMEOUT_SECONDS,
|
|
69
|
+
)
|
|
70
|
+
except (OSError, subprocess.TimeoutExpired) as error:
|
|
71
|
+
return GhAttempt(starred=False, detail=f"the GitHub CLI could not run: {error}")
|
|
72
|
+
if completed.returncode == 0:
|
|
73
|
+
return GhAttempt(starred=True, detail="starred through the GitHub CLI")
|
|
74
|
+
reason = (completed.stderr or "").strip().splitlines()
|
|
75
|
+
detail = reason[-1] if reason else f"gh exited with status {completed.returncode}"
|
|
76
|
+
return GhAttempt(starred=False, detail=detail)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class StarCommand:
|
|
80
|
+
"""Registers the one command that stars the public repository, and only on request."""
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
starrer: GitHubCliStarrer | None = None,
|
|
85
|
+
launch: Callable[[str], int] = click.launch,
|
|
86
|
+
) -> None:
|
|
87
|
+
self._starrer = starrer or GitHubCliStarrer()
|
|
88
|
+
self._launch = launch
|
|
89
|
+
|
|
90
|
+
def register(self, parent: click.Group) -> None:
|
|
91
|
+
@parent.command(name="star", help=_STAR_HELP)
|
|
92
|
+
@click.option("--browser", is_flag=True, help=_BROWSER_HELP)
|
|
93
|
+
@click.pass_obj
|
|
94
|
+
def _run(context: ApplicationContext, browser: bool) -> None:
|
|
95
|
+
self.execute(context, browser)
|
|
96
|
+
|
|
97
|
+
def execute(self, context: ApplicationContext, browser: bool) -> None:
|
|
98
|
+
output = context.output()
|
|
99
|
+
if not browser:
|
|
100
|
+
attempt = self._starrer.star(REPOSITORY)
|
|
101
|
+
if attempt.starred:
|
|
102
|
+
output.result(
|
|
103
|
+
self._document("gh", starred=True),
|
|
104
|
+
f"Starred {REPOSITORY} on GitHub. Thank you!",
|
|
105
|
+
)
|
|
106
|
+
return
|
|
107
|
+
output.diagnostic(f"Could not star through the GitHub CLI: {attempt.detail}.")
|
|
108
|
+
opened = self._open_browser()
|
|
109
|
+
action = "Opened" if opened else "Open"
|
|
110
|
+
output.result(
|
|
111
|
+
self._document("browser", starred=False, browser_opened=opened),
|
|
112
|
+
f"{action} {REPOSITORY_URL} and click Star at the top right. Thank you!",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def _open_browser(self) -> bool:
|
|
116
|
+
# A headless machine has no browser; the printed URL is the fallback, not an error.
|
|
117
|
+
try:
|
|
118
|
+
return self._launch(REPOSITORY_URL) == 0
|
|
119
|
+
except OSError:
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def _document(method: str, starred: bool, browser_opened: bool | None = None) -> OutputDocument:
|
|
124
|
+
data: dict[str, JsonValue] = {
|
|
125
|
+
"repository": REPOSITORY,
|
|
126
|
+
"url": REPOSITORY_URL,
|
|
127
|
+
"method": method,
|
|
128
|
+
"starred": starred,
|
|
129
|
+
}
|
|
130
|
+
if browser_opened is not None:
|
|
131
|
+
data["browser_opened"] = browser_opened
|
|
132
|
+
return OutputDocument(kind="setup.star", data=data)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Typed HTTP client for the Vidbyte public API.
|
|
2
|
+
|
|
3
|
+
Owns the base URL, API-key header injection, bounded response decoding, and status-driven
|
|
4
|
+
failure classification. Commands never call httpx directly — all HTTP goes through a typed
|
|
5
|
+
endpoint group built on this client.
|
|
6
|
+
|
|
7
|
+
Settings and the credential arrive already resolved, so this client never reads the
|
|
8
|
+
environment: doing so would let a stale variable outrank an explicit option and would skip
|
|
9
|
+
the origin validation that `ResolvedConfig.api_url` has already passed.
|
|
10
|
+
|
|
11
|
+
One request is serialized once and re-issued byte-identically under the same idempotency
|
|
12
|
+
key, because every retry of a priced mutation must be the same mutation.
|
|
13
|
+
Classification is by HTTP status only. The backend serves several different error-body
|
|
14
|
+
shapes, so no single `code` field is a platform contract, and a generic client that knew one
|
|
15
|
+
route's spelling would be wrong for the next one.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import time
|
|
21
|
+
from typing import TYPE_CHECKING, TypeVar
|
|
22
|
+
|
|
23
|
+
from pydantic import BaseModel
|
|
24
|
+
|
|
25
|
+
from ..auth.credentials import Credentials
|
|
26
|
+
from ..config import ResolvedConfig
|
|
27
|
+
from ..errors.failures import ApiRouteMisconfigured
|
|
28
|
+
from ..runtime.version import current_version
|
|
29
|
+
from .response import ResponseDecoder, ResponseShape
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
import httpx
|
|
33
|
+
|
|
34
|
+
from .retry import RequestOutcome
|
|
35
|
+
from .runtime_payment import RuntimePayment
|
|
36
|
+
|
|
37
|
+
# The backend reads this header first and accepts `Authorization: Bearer` only as a fallback,
|
|
38
|
+
# so the key travels here and nowhere else. Sending both would be worse than sending one: the
|
|
39
|
+
# gatekeeper stops at a malformed `x-api-key` instead of falling through to the bearer value.
|
|
40
|
+
API_KEY_HEADER_NAME = "x-api-key"
|
|
41
|
+
_MAX_CONNECTIONS = 20
|
|
42
|
+
_MAX_KEEPALIVE_CONNECTIONS = 10
|
|
43
|
+
|
|
44
|
+
TModel = TypeVar("TModel", bound=BaseModel)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ApiClient:
|
|
48
|
+
def __init__(self, config: ResolvedConfig, credentials: Credentials) -> None:
|
|
49
|
+
# Takes the invocation's resolved host, timeout, and secret; resolves nothing itself.
|
|
50
|
+
self.base_url = config.api_url.rstrip("/")
|
|
51
|
+
self.timeout_seconds = config.request_timeout_seconds
|
|
52
|
+
# The key is a secret: it is held here only to set the auth header, never logged.
|
|
53
|
+
self._credentials = credentials
|
|
54
|
+
# Built on first send, so constructing a client opens no socket and `--help` is free.
|
|
55
|
+
self._transport: httpx.Client | None = None
|
|
56
|
+
from .problem import ApiProblemMapper
|
|
57
|
+
from .retry import RetryPolicy
|
|
58
|
+
|
|
59
|
+
self._retry = RetryPolicy()
|
|
60
|
+
self._decoder = ResponseDecoder()
|
|
61
|
+
self._problems = ApiProblemMapper()
|
|
62
|
+
|
|
63
|
+
def auth_headers(self) -> dict[str, str]:
|
|
64
|
+
# The single point where the stored secret is unwrapped for transmission.
|
|
65
|
+
return {API_KEY_HEADER_NAME: self._credentials.secret_value()}
|
|
66
|
+
|
|
67
|
+
def get(
|
|
68
|
+
self,
|
|
69
|
+
path: str,
|
|
70
|
+
model: type[TModel],
|
|
71
|
+
*,
|
|
72
|
+
shape: ResponseShape = ResponseShape.ENVELOPE,
|
|
73
|
+
) -> TModel:
|
|
74
|
+
# Performs an authenticated GET, unwraps the declared shape, validates into `model`.
|
|
75
|
+
return self.request("GET", path, response_model=model, response_shape=shape)
|
|
76
|
+
|
|
77
|
+
def post_direct(self, path: str, model: type[TModel]) -> TModel:
|
|
78
|
+
# POSTs with no body and validates the response object itself, not an envelope's `data`.
|
|
79
|
+
response = self._send("POST", path, None, None, route_not_found=True)
|
|
80
|
+
return self._decoder.one(response, model, ResponseShape.DIRECT)
|
|
81
|
+
|
|
82
|
+
def post(
|
|
83
|
+
self,
|
|
84
|
+
path: str,
|
|
85
|
+
body: BaseModel,
|
|
86
|
+
model: type[TModel],
|
|
87
|
+
*,
|
|
88
|
+
shape: ResponseShape = ResponseShape.ENVELOPE,
|
|
89
|
+
idempotency_key: str | None = None,
|
|
90
|
+
) -> TModel:
|
|
91
|
+
# Performs an authenticated POST, unwraps the declared shape, validates into `model`.
|
|
92
|
+
return self.request(
|
|
93
|
+
"POST",
|
|
94
|
+
path,
|
|
95
|
+
response_model=model,
|
|
96
|
+
response_shape=shape,
|
|
97
|
+
body=body,
|
|
98
|
+
idempotency_key=idempotency_key,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def request(
|
|
102
|
+
self,
|
|
103
|
+
method: str,
|
|
104
|
+
path: str,
|
|
105
|
+
*,
|
|
106
|
+
response_model: type[TModel],
|
|
107
|
+
response_shape: ResponseShape,
|
|
108
|
+
body: BaseModel | None = None,
|
|
109
|
+
idempotency_key: str | None = None,
|
|
110
|
+
) -> TModel:
|
|
111
|
+
# The one path every typed request takes: send with retries, then decode.
|
|
112
|
+
response = self._send(method, path, body, idempotency_key)
|
|
113
|
+
return self._decoder.one(response, response_model, response_shape)
|
|
114
|
+
|
|
115
|
+
def post_runtime_payment(
|
|
116
|
+
self, path: str, body: BaseModel, key: str, payer: RuntimePayment
|
|
117
|
+
) -> httpx.Response:
|
|
118
|
+
# Probe or recover first, then authorize at most one exact x402 payment.
|
|
119
|
+
import httpx
|
|
120
|
+
|
|
121
|
+
from ..errors.failures import RuntimePaymentFailed
|
|
122
|
+
|
|
123
|
+
url = self._url(path)
|
|
124
|
+
content = self._content(body)
|
|
125
|
+
headers = self._headers(key, True)
|
|
126
|
+
outcome = self._retry_request("POST", url, content, headers)
|
|
127
|
+
if isinstance(outcome, httpx.Response) and outcome.status_code == 402:
|
|
128
|
+
headers.update(payer.headers(outcome))
|
|
129
|
+
outcome = self._retry_request("POST", url, content, headers)
|
|
130
|
+
if isinstance(outcome, httpx.Response) and outcome.status_code == 402:
|
|
131
|
+
raise RuntimePaymentFailed("payment_rejected")
|
|
132
|
+
return self._settle(outcome)
|
|
133
|
+
|
|
134
|
+
def close(self) -> None:
|
|
135
|
+
# Releases the connection pool; safe to call when no request was ever made.
|
|
136
|
+
if self._transport is not None:
|
|
137
|
+
self._transport.close()
|
|
138
|
+
self._transport = None
|
|
139
|
+
|
|
140
|
+
def _send(
|
|
141
|
+
self,
|
|
142
|
+
method: str,
|
|
143
|
+
path: str,
|
|
144
|
+
body: BaseModel | None,
|
|
145
|
+
idempotency_key: str | None,
|
|
146
|
+
*,
|
|
147
|
+
route_not_found: bool = False,
|
|
148
|
+
) -> httpx.Response:
|
|
149
|
+
# Re-issues one identical request while the retry policy allows, then classifies.
|
|
150
|
+
url = self._url(path)
|
|
151
|
+
content = self._content(body)
|
|
152
|
+
headers = self._headers(idempotency_key, body is not None)
|
|
153
|
+
outcome = self._retry_request(method, url, content, headers)
|
|
154
|
+
return self._settle(outcome, route_not_found=route_not_found)
|
|
155
|
+
|
|
156
|
+
def _retry_request(
|
|
157
|
+
self, method: str, url: str, data: bytes | None, headers: dict[str, str]
|
|
158
|
+
) -> RequestOutcome:
|
|
159
|
+
# Reuse identical signed bytes and request identity throughout bounded retries.
|
|
160
|
+
from .retry import RequestMetadata
|
|
161
|
+
|
|
162
|
+
metadata = RequestMetadata(method.upper(), "Idempotency-Key" in headers)
|
|
163
|
+
attempt = 1
|
|
164
|
+
while True:
|
|
165
|
+
outcome = self._attempt(method, url, data, headers)
|
|
166
|
+
decision = self._retry.decide(metadata, attempt, outcome)
|
|
167
|
+
if not decision.retry:
|
|
168
|
+
return outcome
|
|
169
|
+
time.sleep(decision.delay_seconds)
|
|
170
|
+
attempt += 1
|
|
171
|
+
|
|
172
|
+
def _attempt(
|
|
173
|
+
self,
|
|
174
|
+
method: str,
|
|
175
|
+
url: str,
|
|
176
|
+
content: bytes | None,
|
|
177
|
+
headers: dict[str, str],
|
|
178
|
+
) -> RequestOutcome:
|
|
179
|
+
# One attempt is either a reply or a transport failure, never both and never neither.
|
|
180
|
+
import httpx
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
return self._client().request(method, url, content=content, headers=headers)
|
|
184
|
+
except httpx.HTTPError as error:
|
|
185
|
+
return error
|
|
186
|
+
|
|
187
|
+
def _settle(self, outcome: RequestOutcome, *, route_not_found: bool = False) -> httpx.Response:
|
|
188
|
+
# The retry policy has given up, so this outcome is the caller's answer.
|
|
189
|
+
import httpx
|
|
190
|
+
|
|
191
|
+
if isinstance(outcome, httpx.HTTPError):
|
|
192
|
+
raise self._problems.from_transport(outcome) from outcome
|
|
193
|
+
if not 200 <= outcome.status_code < 300:
|
|
194
|
+
raise self._problems.from_response(outcome, route_not_found=route_not_found)
|
|
195
|
+
return outcome
|
|
196
|
+
|
|
197
|
+
def _client(self) -> httpx.Client:
|
|
198
|
+
# Built once per invocation so a watch loop reuses one pool across many polls.
|
|
199
|
+
import httpx
|
|
200
|
+
|
|
201
|
+
if self._transport is None:
|
|
202
|
+
self._transport = httpx.Client(
|
|
203
|
+
timeout=httpx.Timeout(self.timeout_seconds),
|
|
204
|
+
follow_redirects=False,
|
|
205
|
+
limits=httpx.Limits(
|
|
206
|
+
max_connections=_MAX_CONNECTIONS,
|
|
207
|
+
max_keepalive_connections=_MAX_KEEPALIVE_CONNECTIONS,
|
|
208
|
+
),
|
|
209
|
+
)
|
|
210
|
+
return self._transport
|
|
211
|
+
|
|
212
|
+
def _content(self, body: BaseModel | None) -> bytes | None:
|
|
213
|
+
# Serialized once so every attempt is byte-identical, and `exclude_none` is what
|
|
214
|
+
# keeps unset options out of a request DTO the backend validates with extra="forbid".
|
|
215
|
+
if body is None:
|
|
216
|
+
return None
|
|
217
|
+
return body.model_dump_json(exclude_none=True, by_alias=True).encode("utf-8")
|
|
218
|
+
|
|
219
|
+
def _url(self, path: str) -> str:
|
|
220
|
+
# A route carrying its own scheme or host would send the key to an unconfigured origin.
|
|
221
|
+
if not path.startswith("/") or path.startswith("//") or "://" in path:
|
|
222
|
+
raise ApiRouteMisconfigured()
|
|
223
|
+
return f"{self.base_url}{path}"
|
|
224
|
+
|
|
225
|
+
def _headers(self, idempotency_key: str | None, has_body: bool) -> dict[str, str]:
|
|
226
|
+
# Identity, content negotiation, and the credential; nothing derived from user input.
|
|
227
|
+
headers = {
|
|
228
|
+
"Accept": "application/json",
|
|
229
|
+
"User-Agent": f"vidbyte-cli/{current_version()}",
|
|
230
|
+
**self.auth_headers(),
|
|
231
|
+
}
|
|
232
|
+
if has_body:
|
|
233
|
+
headers["Content-Type"] = "application/json"
|
|
234
|
+
if idempotency_key is not None:
|
|
235
|
+
headers["Idempotency-Key"] = idempotency_key
|
|
236
|
+
return headers
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""The one authentication route the CLI calls, and the identity it returns.
|
|
2
|
+
|
|
3
|
+
`validate` is the only permission-free liveness check the backend serves. It is chosen over a
|
|
4
|
+
cheap authenticated resource read because the API grants read and write scopes separately: a
|
|
5
|
+
key scoped only to write is perfectly valid, and a read-based check would refuse to log it in.
|
|
6
|
+
Capability belongs at the point of use, where a rejection can name the missing scope.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from ....types.api import KeyIdentity
|
|
12
|
+
from ..client import ApiClient
|
|
13
|
+
|
|
14
|
+
AUTH_VALIDATE_PATH = "/api/skills/auth/validate"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AuthEndpoints:
|
|
18
|
+
def __init__(self, client: ApiClient) -> None:
|
|
19
|
+
# Binds the endpoint group to a configured ApiClient.
|
|
20
|
+
self._client = client
|
|
21
|
+
|
|
22
|
+
def validate(self) -> KeyIdentity:
|
|
23
|
+
# Proves the configured key is live and returns its non-secret identity.
|
|
24
|
+
return self._client.post_direct(AUTH_VALIDATE_PATH, KeyIdentity)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Authenticated billing operations exposed by the Vidbyte API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
from uuid import uuid4
|
|
7
|
+
|
|
8
|
+
from ....types.api import BillingTopUpResult
|
|
9
|
+
from ....types.runtime import RuntimeAdmissionRequest, RuntimeHost
|
|
10
|
+
from ..client import ApiClient
|
|
11
|
+
from ..response import ResponseDecoder, ResponseShape
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from ..runtime_payment import RuntimePayment
|
|
15
|
+
|
|
16
|
+
TOP_UP_PATH = "/agent/topup"
|
|
17
|
+
TOP_UP_CENTS = 500
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BillingEndpoints:
|
|
21
|
+
"""Billing operations bound to one authenticated API client."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, client: ApiClient) -> None:
|
|
24
|
+
# Retains the invocation-owned client without opening a connection.
|
|
25
|
+
self._client = client
|
|
26
|
+
|
|
27
|
+
def top_up(self, key: str | None, payer: RuntimePayment) -> BillingTopUpResult:
|
|
28
|
+
# Pays one exact top-up challenge and validates the credited balance response.
|
|
29
|
+
idempotency_key = key or str(uuid4())
|
|
30
|
+
response = self._client.post_runtime_payment(
|
|
31
|
+
TOP_UP_PATH,
|
|
32
|
+
RuntimeAdmissionRequest(host=RuntimeHost.CODEX),
|
|
33
|
+
idempotency_key,
|
|
34
|
+
payer,
|
|
35
|
+
)
|
|
36
|
+
return ResponseDecoder().one(response, BillingTopUpResult, ResponseShape.DIRECT)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Typed wrappers for the /api/v1/research/* public API routes.
|
|
2
|
+
|
|
3
|
+
These six are the entire surface an API key may call. There is no sources, artifacts,
|
|
4
|
+
capabilities, exports, or run-listing route, so there is no method here for one.
|
|
5
|
+
|
|
6
|
+
Every route returns its DTO directly rather than inside the `{success, data}` envelope the
|
|
7
|
+
older public resources use, which is why each call names `ResponseShape.DIRECT` explicitly.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from urllib.parse import quote, urlencode
|
|
13
|
+
|
|
14
|
+
from ....types.research import (
|
|
15
|
+
ResearchPortfolioPage,
|
|
16
|
+
ResearchRunAccepted,
|
|
17
|
+
ResearchRunCreateRequest,
|
|
18
|
+
ResearchRunStatus,
|
|
19
|
+
ResearchThreadDetail,
|
|
20
|
+
)
|
|
21
|
+
from ..client import ApiClient
|
|
22
|
+
from ..response import ResponseShape
|
|
23
|
+
|
|
24
|
+
_RESEARCH_ROOT = "/api/v1/research"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ResearchEndpoints:
|
|
28
|
+
def __init__(self, client: ApiClient) -> None:
|
|
29
|
+
# Binds the endpoint group to a configured ApiClient.
|
|
30
|
+
self._client = client
|
|
31
|
+
|
|
32
|
+
def create_run(
|
|
33
|
+
self,
|
|
34
|
+
request: ResearchRunCreateRequest,
|
|
35
|
+
idempotency_key: str,
|
|
36
|
+
) -> ResearchRunAccepted:
|
|
37
|
+
# POST /api/v1/research/run — opens a new thread and admits its first run.
|
|
38
|
+
return self._client.post(
|
|
39
|
+
f"{_RESEARCH_ROOT}/run",
|
|
40
|
+
request,
|
|
41
|
+
ResearchRunAccepted,
|
|
42
|
+
shape=ResponseShape.DIRECT,
|
|
43
|
+
idempotency_key=idempotency_key,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def append_run(
|
|
47
|
+
self,
|
|
48
|
+
thread_id: str,
|
|
49
|
+
request: ResearchRunCreateRequest,
|
|
50
|
+
idempotency_key: str,
|
|
51
|
+
) -> ResearchRunAccepted:
|
|
52
|
+
# POST /api/v1/research/threads/{id}/run — admits another prompt into one thread.
|
|
53
|
+
return self._client.post(
|
|
54
|
+
f"{_RESEARCH_ROOT}/threads/{self._segment(thread_id)}/run",
|
|
55
|
+
request,
|
|
56
|
+
ResearchRunAccepted,
|
|
57
|
+
shape=ResponseShape.DIRECT,
|
|
58
|
+
idempotency_key=idempotency_key,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def continue_run(self, run_id: str, idempotency_key: str) -> ResearchRunAccepted:
|
|
62
|
+
# POST /api/v1/research/runs/{id}/continue — reopens one terminal-resumable run.
|
|
63
|
+
# The route takes no body: a continuation replays the original prompt, never a new one.
|
|
64
|
+
return self._client.request(
|
|
65
|
+
"POST",
|
|
66
|
+
f"{_RESEARCH_ROOT}/runs/{self._segment(run_id)}/continue",
|
|
67
|
+
response_model=ResearchRunAccepted,
|
|
68
|
+
response_shape=ResponseShape.DIRECT,
|
|
69
|
+
idempotency_key=idempotency_key,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def get_run(self, run_id: str) -> ResearchRunStatus:
|
|
73
|
+
# GET /api/v1/research/runs/{id} — one run's coarse durable status.
|
|
74
|
+
return self._client.get(
|
|
75
|
+
f"{_RESEARCH_ROOT}/runs/{self._segment(run_id)}",
|
|
76
|
+
ResearchRunStatus,
|
|
77
|
+
shape=ResponseShape.DIRECT,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def get_portfolio(self, cursor: str | None, limit: int | None) -> ResearchPortfolioPage:
|
|
81
|
+
# GET /api/v1/research/portfolio — one bounded page of the caller's threads.
|
|
82
|
+
return self._client.get(
|
|
83
|
+
self._paged(f"{_RESEARCH_ROOT}/portfolio", cursor, limit),
|
|
84
|
+
ResearchPortfolioPage,
|
|
85
|
+
shape=ResponseShape.DIRECT,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def get_thread(self, thread_id: str) -> ResearchThreadDetail:
|
|
89
|
+
# GET /api/v1/research/threads/{id} — one owned thread and its rollup counters.
|
|
90
|
+
return self._client.get(
|
|
91
|
+
f"{_RESEARCH_ROOT}/threads/{self._segment(thread_id)}",
|
|
92
|
+
ResearchThreadDetail,
|
|
93
|
+
shape=ResponseShape.DIRECT,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def _segment(self, value: str) -> str:
|
|
97
|
+
# Encoding an opaque identifier means it can never alter the route's structure.
|
|
98
|
+
return quote(value, safe="")
|
|
99
|
+
|
|
100
|
+
def _paged(self, path: str, cursor: str | None, limit: int | None) -> str:
|
|
101
|
+
# Unset parameters are omitted so the server keeps ownership of both defaults.
|
|
102
|
+
query = {name: value for name, value in (("cursor", cursor), ("limit", limit)) if value}
|
|
103
|
+
return f"{path}?{urlencode(query)}" if query else path
|