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.
Files changed (269) hide show
  1. vidbyte_cli/__init__.py +11 -0
  2. vidbyte_cli/__main__.py +6 -0
  3. vidbyte_cli/cli.py +18 -0
  4. vidbyte_cli/commands/__init__.py +86 -0
  5. vidbyte_cli/commands/agent_options.py +53 -0
  6. vidbyte_cli/commands/agents/__init__.py +31 -0
  7. vidbyte_cli/commands/agents/suggestion/__init__.py +38 -0
  8. vidbyte_cli/commands/agents/suggestion/prompts/__init__.py +3 -0
  9. vidbyte_cli/commands/agents/suggestion/prompts/all_categories.md +5 -0
  10. vidbyte_cli/commands/agents/suggestion/prompts/approaches.md +100 -0
  11. vidbyte_cli/commands/agents/suggestion/prompts/avoid.md +100 -0
  12. vidbyte_cli/commands/agents/suggestion/prompts/blockers.md +100 -0
  13. vidbyte_cli/commands/agents/suggestion/prompts/capability.md +100 -0
  14. vidbyte_cli/commands/agents/suggestion/prompts/categories.md +5 -0
  15. vidbyte_cli/commands/agents/suggestion/prompts/category.md +5 -0
  16. vidbyte_cli/commands/agents/suggestion/prompts/completed.md +100 -0
  17. vidbyte_cli/commands/agents/suggestion/prompts/constraint.md +100 -0
  18. vidbyte_cli/commands/agents/suggestion/prompts/context.md +100 -0
  19. vidbyte_cli/commands/agents/suggestion/prompts/count.md +5 -0
  20. vidbyte_cli/commands/agents/suggestion/prompts/critic_model.md +100 -0
  21. vidbyte_cli/commands/agents/suggestion/prompts/decision.md +100 -0
  22. vidbyte_cli/commands/agents/suggestion/prompts/dry_run.md +5 -0
  23. vidbyte_cli/commands/agents/suggestion/prompts/extra_compute.md +5 -0
  24. vidbyte_cli/commands/agents/suggestion/prompts/feedback_accept.md +7 -0
  25. vidbyte_cli/commands/agents/suggestion/prompts/feedback_group.md +7 -0
  26. vidbyte_cli/commands/agents/suggestion/prompts/feedback_project.md +100 -0
  27. vidbyte_cli/commands/agents/suggestion/prompts/feedback_reason.md +98 -0
  28. vidbyte_cli/commands/agents/suggestion/prompts/feedback_reject.md +7 -0
  29. vidbyte_cli/commands/agents/suggestion/prompts/feedback_suggestion.md +101 -0
  30. vidbyte_cli/commands/agents/suggestion/prompts/files.md +100 -0
  31. vidbyte_cli/commands/agents/suggestion/prompts/forbidden.md +100 -0
  32. vidbyte_cli/commands/agents/suggestion/prompts/goal.md +100 -0
  33. vidbyte_cli/commands/agents/suggestion/prompts/handoff.md +7 -0
  34. vidbyte_cli/commands/agents/suggestion/prompts/horizon.md +5 -0
  35. vidbyte_cli/commands/agents/suggestion/prompts/hypotheses.md +100 -0
  36. vidbyte_cli/commands/agents/suggestion/prompts/idea.md +100 -0
  37. vidbyte_cli/commands/agents/suggestion/prompts/in_progress.md +100 -0
  38. vidbyte_cli/commands/agents/suggestion/prompts/input.md +100 -0
  39. vidbyte_cli/commands/agents/suggestion/prompts/library.py +29 -0
  40. vidbyte_cli/commands/agents/suggestion/prompts/max_messages.md +5 -0
  41. vidbyte_cli/commands/agents/suggestion/prompts/max_output_tokens.md +5 -0
  42. vidbyte_cli/commands/agents/suggestion/prompts/max_total_tokens.md +5 -0
  43. vidbyte_cli/commands/agents/suggestion/prompts/mistakes.md +100 -0
  44. vidbyte_cli/commands/agents/suggestion/prompts/outcomes.md +100 -0
  45. vidbyte_cli/commands/agents/suggestion/prompts/project.md +100 -0
  46. vidbyte_cli/commands/agents/suggestion/prompts/project_create.md +7 -0
  47. vidbyte_cli/commands/agents/suggestion/prompts/project_description.md +98 -0
  48. vidbyte_cli/commands/agents/suggestion/prompts/project_group.md +7 -0
  49. vidbyte_cli/commands/agents/suggestion/prompts/project_key.md +96 -0
  50. vidbyte_cli/commands/agents/suggestion/prompts/project_list.md +7 -0
  51. vidbyte_cli/commands/agents/suggestion/prompts/project_title.md +99 -0
  52. vidbyte_cli/commands/agents/suggestion/prompts/provider.md +5 -0
  53. vidbyte_cli/commands/agents/suggestion/prompts/question.md +100 -0
  54. vidbyte_cli/commands/agents/suggestion/prompts/risks.md +100 -0
  55. vidbyte_cli/commands/agents/suggestion/prompts/rounds.md +5 -0
  56. vidbyte_cli/commands/agents/suggestion/prompts/run.md +5 -0
  57. vidbyte_cli/commands/agents/suggestion/prompts/success.md +100 -0
  58. vidbyte_cli/commands/agents/suggestion/prompts/timeout.md +5 -0
  59. vidbyte_cli/commands/agents/suggestion/prompts/trajectory.md +100 -0
  60. vidbyte_cli/commands/agents/suggestion/prompts/view.md +5 -0
  61. vidbyte_cli/commands/agents/suggestion/prompts/view_all.md +5 -0
  62. vidbyte_cli/commands/agents/suggestion/render.py +77 -0
  63. vidbyte_cli/commands/agents/suggestion/request_builder.py +240 -0
  64. vidbyte_cli/commands/agents/suggestion/suggest.py +154 -0
  65. vidbyte_cli/commands/agents/suggestion/suggestion_categories.py +77 -0
  66. vidbyte_cli/commands/agents/suggestion/suggestion_feedback.py +85 -0
  67. vidbyte_cli/commands/agents/suggestion/suggestion_handoff.py +67 -0
  68. vidbyte_cli/commands/agents/suggestion/suggestion_projects.py +95 -0
  69. vidbyte_cli/commands/auth/__init__.py +0 -0
  70. vidbyte_cli/commands/auth/login.py +100 -0
  71. vidbyte_cli/commands/auth/logout.py +41 -0
  72. vidbyte_cli/commands/auth/whoami.py +51 -0
  73. vidbyte_cli/commands/billing/__init__.py +20 -0
  74. vidbyte_cli/commands/billing/prompts/__init__.py +1 -0
  75. vidbyte_cli/commands/billing/prompts/confirm.md +16 -0
  76. vidbyte_cli/commands/billing/prompts/group.md +13 -0
  77. vidbyte_cli/commands/billing/prompts/idempotency_key.md +16 -0
  78. vidbyte_cli/commands/billing/prompts/library.py +18 -0
  79. vidbyte_cli/commands/billing/prompts/top_up.md +16 -0
  80. vidbyte_cli/commands/billing/top_up.py +65 -0
  81. vidbyte_cli/commands/config/__init__.py +0 -0
  82. vidbyte_cli/commands/config/get.py +45 -0
  83. vidbyte_cli/commands/config/set.py +39 -0
  84. vidbyte_cli/commands/provider/__init__.py +9 -0
  85. vidbyte_cli/commands/provider/login.py +102 -0
  86. vidbyte_cli/commands/provider/logout.py +45 -0
  87. vidbyte_cli/commands/provider/whoami.py +49 -0
  88. vidbyte_cli/commands/research/__init__.py +5 -0
  89. vidbyte_cli/commands/research/add.py +41 -0
  90. vidbyte_cli/commands/research/options.py +134 -0
  91. vidbyte_cli/commands/research/render.py +157 -0
  92. vidbyte_cli/commands/research/resume.py +37 -0
  93. vidbyte_cli/commands/research/start.py +37 -0
  94. vidbyte_cli/commands/research/status.py +32 -0
  95. vidbyte_cli/commands/research/thread.py +30 -0
  96. vidbyte_cli/commands/research/threads.py +35 -0
  97. vidbyte_cli/commands/research/watch.py +93 -0
  98. vidbyte_cli/commands/runtime/__init__.py +23 -0
  99. vidbyte_cli/commands/runtime/adversarial_team.py +41 -0
  100. vidbyte_cli/commands/runtime/doctor.py +44 -0
  101. vidbyte_cli/commands/runtime/list.py +47 -0
  102. vidbyte_cli/commands/runtime/persistence.py +141 -0
  103. vidbyte_cli/commands/runtime/same_host_ensemble.py +221 -0
  104. vidbyte_cli/commands/runtime/stages.py +553 -0
  105. vidbyte_cli/commands/runtime/task_board.py +1342 -0
  106. vidbyte_cli/commands/setup/__init__.py +0 -0
  107. vidbyte_cli/commands/setup/doctor.py +64 -0
  108. vidbyte_cli/commands/setup/prompts/__init__.py +1 -0
  109. vidbyte_cli/commands/setup/prompts/browser.md +7 -0
  110. vidbyte_cli/commands/setup/prompts/library.py +18 -0
  111. vidbyte_cli/commands/setup/prompts/star.md +13 -0
  112. vidbyte_cli/commands/setup/star.py +132 -0
  113. vidbyte_cli/lib/__init__.py +0 -0
  114. vidbyte_cli/lib/api/__init__.py +0 -0
  115. vidbyte_cli/lib/api/client.py +236 -0
  116. vidbyte_cli/lib/api/endpoints/__init__.py +0 -0
  117. vidbyte_cli/lib/api/endpoints/auth.py +24 -0
  118. vidbyte_cli/lib/api/endpoints/billing.py +36 -0
  119. vidbyte_cli/lib/api/endpoints/research.py +103 -0
  120. vidbyte_cli/lib/api/endpoints/runtime.py +113 -0
  121. vidbyte_cli/lib/api/problem.py +86 -0
  122. vidbyte_cli/lib/api/response.py +101 -0
  123. vidbyte_cli/lib/api/retry.py +116 -0
  124. vidbyte_cli/lib/api/runtime_payment.py +89 -0
  125. vidbyte_cli/lib/auth/__init__.py +16 -0
  126. vidbyte_cli/lib/auth/credentials.py +223 -0
  127. vidbyte_cli/lib/auth/input.py +59 -0
  128. vidbyte_cli/lib/auth/keyring_store.py +126 -0
  129. vidbyte_cli/lib/auth/provider_credentials.py +157 -0
  130. vidbyte_cli/lib/auth/provider_input.py +54 -0
  131. vidbyte_cli/lib/auth/provider_resolver.py +89 -0
  132. vidbyte_cli/lib/auth/provider_store.py +149 -0
  133. vidbyte_cli/lib/auth/provider_verifier.py +219 -0
  134. vidbyte_cli/lib/auth/resolver.py +63 -0
  135. vidbyte_cli/lib/auth/verifier.py +38 -0
  136. vidbyte_cli/lib/config/__init__.py +25 -0
  137. vidbyte_cli/lib/config/atomic.py +47 -0
  138. vidbyte_cli/lib/config/config.py +141 -0
  139. vidbyte_cli/lib/config/migration.py +81 -0
  140. vidbyte_cli/lib/config/models.py +145 -0
  141. vidbyte_cli/lib/config/paths.py +66 -0
  142. vidbyte_cli/lib/config/resolver.py +137 -0
  143. vidbyte_cli/lib/constants/__init__.py +1 -0
  144. vidbyte_cli/lib/constants/runtime.py +186 -0
  145. vidbyte_cli/lib/errors/__init__.py +12 -0
  146. vidbyte_cli/lib/errors/cli_error.py +86 -0
  147. vidbyte_cli/lib/errors/codes.py +43 -0
  148. vidbyte_cli/lib/errors/failures.py +2910 -0
  149. vidbyte_cli/lib/errors/handler.py +72 -0
  150. vidbyte_cli/lib/files/__init__.py +6 -0
  151. vidbyte_cli/lib/files/documents.py +94 -0
  152. vidbyte_cli/lib/files/store.py +156 -0
  153. vidbyte_cli/lib/io/__init__.py +16 -0
  154. vidbyte_cli/lib/io/attachments.py +129 -0
  155. vidbyte_cli/lib/io/codex_attachments.py +47 -0
  156. vidbyte_cli/lib/io/prompt.py +71 -0
  157. vidbyte_cli/lib/io/streams.py +37 -0
  158. vidbyte_cli/lib/io/terminal.py +66 -0
  159. vidbyte_cli/lib/output/__init__.py +7 -0
  160. vidbyte_cli/lib/output/formats.py +29 -0
  161. vidbyte_cli/lib/output/manager.py +95 -0
  162. vidbyte_cli/lib/output/models.py +49 -0
  163. vidbyte_cli/lib/runtime/__init__.py +9 -0
  164. vidbyte_cli/lib/runtime/application.py +134 -0
  165. vidbyte_cli/lib/runtime/context.py +250 -0
  166. vidbyte_cli/lib/runtime/options.py +159 -0
  167. vidbyte_cli/lib/runtime/version.py +30 -0
  168. vidbyte_cli/lib/runtime_primitives/__init__.py +28 -0
  169. vidbyte_cli/lib/runtime_primitives/executor.py +54 -0
  170. vidbyte_cli/lib/runtime_primitives/gate.py +105 -0
  171. vidbyte_cli/lib/runtime_primitives/hosts.py +45 -0
  172. vidbyte_cli/lib/runtime_primitives/planner.py +79 -0
  173. vidbyte_cli/lib/runtime_primitives/stages.py +338 -0
  174. vidbyte_cli/lib/runtime_primitives/task_board.py +982 -0
  175. vidbyte_cli/lib/runtime_primitives/task_board_checkpoints.py +473 -0
  176. vidbyte_cli/lib/runtime_primitives/task_board_system.md +33 -0
  177. vidbyte_cli/lib/runtime_primitives/task_board_tasks.py +111 -0
  178. vidbyte_cli/lib/runtime_primitives/verification.py +68 -0
  179. vidbyte_cli/services/__init__.py +5 -0
  180. vidbyte_cli/services/ensemble/__init__.py +7 -0
  181. vidbyte_cli/services/ensemble/prompts/__init__.py +6 -0
  182. vidbyte_cli/services/ensemble/prompts/implementer_system.md +546 -0
  183. vidbyte_cli/services/ensemble/prompts/implementer_turn.md +49 -0
  184. vidbyte_cli/services/ensemble/prompts/library.py +188 -0
  185. vidbyte_cli/services/ensemble/prompts/planner_system.md +728 -0
  186. vidbyte_cli/services/ensemble/prompts/planner_turn.md +6 -0
  187. vidbyte_cli/services/ensemble/prompts/role_system.md +104 -0
  188. vidbyte_cli/services/ensemble/prompts/role_turn.md +19 -0
  189. vidbyte_cli/services/ensemble/prompts/selector_final_turn.md +19 -0
  190. vidbyte_cli/services/ensemble/prompts/selector_round_turn.md +17 -0
  191. vidbyte_cli/services/ensemble/prompts/selector_system.md +89 -0
  192. vidbyte_cli/services/ensemble/runner.py +30 -0
  193. vidbyte_cli/services/ensemble/sdk.py +134 -0
  194. vidbyte_cli/services/ensemble/service.py +233 -0
  195. vidbyte_cli/services/ensemble/settings.py +60 -0
  196. vidbyte_cli/services/persistence/__init__.py +6 -0
  197. vidbyte_cli/services/persistence/prompts/__init__.py +6 -0
  198. vidbyte_cli/services/persistence/prompts/library.py +46 -0
  199. vidbyte_cli/services/persistence/prompts/persistence_system.md +1 -0
  200. vidbyte_cli/services/persistence/prompts/persistence_turn.md +3 -0
  201. vidbyte_cli/services/persistence/runner.py +35 -0
  202. vidbyte_cli/services/persistence/session.py +116 -0
  203. vidbyte_cli/services/suggestions/__init__.py +7 -0
  204. vidbyte_cli/services/suggestions/categories.py +270 -0
  205. vidbyte_cli/services/suggestions/context.py +222 -0
  206. vidbyte_cli/services/suggestions/extra_compute.py +53 -0
  207. vidbyte_cli/services/suggestions/handoff.py +173 -0
  208. vidbyte_cli/services/suggestions/message_tools.py +47 -0
  209. vidbyte_cli/services/suggestions/project.py +253 -0
  210. vidbyte_cli/services/suggestions/prompts/__init__.py +1 -0
  211. vidbyte_cli/services/suggestions/prompts/categories/__init__.py +3 -0
  212. vidbyte_cli/services/suggestions/prompts/categories/adjacent_opportunity.md +37 -0
  213. vidbyte_cli/services/suggestions/prompts/categories/alternative.md +38 -0
  214. vidbyte_cli/services/suggestions/prompts/categories/big_bets.md +37 -0
  215. vidbyte_cli/services/suggestions/prompts/categories/bottleneck.md +38 -0
  216. vidbyte_cli/services/suggestions/prompts/categories/business_growth.md +38 -0
  217. vidbyte_cli/services/suggestions/prompts/categories/completion.md +37 -0
  218. vidbyte_cli/services/suggestions/prompts/categories/continuation.md +37 -0
  219. vidbyte_cli/services/suggestions/prompts/categories/coordination.md +37 -0
  220. vidbyte_cli/services/suggestions/prompts/categories/creative_exploration.md +37 -0
  221. vidbyte_cli/services/suggestions/prompts/categories/cross_domain.md +37 -0
  222. vidbyte_cli/services/suggestions/prompts/categories/delegation.md +37 -0
  223. vidbyte_cli/services/suggestions/prompts/categories/experiment.md +37 -0
  224. vidbyte_cli/services/suggestions/prompts/categories/feedback.md +37 -0
  225. vidbyte_cli/services/suggestions/prompts/categories/goal_clarification.md +37 -0
  226. vidbyte_cli/services/suggestions/prompts/categories/immediate_next_steps.md +37 -0
  227. vidbyte_cli/services/suggestions/prompts/categories/investigation.md +37 -0
  228. vidbyte_cli/services/suggestions/prompts/categories/learning.md +37 -0
  229. vidbyte_cli/services/suggestions/prompts/categories/leverage.md +37 -0
  230. vidbyte_cli/services/suggestions/prompts/categories/long_term_directions.md +37 -0
  231. vidbyte_cli/services/suggestions/prompts/categories/preparation.md +37 -0
  232. vidbyte_cli/services/suggestions/prompts/categories/prerequisite.md +37 -0
  233. vidbyte_cli/services/suggestions/prompts/categories/prioritization.md +37 -0
  234. vidbyte_cli/services/suggestions/prompts/categories/product_experience.md +37 -0
  235. vidbyte_cli/services/suggestions/prompts/categories/quick_wins.md +37 -0
  236. vidbyte_cli/services/suggestions/prompts/categories/reframing.md +37 -0
  237. vidbyte_cli/services/suggestions/prompts/categories/risk_prevention.md +37 -0
  238. vidbyte_cli/services/suggestions/prompts/categories/simplification.md +37 -0
  239. vidbyte_cli/services/suggestions/prompts/categories/stop_or_defer.md +37 -0
  240. vidbyte_cli/services/suggestions/prompts/categories/strategy.md +37 -0
  241. vidbyte_cli/services/suggestions/prompts/categories/technical_possibilities.md +37 -0
  242. vidbyte_cli/services/suggestions/prompts/categories/verification.md +37 -0
  243. vidbyte_cli/services/suggestions/prompts/critic.md +209 -0
  244. vidbyte_cli/services/suggestions/prompts/critic_message.md +8 -0
  245. vidbyte_cli/services/suggestions/prompts/generator.md +64 -0
  246. vidbyte_cli/services/suggestions/prompts/library.py +72 -0
  247. vidbyte_cli/services/suggestions/prompts/message_generator_argument.md +1 -0
  248. vidbyte_cli/services/suggestions/prompts/message_generator_tool.md +1 -0
  249. vidbyte_cli/services/suggestions/prompts/message_parent_argument.md +1 -0
  250. vidbyte_cli/services/suggestions/prompts/message_parent_tool.md +1 -0
  251. vidbyte_cli/services/suggestions/prompts/message_receipt.md +1 -0
  252. vidbyte_cli/services/suggestions/prompts/revision.md +8 -0
  253. vidbyte_cli/services/suggestions/sdk.py +228 -0
  254. vidbyte_cli/services/suggestions/selection.py +78 -0
  255. vidbyte_cli/services/suggestions/service.py +540 -0
  256. vidbyte_cli/types/__init__.py +0 -0
  257. vidbyte_cli/types/api.py +101 -0
  258. vidbyte_cli/types/attachments.py +79 -0
  259. vidbyte_cli/types/ensemble.py +189 -0
  260. vidbyte_cli/types/provider.py +62 -0
  261. vidbyte_cli/types/research.py +260 -0
  262. vidbyte_cli/types/runtime.py +1098 -0
  263. vidbyte_cli/types/suggestions.py +518 -0
  264. vidbyte_cli-0.1.0.dist-info/METADATA +335 -0
  265. vidbyte_cli-0.1.0.dist-info/RECORD +269 -0
  266. vidbyte_cli-0.1.0.dist-info/WHEEL +5 -0
  267. vidbyte_cli-0.1.0.dist-info/entry_points.txt +2 -0
  268. vidbyte_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  269. vidbyte_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,85 @@
1
+ """Registers explicit accepted and rejected suggestion feedback commands.
2
+
3
+ Feedback is appended to one project's local JSON memory and never inferred from silence or
4
+ implementation. This module owns Click parsing and result rendering; validation and
5
+ persistence belong to `SuggestionFeedbackInput` and `SuggestionProject`.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import click
11
+
12
+ from ....lib.errors.failures import SuggestionFeedbackInvalid, SuggestionProjectInvalid
13
+ from ....lib.output import OutputDocument
14
+ from ....lib.runtime.context import ApplicationContext as Context
15
+ from ....services.suggestions.project import (
16
+ SuggestionFeedbackInput,
17
+ SuggestionProject,
18
+ SuggestionProjectKey,
19
+ )
20
+ from ....types.suggestions import FeedbackType
21
+ from .prompts.library import SuggestionHelpLibrary
22
+
23
+ _HELP = SuggestionHelpLibrary()
24
+ _GROUP_HELP = _HELP.load("feedback_group")
25
+ _ACCEPT_HELP = _HELP.load("feedback_accept")
26
+ _REJECT_HELP = _HELP.load("feedback_reject")
27
+ _PROJECT_HELP = _HELP.load("feedback_project")
28
+ _SUGGESTION_HELP = _HELP.load("feedback_suggestion")
29
+ _REASON_HELP = _HELP.load("feedback_reason")
30
+
31
+
32
+ class SuggestionFeedbackGroup:
33
+ """Attaches accepted and rejected feedback verbs below the suggest group."""
34
+
35
+ def register(self, parent: click.Group) -> None:
36
+ # Keeps both dispositions on one command family with one persistence implementation.
37
+ feedback = click.Group(name="feedback", help=_GROUP_HELP)
38
+ SuggestionFeedbackCommand(FeedbackType.ACCEPTED).register(feedback)
39
+ SuggestionFeedbackCommand(FeedbackType.REJECTED).register(feedback)
40
+ parent.add_command(feedback)
41
+
42
+
43
+ class SuggestionFeedbackCommand:
44
+ """Validates and appends one explicit accepted or rejected reaction."""
45
+
46
+ def __init__(self, feedback_type: FeedbackType) -> None:
47
+ # The disposition is fixed by registration so callers cannot pass arbitrary types.
48
+ self._feedback_type = feedback_type
49
+
50
+ def register(self, parent: click.Group) -> None:
51
+ # Adds one verb whose name and help match its fixed disposition.
52
+ accepted = self._feedback_type is FeedbackType.ACCEPTED
53
+
54
+ @parent.command(
55
+ name="accept" if accepted else "reject",
56
+ help=_ACCEPT_HELP if accepted else _REJECT_HELP,
57
+ )
58
+ @click.option("--project", required=True, help=_PROJECT_HELP)
59
+ @click.option("--suggestion", required=True, help=_SUGGESTION_HELP)
60
+ @click.option("--reason", default=None, help=_REASON_HELP)
61
+ @click.pass_obj
62
+ def _feedback(ctx: Context, project: str, suggestion: str, reason: str | None) -> None:
63
+ self.execute(ctx, project, suggestion, reason)
64
+
65
+ def execute(self, context: Context, project: str, suggestion: str, reason: str | None) -> None:
66
+ # A malformed key and malformed feedback name different options, so they fail apart.
67
+ try:
68
+ key = SuggestionProjectKey(project)
69
+ except (TypeError, ValueError) as error:
70
+ raise SuggestionProjectInvalid(str(error)) from error
71
+ try:
72
+ request = SuggestionFeedbackInput(key, self._feedback_type, suggestion, reason)
73
+ except (TypeError, ValueError) as error:
74
+ raise SuggestionFeedbackInvalid(str(error)) from error
75
+ record = SuggestionProject(context.paths()).record_feedback(request)
76
+ context.output().result(
77
+ OutputDocument(
78
+ kind="suggestions.feedback.recorded",
79
+ data={"project": key.value, "feedback": record.model_dump(mode="json")},
80
+ ),
81
+ f"Recorded {record.type.value} feedback for '{key.value}'.",
82
+ )
83
+
84
+
85
+ __all__ = ["SuggestionFeedbackGroup"]
@@ -0,0 +1,67 @@
1
+ """Extracts one handoff from a saved suggestion result without a model.
2
+
3
+ The command reads a `suggestions.result` document, selects one idea by id,
4
+ and emits its handoff as `suggestions.handoff`. This is a pure local read, so
5
+ it never needs credentials and never calls a provider on any code path.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from pathlib import Path
12
+
13
+ import click
14
+
15
+ from ....lib.errors.failures import SuggestionContextUnreadable, SuggestionInputInvalid
16
+ from ....lib.runtime.context import ApplicationContext as Context
17
+ from ....types.suggestions import SuggestionHandoff, SuggestionResult
18
+ from .prompts.library import SuggestionHelpLibrary
19
+ from .render import SuggestionRenderer
20
+
21
+ _HELP = SuggestionHelpLibrary()
22
+ _COMMAND_HELP = _HELP.load("handoff")
23
+ _INPUT_HELP = _HELP.load("input")
24
+ _IDEA_HELP = _HELP.load("idea")
25
+
26
+
27
+ class SuggestionHandoffCommand:
28
+ """Reads a saved batch and renders one deterministic handoff packet."""
29
+
30
+ def register(self, parent: click.Group) -> None:
31
+ # Attaches handoff with input and idea selectors as its only options.
32
+ @parent.command(name="handoff", help=_COMMAND_HELP)
33
+ @click.option(
34
+ "--input",
35
+ "input_path",
36
+ required=True,
37
+ type=click.Path(path_type=Path),
38
+ help=_INPUT_HELP,
39
+ )
40
+ @click.option("--idea", required=True, help=_IDEA_HELP)
41
+ @click.pass_obj
42
+ def _handoff(ctx: Context, input_path: Path, idea: str) -> None:
43
+ # Delegates to the execution method for direct testing.
44
+ self.execute(ctx, input_path, idea)
45
+
46
+ def execute(self, context: Context, input_path: Path, idea: str) -> None:
47
+ # Validates the document, selects the idea, and emits its handoff.
48
+ result = self._load(input_path)
49
+ selected = next((item for item in result.ideas if item.id == idea.strip()), None)
50
+ if selected is None:
51
+ raise SuggestionInputInvalid()
52
+ handoff = SuggestionHandoff.model_validate(selected.handoff.model_dump(mode="json"))
53
+ SuggestionRenderer().render_handoff(context, handoff)
54
+
55
+ def _load(self, input_path: Path) -> SuggestionResult:
56
+ # Rejects missing, malformed, and wrong-kind documents before selecting.
57
+ try:
58
+ text = Path(input_path).read_text(encoding="utf-8-sig")
59
+ document = json.loads(text)
60
+ except (OSError, json.JSONDecodeError) as error:
61
+ raise SuggestionContextUnreadable(str(error)) from error
62
+ if not isinstance(document, dict) or document.get("kind") != "suggestions.result":
63
+ raise SuggestionInputInvalid()
64
+ try:
65
+ return SuggestionResult.model_validate(document.get("data", document))
66
+ except Exception as error:
67
+ raise SuggestionContextUnreadable(str(error)) from error
@@ -0,0 +1,95 @@
1
+ """Registers local suggestion project creation and catalog listing commands.
2
+
3
+ Projects are small, credential-free local records that give later suggestion runs a stable
4
+ memory scope. This module owns Click parsing and output only; what a project is and how it is
5
+ stored belong to `SuggestionProject`.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import cast
11
+
12
+ import click
13
+ from pydantic import JsonValue
14
+
15
+ from ....lib.errors.failures import SuggestionProjectInvalid
16
+ from ....lib.output import OutputDocument
17
+ from ....lib.runtime.context import ApplicationContext as Context
18
+ from ....services.suggestions.project import (
19
+ SuggestionProject,
20
+ SuggestionProjectCreateInput,
21
+ SuggestionProjectKey,
22
+ )
23
+ from .prompts.library import SuggestionHelpLibrary
24
+
25
+ _HELP = SuggestionHelpLibrary()
26
+ _GROUP_HELP = _HELP.load("project_group")
27
+ _CREATE_HELP = _HELP.load("project_create")
28
+ _LIST_HELP = _HELP.load("project_list")
29
+ _KEY_HELP = _HELP.load("project_key")
30
+ _TITLE_HELP = _HELP.load("project_title")
31
+ _DESCRIPTION_HELP = _HELP.load("project_description")
32
+
33
+
34
+ class SuggestionProjectGroup:
35
+ """Attaches project create and list commands below the suggest group."""
36
+
37
+ def register(self, parent: click.Group) -> None:
38
+ # Keeps project verbs together without adding persistence logic to registration.
39
+ project = click.Group(name="project", help=_GROUP_HELP)
40
+ SuggestionProjectCreateCommand().register(project)
41
+ SuggestionProjectListCommand().register(project)
42
+ parent.add_command(project)
43
+
44
+
45
+ class SuggestionProjectCreateCommand:
46
+ """Validates and creates one local suggestion project."""
47
+
48
+ def register(self, parent: click.Group) -> None:
49
+ # Click owns required-field errors while the input dataclass owns domain validation.
50
+ @parent.command(name="create", help=_CREATE_HELP)
51
+ @click.option("--key", required=True, help=_KEY_HELP)
52
+ @click.option("--title", required=True, help=_TITLE_HELP)
53
+ @click.option("--description", required=True, help=_DESCRIPTION_HELP)
54
+ @click.pass_obj
55
+ def _create(ctx: Context, key: str, title: str, description: str) -> None:
56
+ self.execute(ctx, key, title, description)
57
+
58
+ def execute(self, context: Context, key: str, title: str, description: str) -> None:
59
+ # Every field is validated before the catalog is read, so a bad value writes nothing.
60
+ try:
61
+ request = SuggestionProjectCreateInput(SuggestionProjectKey(key), title, description)
62
+ except (TypeError, ValueError) as error:
63
+ raise SuggestionProjectInvalid(str(error)) from error
64
+ project = SuggestionProject(context.paths()).create(request)
65
+ context.output().result(
66
+ OutputDocument(
67
+ kind="suggestions.project.created", data=project.model_dump(mode="json")
68
+ ),
69
+ f"Created suggestion project '{project.key}'.",
70
+ )
71
+
72
+
73
+ class SuggestionProjectListCommand:
74
+ """Reads and renders the local suggestion project catalog."""
75
+
76
+ def register(self, parent: click.Group) -> None:
77
+ # Catalog listing is read-only and remains available without credentials.
78
+ @parent.command(name="list", help=_LIST_HELP)
79
+ @click.pass_obj
80
+ def _list(ctx: Context) -> None:
81
+ self.execute(ctx)
82
+
83
+ def execute(self, context: Context) -> None:
84
+ # Renders project summaries only; feedback bodies stay in their own files.
85
+ projects = SuggestionProject(context.paths()).list_projects()
86
+ data: dict[str, JsonValue] = {
87
+ "projects": [cast(JsonValue, project.model_dump(mode="json")) for project in projects]
88
+ }
89
+ human = "\n\n".join(
90
+ f"{project.key} {project.title}\n {project.description}" for project in projects
91
+ )
92
+ context.output().result(OutputDocument(kind="suggestions.projects", data=data), human)
93
+
94
+
95
+ __all__ = ["SuggestionProjectGroup"]
File without changes
@@ -0,0 +1,100 @@
1
+ """`vidbyte-cli login` — stores the user's Vidbyte API key for later commands.
2
+
3
+ There is no `--api-key` option: a secret in argv leaks into `ps`, shell history, and CI
4
+ logs. `--with-token` reads bounded stdin; anything else uses a hidden prompt.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import click
10
+
11
+ from ...lib.auth import CredentialStorage
12
+ from ...lib.auth.input import CredentialInput
13
+ from ...lib.errors.failures import FileFallbackNotApproved
14
+ from ...lib.output import OutputDocument
15
+ from ...lib.runtime.context import ApplicationContext
16
+
17
+
18
+ class LoginCommand:
19
+ def register(self, parent: click.Group) -> None:
20
+ # Attaches `login` to the root program.
21
+ @parent.command(name="login", help="Authenticate with a Vidbyte API key")
22
+ @click.option(
23
+ "--with-token",
24
+ is_flag=True,
25
+ help="Read the API key from stdin instead of a hidden prompt.",
26
+ )
27
+ @click.option(
28
+ "--allow-file-fallback",
29
+ is_flag=True,
30
+ help="Approve restricted-file storage when no OS keyring is available.",
31
+ )
32
+ @click.pass_obj
33
+ def _run(
34
+ context: ApplicationContext,
35
+ with_token: bool,
36
+ allow_file_fallback: bool,
37
+ ) -> None:
38
+ self.execute(context, with_token, allow_file_fallback)
39
+
40
+ def execute(
41
+ self,
42
+ context: ApplicationContext,
43
+ with_token: bool,
44
+ allow_file_fallback: bool,
45
+ ) -> None:
46
+ config = context.resolved_config()
47
+ credentials = CredentialInput(
48
+ context.streams,
49
+ context.output().terminal,
50
+ no_input=context.options.no_input,
51
+ ).read(from_stdin=with_token)
52
+ # Verify-before-persist. Syntactic checks cannot prove a key authenticates, and this
53
+ # call sits immediately before the only write so a reordering is obvious in review.
54
+ context.credential_verifier().verify(credentials, config)
55
+ fallback_allowed = self._fallback_consent(context, allow_file_fallback)
56
+ # Legacy state moves only on an explicit mutation, never on a read path.
57
+ context.migration().migrate_if_needed()
58
+ storage = context.credential_store().write(
59
+ credentials,
60
+ config.profile,
61
+ config.api_url,
62
+ allow_file_fallback=fallback_allowed,
63
+ )
64
+ if storage is CredentialStorage.RESTRICTED_FILE:
65
+ context.output().warning(
66
+ "The API key is stored in a permission-restricted file because no OS "
67
+ "keyring is available."
68
+ )
69
+ context.output().result(
70
+ OutputDocument(
71
+ kind="auth.login",
72
+ data={
73
+ "profile": config.profile,
74
+ "api_url": config.api_url,
75
+ "storage": storage.value,
76
+ },
77
+ ),
78
+ f"Authenticated profile '{config.profile}' using {storage.value}.",
79
+ )
80
+
81
+ def _fallback_consent(
82
+ self,
83
+ context: ApplicationContext,
84
+ explicitly_allowed: bool,
85
+ ) -> bool:
86
+ # The fallback file is readable by anything running as this user, so consent is the
87
+ # caller's to give — asked for here only when there is a terminal to ask on.
88
+ if context.credential_store().keyring.available():
89
+ return False
90
+ if explicitly_allowed:
91
+ return True
92
+ if context.options.no_input or not context.output().terminal.interactive:
93
+ raise FileFallbackNotApproved()
94
+ return bool(
95
+ click.confirm(
96
+ "No OS keyring is available. Store the key in a restricted local file?",
97
+ default=False,
98
+ err=True,
99
+ )
100
+ )
@@ -0,0 +1,41 @@
1
+ """`vidbyte-cli logout` — removes stored credentials from this machine.
2
+
3
+ Logout is a desired state, not an action that must have had something to undo: an
4
+ already-logged-out profile succeeds, so a cleanup script can run it unconditionally.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import click
10
+
11
+ from ...lib.output import OutputDocument
12
+ from ...lib.runtime.context import ApplicationContext
13
+
14
+
15
+ class LogoutCommand:
16
+ def register(self, parent: click.Group) -> None:
17
+ @parent.command(name="logout", help="Remove stored Vidbyte credentials from this machine")
18
+ @click.pass_obj
19
+ def _run(context: ApplicationContext) -> None:
20
+ self.execute(context)
21
+
22
+ def execute(self, context: ApplicationContext) -> None:
23
+ # Scoped to the selected profile and API host: logging out of staging must leave the
24
+ # production credential alone.
25
+ config = context.resolved_config()
26
+ removed = context.credential_store().clear(config.profile, config.api_url)
27
+ context.output().result(
28
+ OutputDocument(
29
+ kind="auth.logout",
30
+ data={
31
+ "profile": config.profile,
32
+ "api_url": config.api_url,
33
+ "removed": removed,
34
+ },
35
+ ),
36
+ (
37
+ f"Removed credentials for profile '{config.profile}'."
38
+ if removed
39
+ else f"Profile '{config.profile}' was already logged out."
40
+ ),
41
+ )
@@ -0,0 +1,51 @@
1
+ """`vidbyte-cli whoami` — shows which account the stored credentials belong to.
2
+
3
+ The identity comes from the same backend check login uses, so the two commands can never
4
+ disagree about whether a key is good. Reading is done through `CredentialResolver`, which puts
5
+ `VIDBYTE_API_KEY` ahead of the keyring — correct here, because that is the key later commands
6
+ would actually send.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import click
12
+ from pydantic import JsonValue
13
+
14
+ from ...lib.errors.failures import AuthenticationRequired
15
+ from ...lib.output import OutputDocument
16
+ from ...lib.runtime.context import ApplicationContext
17
+
18
+
19
+ class WhoamiCommand:
20
+ def register(self, parent: click.Group) -> None:
21
+ # Attaches `whoami` to the root program.
22
+ @parent.command(name="whoami", help="Show the account behind the stored credentials")
23
+ @click.pass_obj
24
+ def _run(context: ApplicationContext) -> None:
25
+ self.execute(context)
26
+
27
+ def execute(self, context: ApplicationContext) -> None:
28
+ # Resolves the stored credential, proves it against the backend, and prints the identity.
29
+ config = context.resolved_config()
30
+ credential = context.credential_resolver().resolve(config.profile, config.api_url)
31
+ if credential is None:
32
+ # Raised before any request, so an unauthenticated machine never reaches the network.
33
+ raise AuthenticationRequired()
34
+ identity = context.credential_verifier().verify(credential.credentials, config)
35
+ data: dict[str, JsonValue] = {
36
+ "profile": config.profile,
37
+ "api_url": config.api_url,
38
+ "username": identity.username,
39
+ "account_tier": identity.account_tier,
40
+ "credential_source": credential.source.value,
41
+ }
42
+ human = "\n".join(
43
+ (
44
+ f"Authenticated as: {identity.username}",
45
+ f"Account tier: {identity.account_tier}",
46
+ f"Profile: {config.profile}",
47
+ f"API URL: {config.api_url}",
48
+ f"Credential source: {credential.source.value}",
49
+ )
50
+ )
51
+ context.output().result(OutputDocument(kind="auth.whoami", data=data), human)
@@ -0,0 +1,20 @@
1
+ """Commands for explicitly funding the authenticated Vidbyte API balance."""
2
+
3
+ import click
4
+
5
+ from .prompts.library import BillingHelpLibrary
6
+ from .top_up import BillingTopUpCommand
7
+
8
+ _GROUP_HELP = BillingHelpLibrary().load("group")
9
+
10
+
11
+ class BillingCommandGroup:
12
+ """Registers billing commands beneath the CLI's top-level billing group."""
13
+
14
+ def register(self, parent: click.Group) -> None:
15
+ billing = click.Group(name="billing", help=_GROUP_HELP)
16
+ BillingTopUpCommand().register(billing)
17
+ parent.add_command(billing)
18
+
19
+
20
+ __all__ = ["BillingCommandGroup", "BillingTopUpCommand"]
@@ -0,0 +1 @@
1
+ """Markdown assets used by billing command help."""
@@ -0,0 +1,16 @@
1
+ # `--confirm`
2
+
3
+ ## Purpose
4
+ This required opt-in authorizes the billing command to proceed toward a paid Vidbyte API-balance top-up. Supplying it means the caller has obtained the user's explicit approval for a minimum five-dollar purchase. Without it, the command exits before resolving credentials, constructing a signer, or sending an HTTP request. It does not approve unrelated charges or authorize a later top-up. Error handling in other commands never adds this flag automatically.
5
+
6
+ ## Default and when to use
7
+ The default is off, so omitting the flag cannot create a payment authorization. Add it only when the user has reviewed the amount and asked to fund the Vidbyte API balance. Do not infer approval from an earlier request to run research or from a prior top-up. If the user has not approved spending, leave the flag out and stop at the recovery instructions. The command does not prompt interactively in place of this explicit choice.
8
+
9
+ ## Payment boundary
10
+ With confirmation present, the command still pays only the exact challenge returned by `POST /agent/topup`. The endpoint controls the accepted amount, network, asset, and recipient. The flag does not expose or print the private key and does not authorize payment to a different endpoint. If payment configuration is missing or the challenge is invalid, the command fails without launching an agent. Provider-model charges are outside this confirmation and remain governed by the provider account.
11
+
12
+ ## Examples
13
+ After user approval, run `vidbyte-cli billing top-up --confirm` to create a new top-up request. To recover the same uncertain request, pass the original key as `--idempotency-key` along with `--confirm`. Do not combine confirmation with a new key when recovering an uncertain payment, because a new key can represent a second charge. After success, verify the available balance before retrying the original operation. If approval is absent, run no payment command.
14
+
15
+ ## Related commands
16
+ The parent command is `vidbyte-cli billing top-up`. Its help explains account setup, payment behavior, output, and recovery. The idempotency option's help explains how to reuse a key after an uncertain response. Neither option changes the requirement for explicit user approval. The browser fallback is <https://vidbyte.pro/settings/api>.
@@ -0,0 +1,13 @@
1
+ # Billing commands
2
+
3
+ ## Purpose
4
+ The billing group contains explicit operations for managing the Vidbyte API balance used by API-key requests. Its commands interact with Vidbyte billing endpoints and do not manage model-provider accounts. Billing actions that spend money require a separate explicit command invocation and user approval. A failed research or runtime command never calls a billing command automatically. Read the child command help before making a purchase.
5
+
6
+ ## Available command
7
+ Use `billing top-up` to add the backend's minimum five-dollar credit to the API balance associated with the configured Vidbyte key. The top-up command pays the exact x402 challenge returned by the billing endpoint. It reports the credited amount and the balance available afterward. It does not start or resume any prior operation. After a top-up, confirm the balance and then decide whether the original operation should be retried or resumed.
8
+
9
+ ## Approval and credentials
10
+ Obtain explicit approval from the user before invoking a paid billing action. Configure the same Vidbyte API key that made the request requiring more balance. Configure any payment signer through the supported environment variable and never pass its secret as a command argument. The command requires `--confirm` before it resolves credentials or sends HTTP. If the user has not approved a purchase, stop and use the account settings page instead.
11
+
12
+ ## Recovery and related commands
13
+ When a top-up response is uncertain, repeat the same top-up with its original idempotency key so the backend can recover that purchase. A new key represents a distinct operation and can create an additional charge. The idempotency key from the failed research operation is separate and should remain unchanged when retrying that operation. If payment cannot be completed in the CLI, visit <https://vidbyte.pro/settings/api> to manage API balance in a browser. Provider charges for model usage remain separate from Vidbyte API balance.
@@ -0,0 +1,16 @@
1
+ # `--idempotency-key`
2
+
3
+ ## Purpose
4
+ This option identifies one top-up operation to the backend so a repeated request can recover the same result instead of creating a second purchase. For a new top-up, leave it unset and the CLI creates a fresh key. For a retry after a timeout or lost response, provide exactly the key used by the original attempt. The key applies only to this top-up and does not replace the key from the failed research or runtime operation. Reusing a key does not itself prove that payment succeeded; inspect the returned balance.
5
+
6
+ ## Defaults and recovery
7
+ The default is no caller-provided key, which causes the CLI to generate one for a new purchase. Supply a key only when recovering the same top-up whose outcome is uncertain. Keep the value identical across retries and preserve it somewhere safe until the outcome is known. Do not reuse a completed top-up's key for a separate purchase. Do not generate a replacement key just because the first response was missing, since that can create another charge.
8
+
9
+ ## Accepted value
10
+ Pass one stable string of 8 to 128 letters, digits, dots, underscores, colons, or hyphens. Avoid including account data, credentials, or other secrets in the value. The key is sent to the Vidbyte API as the idempotency identifier for the top-up request. The same key must accompany both the unpaid challenge request and the paid retry. The CLI does not print the payment authorization as part of its result.
11
+
12
+ ## Examples
13
+ Start a new approved purchase with `vidbyte-cli billing top-up --confirm` and let the CLI generate a key. If that request times out, repeat it with `vidbyte-cli billing top-up --confirm --idempotency-key <same-key>`. Replace the placeholder with the exact key from the original request rather than the literal angle-bracket text. Confirm the balance returned by the API before retrying the original operation. Use a different fresh key only for a distinct top-up after a separate user approval.
14
+
15
+ ## Related commands
16
+ The parent `vidbyte-cli billing top-up` help describes the charge and the post-payment recovery sequence. `--confirm` remains mandatory for both new and recovered attempts. The original failed operation's idempotency key is separate and must be retained for retrying that operation. Use <https://vidbyte.pro/settings/api> when the caller cannot safely make the payment through this command.
@@ -0,0 +1,18 @@
1
+ """Loads caller-facing billing help from packaged Markdown assets."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib import resources
6
+
7
+
8
+ class BillingHelpLibrary:
9
+ """Reads each billing 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,16 @@
1
+ # Billing top-up
2
+
3
+ ## Purpose
4
+ Use this command to add the backend's minimum five-dollar credit to the Vidbyte API balance associated with the configured API key. The command does not top up a model-provider account or change the provider credentials used by a research run. It sends `POST /agent/topup` and pays the exact x402 challenge returned by that endpoint. The backend response reports the amount credited and the remaining available balance. Call this command only after the account owner has approved the purchase.
5
+
6
+ ## When to use
7
+ Use this command after a Vidbyte API-balance error when the user has approved a charge and wants to continue the original operation. Before running it, configure the API key for the same Vidbyte account that received the error. Configure the x402 private key through the supported environment variable without pasting it into a command, prompt, or log. If a payment result is uncertain, reuse the idempotency key from that same top-up instead of creating a new purchase. If payment setup is unavailable, the user can add balance at <https://vidbyte.pro/settings/api>.
8
+
9
+ ## Approval and cost
10
+ This command creates a real payment authorization and must never be invoked as an automatic error-recovery side effect. Obtain explicit user approval before adding `--confirm`. The API determines the exact payment challenge; this CLI does not let the caller select a different amount, rail, or recipient. The minimum top-up is five US dollars, and any additional charge must be established by the challenge itself rather than inferred from this help text. Model-provider usage remains separately billed by that provider.
11
+
12
+ ## Output and next step
13
+ On success, human output names the credited amount, payment rail, and available Vidbyte API balance. JSON and JSONL output contain the corresponding typed result fields. The command does not start or resume the failed agent operation. Verify that the available balance covers the original or remaining operation, then retry or resume it explicitly and preserve its original idempotency key when one was supplied.
14
+
15
+ ## Related commands
16
+ Use `vidbyte-cli agents` or the relevant research command only after the balance is confirmed. The original operation may have made progress before a mid-run exhaustion error, so inspect its status before deciding whether to resume or repeat it. For browser-based account management, use the API settings URL above. Do not rerun a top-up with a new key merely because the first response was lost.
@@ -0,0 +1,65 @@
1
+ """Explicitly pay the Vidbyte API-balance top-up challenge."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+ from uuid import uuid4
7
+
8
+ import click
9
+
10
+ from ...lib.api.endpoints.billing import TOP_UP_CENTS
11
+ from ...lib.errors.failures import BillingTopUpApprovalRequired
12
+ from ...lib.output import OutputDocument
13
+ from .prompts.library import BillingHelpLibrary
14
+
15
+ if TYPE_CHECKING:
16
+ from ...lib.runtime.context import ApplicationContext
17
+
18
+ _TOP_UP_HELP = BillingHelpLibrary().load("top_up")
19
+ _CONFIRM_HELP = BillingHelpLibrary().load("confirm")
20
+ _IDEMPOTENCY_KEY_HELP = BillingHelpLibrary().load("idempotency_key")
21
+
22
+
23
+ class BillingTopUpCommand:
24
+ """Registers the one explicit command that can purchase API balance."""
25
+
26
+ def register(self, parent: click.Group) -> None:
27
+ # Keeps payment behind an explicit confirmation flag and never runs from error handling.
28
+ @parent.command(name="top-up", help=_TOP_UP_HELP)
29
+ @click.option(
30
+ "--confirm",
31
+ is_flag=True,
32
+ help=_CONFIRM_HELP,
33
+ )
34
+ @click.option(
35
+ "--idempotency-key",
36
+ default=None,
37
+ help=_IDEMPOTENCY_KEY_HELP,
38
+ )
39
+ @click.pass_obj
40
+ def _run(context: ApplicationContext, confirm: bool, idempotency_key: str | None) -> None:
41
+ # Routes parsed Click options through the typed command boundary.
42
+ self.execute(context, confirm, idempotency_key)
43
+
44
+ def execute(
45
+ self, context: ApplicationContext, confirm: bool, idempotency_key: str | None
46
+ ) -> None:
47
+ # Validates approval before resolving credentials or creating a payment authorization.
48
+ if not confirm:
49
+ raise BillingTopUpApprovalRequired()
50
+ from ...lib.api.runtime_payment import RuntimePayment
51
+
52
+ payer = RuntimePayment(context.environment, TOP_UP_CENTS)
53
+ result = context.billing_endpoints().top_up(idempotency_key or str(uuid4()), payer)
54
+ context.output().result(
55
+ OutputDocument(kind="billing.top_up", data=result.model_dump(mode="json")),
56
+ self._summary(result.credited_cents, result.available_balance_cents, result.rail),
57
+ )
58
+
59
+ @staticmethod
60
+ def _summary(credited_cents: int, available_balance_cents: int, rail: str) -> str:
61
+ # Reports only non-secret financial outcome fields returned by the backend.
62
+ return (
63
+ f"Added ${credited_cents / 100:.2f} to Vidbyte API balance via {rail}. "
64
+ f"Available balance: ${available_balance_cents / 100:.2f}."
65
+ )
File without changes