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,45 @@
1
+ """`vidbyte-cli config get <key>` — reads a non-secret CLI setting.
2
+
3
+ It reports the *effective* value, not the stored one, so an environment override is visible
4
+ rather than mysterious. Machine output carries the provenance alongside it.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from enum import Enum
10
+
11
+ import click
12
+
13
+ from ...lib.config import ConfigField
14
+ from ...lib.output import OutputDocument
15
+ from ...lib.runtime.context import ApplicationContext
16
+
17
+
18
+ class ConfigGetCommand:
19
+ def register(self, parent: click.Group) -> None:
20
+ @parent.command(name="get", help="Read an effective CLI configuration value")
21
+ @click.argument("key", type=click.Choice([item.value for item in ConfigField]))
22
+ @click.pass_obj
23
+ def _run(context: ApplicationContext, key: str) -> None:
24
+ self.execute(context, ConfigField(key))
25
+
26
+ def execute(self, context: ApplicationContext, field: ConfigField) -> None:
27
+ config = context.resolved_config()
28
+ value = getattr(config, field.value)
29
+ serialized = value.value if isinstance(value, Enum) else value
30
+ source = config.provenance[field]
31
+ if context.options.debug:
32
+ context.output().diagnostic(f"{field.value} source: {source.value}")
33
+ # Human output is the bare value so `$(vidbyte-cli config get api_url)` is usable.
34
+ context.output().result(
35
+ OutputDocument(
36
+ kind="config.value",
37
+ data={
38
+ "profile": config.profile,
39
+ "key": field.value,
40
+ "value": serialized,
41
+ "source": source.value,
42
+ },
43
+ ),
44
+ str(serialized),
45
+ )
@@ -0,0 +1,39 @@
1
+ """`vidbyte-cli config set <key> <value>` — writes a non-secret CLI setting.
2
+
3
+ The value stays a string across Click's boundary and is parsed by `ProfileConfig`, so the
4
+ command line, the environment, and the persisted document all validate the same way.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from enum import Enum
10
+
11
+ import click
12
+
13
+ from ...lib.config import ConfigField
14
+ from ...lib.output import OutputDocument
15
+ from ...lib.runtime.context import ApplicationContext
16
+
17
+
18
+ class ConfigSetCommand:
19
+ def register(self, parent: click.Group) -> None:
20
+ @parent.command(name="set", help="Write a CLI configuration value")
21
+ @click.argument("key", type=click.Choice([item.value for item in ConfigField]))
22
+ @click.argument("value")
23
+ @click.pass_obj
24
+ def _run(context: ApplicationContext, key: str, value: str) -> None:
25
+ self.execute(context, ConfigField(key), value)
26
+
27
+ def execute(self, context: ApplicationContext, field: ConfigField, value: str) -> None:
28
+ profile = context.options.profile
29
+ updated = context.config_store().set(field, value, profile)
30
+ # Echo what was stored, not what was typed: normalization may have changed it.
31
+ saved = getattr(updated, field.value)
32
+ serialized = saved.value if isinstance(saved, Enum) else saved
33
+ context.output().result(
34
+ OutputDocument(
35
+ kind="config.updated",
36
+ data={"profile": profile, "key": field.value, "value": serialized},
37
+ ),
38
+ f"Set {field.value} for profile '{profile}' to {serialized}.",
39
+ )
@@ -0,0 +1,9 @@
1
+ """Provider commands group export."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .login import ProviderLoginCommand
6
+ from .logout import ProviderLogoutCommand
7
+ from .whoami import ProviderWhoamiCommand
8
+
9
+ __all__ = ["ProviderLoginCommand", "ProviderLogoutCommand", "ProviderWhoamiCommand"]
@@ -0,0 +1,102 @@
1
+ """`vidbyte-cli provider login` — verifies a provider key then stores it."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ from ...lib.auth.provider_input import ProviderCredentialInput
8
+ from ...lib.errors.failures import FileFallbackNotApprovedForProvider
9
+ from ...lib.output import OutputDocument
10
+ from ...lib.runtime.context import ApplicationContext
11
+ from ...types.provider import Provider
12
+
13
+
14
+ class ProviderLoginCommand:
15
+ """Authenticates a BYOK provider key."""
16
+
17
+ def register(self, parent: click.Group) -> None:
18
+ # Adds `provider login <provider>` with optional stdin and fallback flags.
19
+ @parent.command(
20
+ name="login",
21
+ help="Authenticate a provider API key (openai, claude, grok, deepseek, glm, muse, gemini)", # noqa: E501
22
+ )
23
+ @click.argument("provider", type=click.Choice([p.value for p in Provider]))
24
+ @click.option(
25
+ "--with-token",
26
+ is_flag=True,
27
+ help="Read the provider key from stdin instead of a hidden prompt.",
28
+ )
29
+ @click.option(
30
+ "--allow-file-fallback",
31
+ is_flag=True,
32
+ help="Approve restricted-file storage when no OS keyring is available.",
33
+ )
34
+ @click.pass_obj
35
+ def _run(
36
+ context: ApplicationContext,
37
+ provider: str,
38
+ with_token: bool,
39
+ allow_file_fallback: bool,
40
+ ) -> None:
41
+ self.execute(context, provider, with_token, allow_file_fallback)
42
+
43
+ def execute(
44
+ self,
45
+ context: ApplicationContext,
46
+ provider: str,
47
+ with_token: bool,
48
+ allow_file_fallback: bool,
49
+ ) -> None:
50
+ # Verify-before-persist so a bad key never reaches storage.
51
+ config = context.resolved_config()
52
+ typed = Provider(provider)
53
+ credentials = ProviderCredentialInput(
54
+ context.streams,
55
+ context.output().terminal,
56
+ no_input=context.options.no_input,
57
+ provider=typed,
58
+ ).read(from_stdin=with_token)
59
+ context.provider_verifier(typed).verify(credentials)
60
+ fallback_allowed = self._fallback_consent(context, allow_file_fallback)
61
+ context.migration().migrate_if_needed()
62
+ storage = context.provider_store().write(
63
+ credentials,
64
+ config.profile,
65
+ typed,
66
+ allow_file_fallback=fallback_allowed,
67
+ )
68
+ if storage.value == "restricted_file":
69
+ context.output().warning(
70
+ "The provider key is stored in a permission-restricted file because no OS "
71
+ "keyring is available."
72
+ )
73
+ context.output().result(
74
+ OutputDocument(
75
+ kind="provider.login",
76
+ data={
77
+ "profile": config.profile,
78
+ "provider": typed.value,
79
+ "storage": storage.value,
80
+ },
81
+ ),
82
+ (
83
+ f"Authenticated provider '{typed.value}' for profile "
84
+ f"'{config.profile}' using {storage.value}."
85
+ ),
86
+ )
87
+
88
+ def _fallback_consent(self, context: ApplicationContext, explicitly_allowed: bool) -> bool:
89
+ # Restricted file needs explicit consent when no keyring is available.
90
+ if context.provider_store().keyring.available():
91
+ return False
92
+ if explicitly_allowed:
93
+ return True
94
+ if context.options.no_input or not context.output().terminal.interactive:
95
+ raise FileFallbackNotApprovedForProvider(typed_name="provider")
96
+ return bool(
97
+ click.confirm(
98
+ "No OS keyring is available. Store the provider key in a restricted local file?",
99
+ default=False,
100
+ err=True,
101
+ )
102
+ )
@@ -0,0 +1,45 @@
1
+ """`vidbyte-cli provider logout` — removes a stored provider key."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ from ...lib.output import OutputDocument
8
+ from ...lib.runtime.context import ApplicationContext
9
+ from ...types.provider import Provider
10
+
11
+
12
+ class ProviderLogoutCommand:
13
+ """Removes a stored BYOK provider credential."""
14
+
15
+ def register(self, parent: click.Group) -> None:
16
+ # Scoped logout: only the named provider for the active profile is cleared.
17
+ @parent.command(name="logout", help="Remove a stored provider API key")
18
+ @click.argument("provider", type=click.Choice([p.value for p in Provider]))
19
+ @click.pass_obj
20
+ def _run(context: ApplicationContext, provider: str) -> None:
21
+ self.execute(context, provider)
22
+
23
+ def execute(self, context: ApplicationContext, provider: str) -> None:
24
+ # Clears keyring and file entries for just this provider.
25
+ config = context.resolved_config()
26
+ typed = Provider(provider)
27
+ removed = context.provider_store().clear(config.profile, typed)
28
+ context.output().result(
29
+ OutputDocument(
30
+ kind="provider.logout",
31
+ data={
32
+ "profile": config.profile,
33
+ "provider": typed.value,
34
+ "removed": removed,
35
+ },
36
+ ),
37
+ (
38
+ f"Removed provider '{typed.value}' for profile '{config.profile}'."
39
+ if removed
40
+ else (
41
+ f"Provider '{typed.value}' was already logged out for profile "
42
+ f"'{config.profile}'."
43
+ )
44
+ ),
45
+ )
@@ -0,0 +1,49 @@
1
+ """`vidbyte-cli provider whoami` — shows which provider key is stored."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+ from pydantic import JsonValue
7
+
8
+ from ...lib.output import OutputDocument
9
+ from ...lib.runtime.context import ApplicationContext
10
+ from ...types.provider import Provider
11
+
12
+
13
+ class ProviderWhoamiCommand:
14
+ """Reports the identity behind a stored provider key."""
15
+
16
+ def register(self, parent: click.Group) -> None:
17
+ # Attaches `whoami` to the provider group.
18
+ @parent.command(name="whoami", help="Show the provider behind stored credentials")
19
+ @click.argument("provider", type=click.Choice([p.value for p in Provider]))
20
+ @click.pass_obj
21
+ def _run(context: ApplicationContext, provider: str) -> None:
22
+ self.execute(context, provider)
23
+
24
+ def execute(self, context: ApplicationContext, provider: str) -> None:
25
+ # Resolves the stored provider key and re-probes to prove it is live.
26
+ config = context.resolved_config()
27
+ typed = Provider(provider)
28
+ resolved = context.provider_resolver().resolve(config.profile, typed)
29
+ if resolved is None:
30
+ # No probe when nothing is stored — mirrors whoami's no-network rule.
31
+ from ...lib.errors.failures import ProviderAuthenticationRequired
32
+
33
+ raise ProviderAuthenticationRequired(typed.value)
34
+ identity = context.provider_verifier(typed).verify(resolved.credentials)
35
+ data: dict[str, JsonValue] = {
36
+ "profile": config.profile,
37
+ "provider": typed.value,
38
+ "verified": identity.verified,
39
+ "credential_source": resolved.source.value,
40
+ }
41
+ human = "\n".join(
42
+ (
43
+ f"Provider: {typed.value}",
44
+ f"Verified: {identity.verified}",
45
+ f"Profile: {config.profile}",
46
+ f"Credential source: {resolved.source.value}",
47
+ )
48
+ )
49
+ context.output().result(OutputDocument(kind="provider.whoami", data=data), human)
@@ -0,0 +1,5 @@
1
+ """Package marker for the `research` command namespace.
2
+
3
+ Deliberately empty of re-exports: `commands/__init__.py` imports each command class from its
4
+ own module, so adding a verb here never widens what a help path has to import.
5
+ """
@@ -0,0 +1,41 @@
1
+ """`vidbyte-cli research add <thread_id> <prompt>` — admit another run into an existing thread.
2
+
3
+ The thread ID is the public share token `research start` and `research threads` print. It is
4
+ checked locally first: the backend matches that path segment against a pattern before any
5
+ lookup, so an internal identifier pasted from another tool fails as a bare validation error
6
+ that explains nothing.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import click
12
+
13
+ from ...lib.runtime.context import ApplicationContext
14
+ from ...types.research import ThreadId
15
+ from .options import ResearchRunOptions
16
+ from .render import ResearchRenderer
17
+
18
+
19
+ class ResearchAddCommand:
20
+ def register(self, parent: click.Group) -> None:
21
+ options = ResearchRunOptions()
22
+
23
+ def _run(context: ApplicationContext, /, **values: object) -> None:
24
+ self.execute(context, values)
25
+
26
+ callback = click.pass_obj(_run)
27
+ callback = click.argument("prompt")(callback)
28
+ callback = click.argument("thread_id")(callback)
29
+ parent.command(name="add", help="Add another prompt to an existing research thread")(
30
+ options.apply(callback)
31
+ )
32
+
33
+ def execute(self, context: ApplicationContext, values: dict[str, object]) -> None:
34
+ # The ID guard runs first so a mistyped thread never triggers a credential lookup.
35
+ thread_id = ThreadId.parse(str(values.get("thread_id", "")))
36
+ options = ResearchRunOptions()
37
+ request = options.build(values)
38
+ key = str(options.key(values))
39
+ accepted = context.research_endpoints().append_run(str(thread_id), request, key)
40
+ rendered = ResearchRenderer().accepted(accepted, key)
41
+ context.output().result(rendered.document, rendered.human)
@@ -0,0 +1,134 @@
1
+ """The request-shaping options `research start` and `research add` both accept.
2
+
3
+ Declared once because the two commands submit the identical body to different routes, and a
4
+ flag that drifted between them would be a silent behaviour difference nobody could see from
5
+ either file.
6
+
7
+ Only options the backend can act on are exposed. `source_sites` and `reference_artifact_ids`
8
+ exist on the request DTO but need a catalogue and an artifact list the API-key surface does
9
+ not publish, so a flag for either would accept values a caller has no way to discover.
10
+
11
+ Click hands a callback its parameters as loose values, so the readers below are where those
12
+ become typed: each one narrows a single option and the model rejects anything still wrong.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from collections.abc import Callable, Mapping
18
+
19
+ import click
20
+
21
+ from ...types.research import (
22
+ IdempotencyKey,
23
+ ResearchKind,
24
+ ResearchRunCreateRequest,
25
+ ResearchSize,
26
+ )
27
+
28
+ CommandCallback = Callable[..., None]
29
+ OptionDecorator = Callable[[CommandCallback], CommandCallback]
30
+
31
+
32
+ class ResearchRunOptions:
33
+ """Declares the shared run options and turns the parsed values into a request."""
34
+
35
+ def apply(self, callback: CommandCallback) -> CommandCallback:
36
+ # Click decorators apply bottom-up, so this reverses them to match the help listing.
37
+ for decorate in reversed(self._decorators()):
38
+ callback = decorate(callback)
39
+ return callback
40
+
41
+ def build(self, values: Mapping[str, object]) -> ResearchRunCreateRequest:
42
+ # Unset options stay None so the encoder omits them and the backend owns its defaults.
43
+ return ResearchRunCreateRequest(
44
+ prompt=self._prompt(values),
45
+ size=self._size(values),
46
+ target_sources=self._count(values, "target_sources"),
47
+ search_calls=self._count(values, "search_calls"),
48
+ resource_kinds=self._kinds(values),
49
+ include_domains=self._many(values, "include_domain"),
50
+ exclude_domains=self._many(values, "exclude_domain"),
51
+ published_after=self._text(values, "published_after"),
52
+ language=self._text(values, "language"),
53
+ max_run_cost_cents=self._count(values, "max_cost_cents"),
54
+ )
55
+
56
+ def key(self, values: Mapping[str, object]) -> IdempotencyKey:
57
+ # Validated before the request is built, so a bad key never reaches the network.
58
+ return IdempotencyKey.create(self._text(values, "idempotency_key"))
59
+
60
+ def _decorators(self) -> tuple[OptionDecorator, ...]:
61
+ return (
62
+ click.option(
63
+ "--size",
64
+ type=click.Choice([item.value for item in ResearchSize]),
65
+ help="Preset run breadth, when not sizing the run by hand.",
66
+ ),
67
+ click.option(
68
+ "--target-sources",
69
+ type=click.IntRange(1, 1_000),
70
+ help="How many sources discovery should aim to collect.",
71
+ ),
72
+ click.option(
73
+ "--search-calls",
74
+ type=click.IntRange(1, 100),
75
+ help="Upper bound on provider search calls for this run.",
76
+ ),
77
+ click.option(
78
+ "--kind",
79
+ multiple=True,
80
+ type=click.Choice([item.value for item in ResearchKind]),
81
+ help="Restrict discovery to this resource kind. Repeatable.",
82
+ ),
83
+ click.option(
84
+ "--include-domain",
85
+ multiple=True,
86
+ help="Restrict discovery to this hostname. Repeatable.",
87
+ ),
88
+ click.option(
89
+ "--exclude-domain",
90
+ multiple=True,
91
+ help="Exclude this hostname from discovery. Repeatable.",
92
+ ),
93
+ click.option(
94
+ "--published-after",
95
+ help="Only consider sources published on or after this YYYY-MM-DD date.",
96
+ ),
97
+ click.option("--language", help="Preferred source language tag, such as 'en'."),
98
+ click.option(
99
+ "--max-cost-cents",
100
+ type=click.IntRange(1, 100_000),
101
+ help="Refuse the run if it would cost more than this many cents.",
102
+ ),
103
+ click.option(
104
+ "--idempotency-key",
105
+ help="Reuse a key to retry a priced mutation without being charged twice.",
106
+ ),
107
+ )
108
+
109
+ def _prompt(self, values: Mapping[str, object]) -> str:
110
+ # A required Click argument is always present; the model rejects a blank one.
111
+ return str(values.get("prompt", ""))
112
+
113
+ def _text(self, values: Mapping[str, object], name: str) -> str | None:
114
+ value = values.get(name)
115
+ return str(value) if isinstance(value, str) and value else None
116
+
117
+ def _count(self, values: Mapping[str, object], name: str) -> int | None:
118
+ value = values.get(name)
119
+ return value if isinstance(value, int) else None
120
+
121
+ def _many(self, values: Mapping[str, object], name: str) -> list[str] | None:
122
+ # A repeatable option arrives as an empty tuple when unused, which must stay absent.
123
+ value = values.get(name)
124
+ if not isinstance(value, tuple) or not value:
125
+ return None
126
+ return [str(item) for item in value]
127
+
128
+ def _size(self, values: Mapping[str, object]) -> ResearchSize | None:
129
+ value = self._text(values, "size")
130
+ return ResearchSize(value) if value is not None else None
131
+
132
+ def _kinds(self, values: Mapping[str, object]) -> list[ResearchKind] | None:
133
+ value = self._many(values, "kind")
134
+ return [ResearchKind(item) for item in value] if value is not None else None
@@ -0,0 +1,157 @@
1
+ """Human text and machine documents for every research result shape.
2
+
3
+ Both encodings are produced together so they cannot drift: a field added to the JSON without
4
+ a line in the human block is the failure mode this file exists to prevent.
5
+
6
+ The only thread identifier that appears anywhere here is `thread_id`, which the models bound
7
+ to the API's public share token. The backend's internal identifier never reaches this layer,
8
+ so no rendered line can offer a value a user would be unable to paste back.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass
14
+
15
+ from pydantic import JsonValue
16
+
17
+ from ...lib.output import OutputDocument
18
+ from ...types.research import (
19
+ ResearchPortfolioPage,
20
+ ResearchRunAccepted,
21
+ ResearchRunStatus,
22
+ ResearchThreadDetail,
23
+ ResearchThreadSummary,
24
+ )
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class RenderedResult:
29
+ """One result in both encodings the output manager may be asked for."""
30
+
31
+ document: OutputDocument
32
+ human: str
33
+
34
+
35
+ class ResearchRenderer:
36
+ """Presentation for the four shapes the research surface returns."""
37
+
38
+ def accepted(self, accepted: ResearchRunAccepted, idempotency_key: str) -> RenderedResult:
39
+ # Admission: the two identifiers the caller needs for every follow-up command.
40
+ data: dict[str, JsonValue] = {
41
+ "thread_id": accepted.thread_id,
42
+ "run_id": accepted.run_id,
43
+ "status": accepted.status.value,
44
+ "idempotency_key": idempotency_key,
45
+ }
46
+ human = "\n".join(
47
+ (
48
+ f"Thread: {accepted.thread_id}",
49
+ f"Run: {accepted.run_id}",
50
+ f"Status: {accepted.status.value}",
51
+ "",
52
+ f"Watch it: vidbyte-cli research watch {accepted.run_id}",
53
+ f'Add to it: vidbyte-cli research add {accepted.thread_id} "..."',
54
+ )
55
+ )
56
+ return RenderedResult(OutputDocument(kind="research.run_accepted", data=data), human)
57
+
58
+ def run_status(self, run: ResearchRunStatus) -> RenderedResult:
59
+ # One run's coarse progress. The status route publishes no live counters.
60
+ data: dict[str, JsonValue] = {
61
+ "run_id": run.run_id,
62
+ "status": run.status.value,
63
+ "phase": run.phase,
64
+ "continuation_count": run.continuation_count,
65
+ "updated_at": run.updated_at.isoformat(),
66
+ "terminal": run.status.is_terminal(),
67
+ # Published so an agent can branch on it without reimplementing the rule that
68
+ # a continuation is legal only from partial, failed, or credit-exhausted.
69
+ "resumable": run.status.is_resumable(),
70
+ }
71
+ lines = [
72
+ f"Run: {run.run_id}",
73
+ f"Status: {run.status.value}",
74
+ f"Phase: {run.phase}",
75
+ f"Continued: {run.continuation_count}",
76
+ f"Updated: {run.updated_at.isoformat()}",
77
+ ]
78
+ lines.extend(self._next_step(run))
79
+ document = OutputDocument(kind="research.run_status", data=data)
80
+ return RenderedResult(document, "\n".join(lines))
81
+
82
+ def _next_step(self, run: ResearchRunStatus) -> list[str]:
83
+ # A settled run is exactly where a caller needs the next command spelled out, and
84
+ # resuming is legal only from the three states `is_resumable` names.
85
+ if run.status.is_resumable():
86
+ return ["", f"Resume it: vidbyte-cli research resume {run.run_id}"]
87
+ if run.status.is_terminal():
88
+ return []
89
+ return ["", f"Watch it: vidbyte-cli research watch {run.run_id}"]
90
+
91
+ def transition(self, run: ResearchRunStatus) -> RenderedResult:
92
+ # A progress line during watch; the same record, marked as not-yet-final.
93
+ rendered = self.run_status(run)
94
+ document = OutputDocument(kind="research.run_transition", data=rendered.document.data)
95
+ return RenderedResult(document, f"{run.status.value} - {run.phase}")
96
+
97
+ def thread(self, thread: ResearchThreadDetail) -> RenderedResult:
98
+ # One thread read on its own, including the two fields a portfolio row omits.
99
+ data = self._thread_fields(thread)
100
+ data["latest_run_phase"] = thread.latest_run_phase
101
+ data["favorite"] = thread.favorite
102
+ human = "\n".join(
103
+ (
104
+ f"Thread: {thread.thread_id}",
105
+ f"Title: {thread.title}",
106
+ f"Runs: {thread.run_count}",
107
+ f"Sources: {thread.source_count}",
108
+ f"Artifacts: {thread.artifact_count}",
109
+ f"Latest: {self._latest(thread)}",
110
+ f"Updated: {thread.updated_at.isoformat()}",
111
+ )
112
+ )
113
+ return RenderedResult(OutputDocument(kind="research.thread", data=data), human)
114
+
115
+ def thread_page(self, page: ResearchPortfolioPage) -> RenderedResult:
116
+ # One page of threads, plus the cursor that continues it when there is more.
117
+ rows: list[JsonValue] = [self._thread_fields(thread) for thread in page.threads]
118
+ data: dict[str, JsonValue] = {"threads": rows, "next_cursor": page.next_cursor}
119
+ return RenderedResult(
120
+ OutputDocument(kind="research.threads", data=data),
121
+ self._thread_page_text(page),
122
+ )
123
+
124
+ def _thread_page_text(self, page: ResearchPortfolioPage) -> str:
125
+ if not page.threads:
126
+ return "No research threads."
127
+ width = max(len(thread.thread_id) for thread in page.threads)
128
+ lines = [
129
+ f"{thread.thread_id.ljust(width)} {thread.run_count:>4} runs {thread.title}"
130
+ for thread in page.threads
131
+ ]
132
+ if page.next_cursor is not None:
133
+ lines.append("")
134
+ lines.append(f"More available: --cursor {page.next_cursor}")
135
+ return "\n".join(lines)
136
+
137
+ def _thread_fields(self, thread: ResearchThreadSummary) -> dict[str, JsonValue]:
138
+ # The row shape shared by the portfolio page and the single-thread read.
139
+ status = thread.latest_run_status
140
+ return {
141
+ "thread_id": thread.thread_id,
142
+ "title": thread.title,
143
+ "run_count": thread.run_count,
144
+ "source_count": thread.source_count,
145
+ "artifact_count": thread.artifact_count,
146
+ "latest_run_id": thread.latest_run_id,
147
+ "latest_run_status": status.value if status is not None else None,
148
+ "created_at": thread.created_at.isoformat(),
149
+ "updated_at": thread.updated_at.isoformat(),
150
+ }
151
+
152
+ def _latest(self, thread: ResearchThreadSummary) -> str:
153
+ # A thread whose runs were all deleted still reads, so both fields may be absent.
154
+ if thread.latest_run_id is None:
155
+ return "no runs"
156
+ status = thread.latest_run_status
157
+ return f"{thread.latest_run_id} ({status.value if status is not None else 'unknown'})"
@@ -0,0 +1,37 @@
1
+ """`vidbyte-cli research resume <run_id>` — continue a run that already settled badly.
2
+
3
+ The backend accepts a continuation only from `partial`, `failed`, or `credit_exhausted`, and
4
+ refuses every other state. That rule is enforced there rather than guessed at here: a status
5
+ read before the attempt would be a second request whose answer could already be stale.
6
+
7
+ There is no request body. A continuation replays the run's original prompt, so this command
8
+ takes no run options — passing a new prompt would silently mean starting different work.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import click
14
+
15
+ from ...lib.runtime.context import ApplicationContext
16
+ from ...types.research import IdempotencyKey
17
+ from .render import ResearchRenderer
18
+
19
+
20
+ class ResearchResumeCommand:
21
+ def register(self, parent: click.Group) -> None:
22
+ @parent.command(name="resume", help="Continue a partial, failed, or out-of-credit run")
23
+ @click.argument("run_id")
24
+ @click.option(
25
+ "--idempotency-key",
26
+ help="Reuse a key to retry a priced mutation without being charged twice.",
27
+ )
28
+ @click.pass_obj
29
+ def _run(context: ApplicationContext, run_id: str, idempotency_key: str | None) -> None:
30
+ self.execute(context, run_id, idempotency_key)
31
+
32
+ def execute(self, context: ApplicationContext, run_id: str, explicit_key: str | None) -> None:
33
+ # Validates the key before resolving credentials, then admits the continuation.
34
+ key = str(IdempotencyKey.create(explicit_key))
35
+ accepted = context.research_endpoints().continue_run(run_id, key)
36
+ rendered = ResearchRenderer().accepted(accepted, key)
37
+ context.output().result(rendered.document, rendered.human)