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,66 @@
1
+ """What this invocation's terminal can safely be asked to do.
2
+
3
+ Capabilities are data, detected once from injected streams and an injected environment.
4
+ Injecting both is what makes redirection, CI, embedding, and Windows consoles predictable
5
+ instead of depending on process globals. No escape sequence is ever written from here.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from collections.abc import Mapping
12
+ from dataclasses import dataclass
13
+ from typing import TextIO
14
+
15
+ from ..output.formats import ColorMode
16
+ from .streams import IOStreams
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class TerminalPolicy:
21
+ """Preferences and environment used for one capability decision."""
22
+
23
+ color: ColorMode = ColorMode.AUTO
24
+ no_input: bool = False
25
+ environment: Mapping[str, str] | None = None
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class TerminalCapabilities:
30
+ """Terminal features safe to use for one invocation."""
31
+
32
+ interactive: bool
33
+ color: bool
34
+ cursor: bool
35
+
36
+ @classmethod
37
+ def detect(cls, streams: IOStreams, policy: TerminalPolicy) -> TerminalCapabilities:
38
+ # Environment injection avoids hiding global reads inside renderers.
39
+ environment = os.environ if policy.environment is None else policy.environment
40
+ terminal_allowed = cls._terminal_control_allowed(cls._is_tty(streams.stderr), environment)
41
+ return cls(
42
+ interactive=cls._is_tty(streams.stdin) and not policy.no_input,
43
+ color=terminal_allowed and policy.color is not ColorMode.NEVER,
44
+ cursor=terminal_allowed,
45
+ )
46
+
47
+ @staticmethod
48
+ def _is_tty(stream: TextIO) -> bool:
49
+ # Redirected and in-memory streams may expose no usable isatty implementation.
50
+ isatty = getattr(stream, "isatty", None)
51
+ if not callable(isatty):
52
+ return False
53
+ try:
54
+ return bool(isatty())
55
+ except (OSError, ValueError):
56
+ return False
57
+
58
+ @staticmethod
59
+ def _terminal_control_allowed(stderr_tty: bool, environment: Mapping[str, str]) -> bool:
60
+ # Accessibility and shell safety outrank an explicit color request: even
61
+ # `--color always` cannot turn on control sequences here.
62
+ if not stderr_tty:
63
+ return False
64
+ if environment.get("TERM", "").lower() == "dumb":
65
+ return False
66
+ return "NO_COLOR" not in environment
@@ -0,0 +1,7 @@
1
+ """Public presentation contracts. Feature presenters stay private to their own slices."""
2
+
3
+ from .formats import ColorMode, OutputFormat
4
+ from .manager import OutputManager, OutputPolicy
5
+ from .models import OutputDocument
6
+
7
+ __all__ = ["ColorMode", "OutputDocument", "OutputFormat", "OutputManager", "OutputPolicy"]
@@ -0,0 +1,29 @@
1
+ """The root presentation vocabulary shared by flags, terminal detection, and rendering.
2
+
3
+ String enums keep Click choices, config values, and diagnostics on one set of spellings.
4
+ Nothing here inspects a TTY or serializes a document — `lib/io` owns capability detection
5
+ and `manager.py` owns stream policy.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from enum import StrEnum
11
+
12
+
13
+ class OutputFormat(StrEnum):
14
+ """Supported result serialization modes."""
15
+
16
+ HUMAN = "human"
17
+ JSON = "json"
18
+ JSONL = "jsonl"
19
+ NONE = "none"
20
+
21
+
22
+ class ColorMode(StrEnum):
23
+ """User preference for terminal color when the stream permits it."""
24
+
25
+ # ALWAYS is still subject to the accessibility rule: a redirected stream, TERM=dumb, or
26
+ # NO_COLOR disables terminal control regardless of what was requested.
27
+ AUTO = "auto"
28
+ ALWAYS = "always"
29
+ NEVER = "never"
@@ -0,0 +1,95 @@
1
+ """Where results, progress, warnings, and failures are allowed to go, for one invocation.
2
+
3
+ Two rules make this file worth existing. Stdout carries results only, so a shell pipeline
4
+ stays parseable while progress and diagnostics run on stderr. And a machine format never
5
+ silently degrades to human text — a consumer that asked for JSON gets JSON or an error.
6
+
7
+ Human error text repeats the agent-native description/trace/file_path that machine documents
8
+ carry, because agents shell out to the default format far more often than they pass --json.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass
14
+ from typing import TYPE_CHECKING, Protocol
15
+
16
+ from .formats import OutputFormat
17
+ from .models import OutputDocument
18
+
19
+ if TYPE_CHECKING:
20
+ from ..errors.cli_error import CliError
21
+ from ..io.terminal import TerminalCapabilities
22
+
23
+
24
+ class OutputStreams(Protocol):
25
+ """Minimum physical stream behavior required by output policy."""
26
+
27
+ def write_output(self, message: str) -> None: ...
28
+
29
+ def write_error(self, message: str) -> None: ...
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class OutputPolicy:
34
+ """Presentation choices used by one output manager."""
35
+
36
+ output_format: OutputFormat
37
+ terminal: TerminalCapabilities
38
+
39
+
40
+ class OutputManager:
41
+ """Invocation-owned presentation and stream policy."""
42
+
43
+ def __init__(self, streams: OutputStreams, policy: OutputPolicy) -> None:
44
+ self._streams = streams
45
+ self.format = policy.output_format
46
+ self.terminal = policy.terminal
47
+
48
+ def result(self, document: OutputDocument, human: str) -> None:
49
+ # JSON and JSONL both emit one record here; only JSONL may emit earlier transitions.
50
+ if self.format is OutputFormat.NONE:
51
+ return
52
+ if self.format is OutputFormat.HUMAN:
53
+ self._streams.write_output(human)
54
+ return
55
+ self._streams.write_output(self._serialize(document))
56
+
57
+ def transition(self, document: OutputDocument, human: str) -> None:
58
+ # A single-JSON consumer must see exactly one final stdout document.
59
+ if self.format is OutputFormat.JSONL:
60
+ self._streams.write_output(self._serialize(document))
61
+ elif self.format is OutputFormat.HUMAN:
62
+ self._streams.write_error(human)
63
+
64
+ def warning(self, message: str) -> None:
65
+ # Warnings stay visible in every format without corrupting structured stdout.
66
+ self._streams.write_error(f"Warning: {message}")
67
+
68
+ def diagnostic(self, message: str) -> None:
69
+ # Debug and verbose details are diagnostics, never command results.
70
+ self._streams.write_error(message)
71
+
72
+ def error(self, error: CliError) -> None:
73
+ # NONE suppresses ordinary output, not actionable failures.
74
+ if self.format in {OutputFormat.JSON, OutputFormat.JSONL}:
75
+ self._streams.write_error(self._serialize(OutputDocument.from_error(error)))
76
+ return
77
+ self._streams.write_error(self._human_error(error))
78
+
79
+ def _serialize(self, document: OutputDocument) -> str:
80
+ # Pydantic owns strict JSON encoding; failures propagate as internal software errors.
81
+ return document.model_dump_json(exclude_none=True)
82
+
83
+ def _human_error(self, error: CliError) -> str:
84
+ # Text labels preserve meaning when color and cursor control are unavailable.
85
+ lines = [
86
+ f"Error [{error.code.value}]: {error.message}",
87
+ f"Details: {error.description}",
88
+ f"Trace: {error.trace}",
89
+ f"Source: {error.file_path}",
90
+ ]
91
+ if error.hint is not None:
92
+ lines.append(f"Hint: {error.hint}")
93
+ if error.request_id is not None:
94
+ lines.append(f"Request ID: {error.request_id}")
95
+ return "\n".join(lines)
@@ -0,0 +1,49 @@
1
+ """The versioned envelope every machine-readable record travels in.
2
+
3
+ `schema_version` and `kind` are the compatibility contract: a consumer branches on them and
4
+ finds its own keys under `data`. Fields may be added, never removed or repurposed.
5
+
6
+ `CliError` is imported for typing only. A runtime import would cycle, because the errors
7
+ package imports the output manager to render failures.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import TYPE_CHECKING, Literal
13
+
14
+ from pydantic import BaseModel, ConfigDict, Field, JsonValue
15
+
16
+ if TYPE_CHECKING:
17
+ from ..errors.cli_error import CliError
18
+
19
+
20
+ class OutputDocument(BaseModel):
21
+ """One stable result, transition, or error record."""
22
+
23
+ model_config = ConfigDict(extra="forbid", frozen=True)
24
+
25
+ schema_version: Literal[1] = 1
26
+ kind: str = Field(min_length=1, pattern=r"^[a-z][a-z0-9_.-]*$")
27
+ data: dict[str, JsonValue] = Field(default_factory=dict)
28
+
29
+ @classmethod
30
+ def from_error(cls, error: CliError) -> OutputDocument:
31
+ # Only explicitly safe fields cross this boundary; the private cause never does.
32
+ # Optional fields are omitted rather than emitted as null so a shell consumer can
33
+ # tell absent metadata from a deliberate value.
34
+ data: dict[str, JsonValue] = {
35
+ "code": error.code_value,
36
+ "message": error.message,
37
+ "description": error.description,
38
+ "trace": error.trace,
39
+ "file_path": error.file_path,
40
+ "exit_code": error.exit_code,
41
+ "retryable": error.retryable,
42
+ }
43
+ if error.hint is not None:
44
+ data["hint"] = error.hint
45
+ if error.request_id is not None:
46
+ data["request_id"] = error.request_id
47
+ if error.remediation is not None:
48
+ data["remediation"] = error.remediation
49
+ return cls(kind="error", data=data)
@@ -0,0 +1,9 @@
1
+ """Runtime contracts that are safe to import from the package root.
2
+
3
+ CliApplication and ApplicationContext are intentionally NOT re-exported: importing
4
+ `vidbyte_cli` must not pull in click commands or HTTP models.
5
+ """
6
+
7
+ from .version import VersionProvider, current_version
8
+
9
+ __all__ = ["VersionProvider", "current_version"]
@@ -0,0 +1,134 @@
1
+ """One CLI invocation: read root policy, build the command tree, dispatch.
2
+
3
+ This is the composition root. `run()` returns a status instead of calling `sys.exit`, so the
4
+ application can be embedded in another Python process, and every failure — Click's included —
5
+ leaves through the single ErrorHandler boundary rather than being printed here.
6
+
7
+ Root options are inspected before Click parses, because `--format` and `--debug` decide how a
8
+ parse failure is rendered and Click's own errors leave through that same boundary. They are
9
+ overrides, not the final answer: the resolver layers them over the environment and the
10
+ selected profile. An invocation that exits before running a command skips that resolution
11
+ entirely, so `--help` never reads a file.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import sys
17
+ from collections.abc import Sequence
18
+ from contextlib import redirect_stderr, redirect_stdout
19
+
20
+ import click
21
+
22
+ from ...commands import register_all_commands
23
+ from ..config import ConfigOverrides
24
+ from ..errors.failures import ConflictingOutputFormat
25
+ from ..io import IOStreams
26
+ from ..output.formats import ColorMode, OutputFormat
27
+ from .context import ApplicationContext, InvocationOptions
28
+ from .options import RootOptionInspector, RootOptionValues
29
+ from .version import current_version
30
+
31
+
32
+ class CliApplication:
33
+ """Runs one command-line invocation and returns its process status."""
34
+
35
+ def __init__(self, context: ApplicationContext | None = None) -> None:
36
+ # A caller may inject a complete graph; otherwise bind the process streams once.
37
+ self._context = context or ApplicationContext(IOStreams.system())
38
+ self._streams = self._context.streams
39
+
40
+ def run(self, argv: Sequence[str] | None = None) -> int:
41
+ # Build, configure, and dispatch inside one trap so every exit path is a return code.
42
+ arguments = list(sys.argv if argv is None else argv)
43
+ try:
44
+ program = self._build_program()
45
+ register_all_commands(program)
46
+ self._preconfigure(arguments)
47
+ return self._invoke(program, arguments)
48
+ except (Exception, KeyboardInterrupt) as error: # noqa: BLE001 - the process boundary.
49
+ return self._context.error_handler().handle(error)
50
+ finally:
51
+ # After the handler has rendered, so a failure still reports before teardown.
52
+ self._context.close()
53
+
54
+ def _build_program(self) -> click.Group:
55
+ # The root group; its callback publishes the invocation context to every command.
56
+ application_context = self._context
57
+
58
+ @click.group(
59
+ name="vidbyte-cli",
60
+ help="Vidbyte CLI: hosted research and local agent runtimes",
61
+ )
62
+ @click.version_option(current_version(), "--version")
63
+ @click.option(
64
+ "--format",
65
+ "output_format",
66
+ type=click.Choice([item.value for item in OutputFormat], case_sensitive=False),
67
+ help="Select human, one-document, streaming, or suppressed result output.",
68
+ )
69
+ @click.option("--json", "as_json", is_flag=True, help="Alias for --format json.")
70
+ @click.option("--profile", type=str, help="Use a named configuration profile.")
71
+ @click.option("--no-input", is_flag=True, help="Never prompt for interactive input.")
72
+ @click.option(
73
+ "--color",
74
+ type=click.Choice([item.value for item in ColorMode], case_sensitive=False),
75
+ # No Click default: an unset flag must stay distinguishable from an explicit
76
+ # `--color auto`, or the option would outrank the stored profile every time.
77
+ default=None,
78
+ )
79
+ @click.option("--debug", is_flag=True, help="Show redacted internal stack frames.")
80
+ @click.pass_context
81
+ def program(click_context: click.Context, /, **values: object) -> None:
82
+ # Rebind root policy from Click, the authoritative parser, then publish context.
83
+ self._configure_context(RootOptionValues.from_click(values))
84
+ click_context.obj = application_context
85
+
86
+ return program
87
+
88
+ def _preconfigure(self, argv: Sequence[str]) -> None:
89
+ # Invalid root syntax stays service-free and is later rendered by Click.
90
+ inspection = RootOptionInspector(argv).inspect()
91
+ if inspection is None:
92
+ return
93
+ # Help and version exit before any command, and resolving configuration would read
94
+ # the filesystem on their behalf.
95
+ if not inspection.exits_before_command:
96
+ self._configure_context(inspection.values)
97
+
98
+ def _invoke(self, program: click.Group, argv: Sequence[str]) -> int:
99
+ # Click writes to the process streams, so bind them to the invocation-owned channels.
100
+ # standalone_mode=False hands click's exceptions back to the trap in run().
101
+ with redirect_stdout(self._streams.stdout), redirect_stderr(self._streams.stderr):
102
+ program.main(args=list(argv[1:]), prog_name="vidbyte-cli", standalone_mode=False)
103
+ return 0
104
+
105
+ def _configure_context(self, values: RootOptionValues) -> None:
106
+ # Root options are the highest-precedence layer, not the whole answer: the resolver
107
+ # fills every unset field from the environment, the profile, then built-ins.
108
+ resolved = self._context.config_resolver().resolve(
109
+ ConfigOverrides(
110
+ profile=values.profile,
111
+ output_format=self._resolve_output_format(values.output_format, values.as_json),
112
+ color=ColorMode(values.color) if values.color is not None else None,
113
+ )
114
+ )
115
+ self._context.configure(
116
+ InvocationOptions(
117
+ output_format=resolved.output_format,
118
+ profile=resolved.profile,
119
+ api_url=resolved.api_url,
120
+ request_timeout_seconds=resolved.request_timeout_seconds,
121
+ no_input=values.no_input,
122
+ color=resolved.color,
123
+ debug=values.debug,
124
+ ),
125
+ resolved,
126
+ )
127
+
128
+ def _resolve_output_format(self, value: str | None, as_json: bool) -> OutputFormat | None:
129
+ # --json is an alias, not an independent mode; duplicate JSON intent is harmless.
130
+ if as_json and value not in {None, OutputFormat.JSON.value}:
131
+ raise ConflictingOutputFormat()
132
+ if as_json:
133
+ return OutputFormat.JSON
134
+ return OutputFormat(value) if value is not None else None
@@ -0,0 +1,250 @@
1
+ """The dependency graph and presentation policy owned by exactly one invocation.
2
+
3
+ Commands reach services through here instead of module globals. Everything expensive is
4
+ built lazily, so `--help` and `--version` never touch the keyring, the config file, or the
5
+ network — a keyring lookup can pop an OS unlock dialog, which no help invocation may do.
6
+
7
+ Root options arrive through `configure()` before any command service exists, because output
8
+ policy has to be settled before the first byte is written.
9
+
10
+ The environment is injected rather than read at each use, so a test or an embedding process
11
+ gets one place to control credential and configuration discovery.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ from collections.abc import Callable, Mapping
18
+ from dataclasses import dataclass
19
+
20
+ from ...types.provider import Provider
21
+ from ..api.client import ApiClient
22
+ from ..api.endpoints.billing import BillingEndpoints
23
+ from ..api.endpoints.research import ResearchEndpoints
24
+ from ..api.endpoints.runtime import RuntimeEndpoints
25
+ from ..auth import (
26
+ ApiCredentialVerifier,
27
+ CredentialResolver,
28
+ CredentialStore,
29
+ CredentialVerifier,
30
+ )
31
+ from ..auth.credentials import Credentials
32
+ from ..auth.provider_credentials import ProviderCredentials
33
+ from ..auth.provider_resolver import ProviderResolver
34
+ from ..auth.provider_store import ProviderCredentialStore
35
+ from ..auth.provider_verifier import ProviderVerifier, verifier_for_provider
36
+ from ..config import ConfigResolver, ConfigStore, ResolvedConfig, VidbytePaths
37
+ from ..config.migration import StateMigration
38
+ from ..config.models import DEFAULT_API_URL, DEFAULT_PROFILE
39
+ from ..errors.failures import AuthenticationRequired, ProviderAuthenticationRequired
40
+ from ..errors.handler import ErrorHandler
41
+ from ..io import IOStreams
42
+ from ..io.terminal import TerminalCapabilities, TerminalPolicy
43
+ from ..output.formats import ColorMode, OutputFormat
44
+ from ..output.manager import OutputManager, OutputPolicy
45
+ from ..runtime_primitives import RuntimeExecutor, RuntimeHostRegistry, RuntimeLaunchPlanner
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class InvocationOptions:
50
+ """Root presentation, profile, and API policy resolved before command execution."""
51
+
52
+ output_format: OutputFormat = OutputFormat.HUMAN
53
+ profile: str = DEFAULT_PROFILE
54
+ api_url: str = DEFAULT_API_URL
55
+ request_timeout_seconds: float = 30.0
56
+ no_input: bool = False
57
+ color: ColorMode = ColorMode.AUTO
58
+ debug: bool = False
59
+
60
+
61
+ class ApplicationContext:
62
+ """Invocation-scoped services, shared with commands via click's context object."""
63
+
64
+ def __init__(
65
+ self,
66
+ streams: IOStreams,
67
+ *,
68
+ environment: Mapping[str, str] | None = None,
69
+ paths: VidbytePaths | None = None,
70
+ verifier_factory: Callable[[], CredentialVerifier] | None = None,
71
+ ) -> None:
72
+ # Construction stays side-effect free; factories run on first request only.
73
+ self.streams = streams
74
+ self.environment = dict(os.environ if environment is None else environment)
75
+ self.options = InvocationOptions()
76
+ self._resolved_config: ResolvedConfig | None = None
77
+ self._paths = paths
78
+ self._config_store: ConfigStore | None = None
79
+ self._config_resolver: ConfigResolver | None = None
80
+ self._credential_store: CredentialStore | None = None
81
+ self._credential_resolver: CredentialResolver | None = None
82
+ self._migration: StateMigration | None = None
83
+ self._verifier_factory = verifier_factory or ApiCredentialVerifier
84
+ self._verifier: CredentialVerifier | None = None
85
+ self._output = self._build_output()
86
+ self._errors = ErrorHandler(self._output)
87
+ self._api_client: ApiClient | None = None
88
+ self._research_endpoints: ResearchEndpoints | None = None
89
+ self._runtime_endpoints: RuntimeEndpoints | None = None
90
+ self._billing_endpoints: BillingEndpoints | None = None
91
+ self._runtime_hosts: RuntimeHostRegistry | None = None
92
+ self._runtime_launch_planner: RuntimeLaunchPlanner | None = None
93
+ self._runtime_executor: RuntimeExecutor | None = None
94
+ self._provider_store: ProviderCredentialStore | None = None
95
+ self._provider_resolver: ProviderResolver | None = None
96
+
97
+ def configure(self, options: InvocationOptions, config: ResolvedConfig) -> None:
98
+ # Root options are read twice — once by the pre-scan, once by Click — so an unchanged
99
+ # policy must not rebuild the OutputManager and leave two disagreeing about a stream.
100
+ if options == self.options and config == self._resolved_config:
101
+ return
102
+ self.options = options
103
+ self._resolved_config = config
104
+ self._output = self._build_output()
105
+ self._errors = ErrorHandler(self._output, debug=options.debug)
106
+
107
+ def resolved_config(self) -> ResolvedConfig:
108
+ # A command reached for configuration without going through the root callback,
109
+ # which happens whenever Click short-circuits; resolve it on demand.
110
+ if self._resolved_config is None:
111
+ self._resolved_config = self.config_resolver().resolve()
112
+ return self._resolved_config
113
+
114
+ def paths(self) -> VidbytePaths:
115
+ if self._paths is None:
116
+ self._paths = VidbytePaths.default()
117
+ return self._paths
118
+
119
+ def config_store(self) -> ConfigStore:
120
+ if self._config_store is None:
121
+ self._config_store = ConfigStore(self.paths())
122
+ return self._config_store
123
+
124
+ def config_resolver(self) -> ConfigResolver:
125
+ if self._config_resolver is None:
126
+ self._config_resolver = ConfigResolver(self.config_store(), self.environment)
127
+ return self._config_resolver
128
+
129
+ def credential_store(self) -> CredentialStore:
130
+ if self._credential_store is None:
131
+ self._credential_store = CredentialStore(paths=self.paths())
132
+ return self._credential_store
133
+
134
+ def credential_resolver(self) -> CredentialResolver:
135
+ if self._credential_resolver is None:
136
+ self._credential_resolver = CredentialResolver(
137
+ self.credential_store(),
138
+ self.environment,
139
+ )
140
+ return self._credential_resolver
141
+
142
+ def migration(self) -> StateMigration:
143
+ if self._migration is None:
144
+ self._migration = StateMigration(
145
+ self.paths(),
146
+ self.config_store(),
147
+ self.credential_store().keyring,
148
+ )
149
+ return self._migration
150
+
151
+ def credential_verifier(self) -> CredentialVerifier:
152
+ if self._verifier is None:
153
+ self._verifier = self._verifier_factory()
154
+ return self._verifier
155
+
156
+ def require_credentials(self) -> Credentials:
157
+ # The API key for this invocation's profile and host, or a clean failure if none.
158
+ config = self.resolved_config()
159
+ resolved = self.credential_resolver().resolve(config.profile, config.api_url)
160
+ if resolved is None:
161
+ raise AuthenticationRequired()
162
+ return resolved.credentials
163
+
164
+ def api_client(self) -> ApiClient:
165
+ # One client per invocation, so a polling command reuses a single connection pool.
166
+ if self._api_client is None:
167
+ self._api_client = ApiClient(self.resolved_config(), self.require_credentials())
168
+ return self._api_client
169
+
170
+ def research_endpoints(self) -> ResearchEndpoints:
171
+ # The authenticated research route group; requires a logged-in user.
172
+ if self._research_endpoints is None:
173
+ self._research_endpoints = ResearchEndpoints(self.api_client())
174
+ return self._research_endpoints
175
+
176
+ def runtime_endpoints(self) -> RuntimeEndpoints:
177
+ # Lazily binds runtime HTTP operations to this invocation's authenticated client.
178
+ if self._runtime_endpoints is None:
179
+ self._runtime_endpoints = RuntimeEndpoints(self.api_client())
180
+ return self._runtime_endpoints
181
+
182
+ def billing_endpoints(self) -> BillingEndpoints:
183
+ # Lazily binds billing HTTP operations to this invocation's API client.
184
+ if self._billing_endpoints is None:
185
+ self._billing_endpoints = BillingEndpoints(self.api_client())
186
+ return self._billing_endpoints
187
+
188
+ def runtime_hosts(self) -> RuntimeHostRegistry:
189
+ # Shares one PATH discovery policy between doctor and launch planning.
190
+ if self._runtime_hosts is None:
191
+ self._runtime_hosts = RuntimeHostRegistry()
192
+ return self._runtime_hosts
193
+
194
+ def runtime_launch_planner(self) -> RuntimeLaunchPlanner:
195
+ # Builds local launch plans without credentials, network calls, or subprocesses.
196
+ if self._runtime_launch_planner is None:
197
+ self._runtime_launch_planner = RuntimeLaunchPlanner(self.runtime_hosts())
198
+ return self._runtime_launch_planner
199
+
200
+ def runtime_executor(self) -> RuntimeExecutor:
201
+ # Returns the inert boundary a later runtime implementation will replace.
202
+ if self._runtime_executor is None:
203
+ self._runtime_executor = RuntimeExecutor()
204
+ return self._runtime_executor
205
+
206
+ def close(self) -> None:
207
+ # Releases network resources this invocation opened; a help path opened none.
208
+ if self._api_client is not None:
209
+ self._api_client.close()
210
+
211
+ def output(self) -> OutputManager:
212
+ # Callers share one policy object so stdout cardinality stays enforceable.
213
+ return self._output
214
+
215
+ def provider_store(self) -> ProviderCredentialStore:
216
+ # Lazily created so --help never opens the keyring.
217
+ if self._provider_store is None:
218
+ self._provider_store = ProviderCredentialStore(paths=self.paths())
219
+ return self._provider_store
220
+
221
+ def provider_resolver(self) -> ProviderResolver:
222
+ # Env > keyring > file, scoped by profile+provider.
223
+ if self._provider_resolver is None:
224
+ self._provider_resolver = ProviderResolver(self.provider_store(), self.environment)
225
+ return self._provider_resolver
226
+
227
+ def provider_verifier(self, provider: Provider) -> ProviderVerifier:
228
+ # Factory keeps command from branching on provider.
229
+ return verifier_for_provider(provider)
230
+
231
+ def require_provider_credentials(self, provider: Provider) -> ProviderCredentials:
232
+ # Provider key for this profile, or a typed failure before any network call.
233
+ config = self.resolved_config()
234
+ resolved = self.provider_resolver().resolve(config.profile, provider)
235
+ if resolved is None:
236
+ raise ProviderAuthenticationRequired(provider.value)
237
+ return resolved.credentials
238
+
239
+ def error_handler(self) -> ErrorHandler:
240
+ return self._errors
241
+
242
+ def _build_output(self) -> OutputManager:
243
+ # Terminal detection reruns whenever color or no-input preferences change.
244
+ terminal_policy = TerminalPolicy(
245
+ self.options.color,
246
+ self.options.no_input,
247
+ self.environment,
248
+ )
249
+ terminal = TerminalCapabilities.detect(self.streams, terminal_policy)
250
+ return OutputManager(self.streams, OutputPolicy(self.options.output_format, terminal))