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,72 @@
1
+ """The single boundary where an exception becomes safe output and a process status.
2
+
3
+ One match statement classifies everything that can reach `CliApplication.run`, so the set of
4
+ exception kinds the CLI understands is readable in one place. Anything unmatched is a CLI
5
+ defect, not a user mistake, and is reported without quoting the exception value — a foreign
6
+ exception string may hold a credential, a prompt, or a backend response body.
7
+
8
+ Debug mode adds stack frames only. Never locals, never exception values, never causes.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import traceback
14
+
15
+ import click
16
+
17
+ from ..output.manager import OutputManager
18
+ from .cli_error import CliError
19
+ from .codes import ExitCode
20
+ from .failures import InvalidCommandUsage, OperationInterrupted, UnexpectedInternalError
21
+
22
+
23
+ class ErrorHandler:
24
+ """Central conversion from boundary exceptions to safe CLI failures."""
25
+
26
+ def __init__(self, output: OutputManager, debug: bool = False) -> None:
27
+ self._output = output
28
+ self._debug = debug
29
+
30
+ def handle(self, error: BaseException) -> int:
31
+ # UsageError precedes ClickException because it is the more specific subclass.
32
+ match error:
33
+ case BrokenPipeError():
34
+ # A closed downstream pipe is a normal shell outcome, not a failure.
35
+ return int(ExitCode.SUCCESS)
36
+ case CliError():
37
+ return self._report(error, error)
38
+ case click.UsageError():
39
+ context = error.ctx
40
+ path = context.command_path if context is not None else "vidbyte-cli"
41
+ return self._report(
42
+ InvalidCommandUsage(error.format_message(), path, error.exit_code, error),
43
+ error,
44
+ )
45
+ case click.ClickException():
46
+ return self._report(
47
+ InvalidCommandUsage(
48
+ error.format_message(), "vidbyte-cli", error.exit_code, error
49
+ ),
50
+ error,
51
+ )
52
+ case click.exceptions.Abort() | KeyboardInterrupt():
53
+ return self._report(OperationInterrupted(), error)
54
+ case _:
55
+ cause = error if isinstance(error, Exception) else None
56
+ return self._report(UnexpectedInternalError(cause), error)
57
+
58
+ def _report(self, mapped: CliError, original: BaseException) -> int:
59
+ # Render once, then optionally add frames from whichever exception carries them.
60
+ self._output.error(mapped)
61
+ if self._debug:
62
+ self._render_redacted_trace(mapped.cause or original)
63
+ return mapped.exit_code
64
+
65
+ def _render_redacted_trace(self, error: BaseException) -> None:
66
+ # Format stack frames only; exception values and chained causes may hold secrets.
67
+ self._output.diagnostic("Debug traceback (exception values redacted):")
68
+ if error.__traceback__ is None:
69
+ self._output.diagnostic(f" {type(error).__module__}.{type(error).__qualname__}")
70
+ return
71
+ for line in traceback.format_tb(error.__traceback__):
72
+ self._output.diagnostic(line.rstrip())
@@ -0,0 +1,6 @@
1
+ """Public local-file contract: stores rooted at one directory, product-neutral."""
2
+
3
+ from .documents import LocalDocumentStore
4
+ from .store import LocalFileStore
5
+
6
+ __all__ = ["LocalDocumentStore", "LocalFileStore"]
@@ -0,0 +1,94 @@
1
+ """Versioned JSON documents validated by a caller-supplied pydantic model.
2
+
3
+ A command that keeps small structured records on disk — catalogs, indexes, per-item memory —
4
+ reads and writes them here, so bounding, parsing, model validation, containment of linked
5
+ names, and deterministic serialization are decided once. Bytes still move through
6
+ `LocalFileStore`; which model a document uses and what its fields mean stay with the caller.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from pathlib import Path
13
+ from typing import TypeVar
14
+
15
+ from pydantic import BaseModel, ValidationError
16
+
17
+ from ..errors.failures import LocalDocumentInvalid, LocalFileReadFailed, LocalFileWriteFailed
18
+ from .store import LocalFileStore
19
+
20
+ DocumentT = TypeVar("DocumentT", bound=BaseModel)
21
+
22
+ DEFAULT_MAX_DOCUMENT_BYTES = 1_000_000
23
+
24
+
25
+ class LocalDocumentStore:
26
+ """Reads, validates, atomically writes, and removes JSON documents under one root."""
27
+
28
+ def __init__(self, root: Path, *, max_bytes: int = DEFAULT_MAX_DOCUMENT_BYTES) -> None:
29
+ # The byte cap is per store so a caller holding tiny indexes can refuse a runaway file
30
+ # before it is decoded, while the file store underneath keeps paths absolute.
31
+ self._files = LocalFileStore(root)
32
+ self._max_bytes = max_bytes
33
+
34
+ @property
35
+ def root(self) -> Path:
36
+ # The absolute directory every document name resolves against.
37
+ return self._files.root
38
+
39
+ def path_of(self, name: str | Path) -> Path:
40
+ # Names often come from another document (a catalog linking to a record), so a name
41
+ # that is absolute, climbs with `..`, or resolves outside the root through a link is
42
+ # refused rather than trusted.
43
+ relative = Path(name)
44
+ if relative.is_absolute() or ".." in relative.parts:
45
+ raise LocalDocumentInvalid(relative.name, "its name leaves the document directory")
46
+ candidate = self._files.path_of(relative).resolve()
47
+ if not candidate.is_relative_to(self.root):
48
+ raise LocalDocumentInvalid(relative.name, "its name leaves the document directory")
49
+ return candidate
50
+
51
+ def exists(self, name: str | Path) -> bool:
52
+ # Presence of a regular file or of a link at that name, since either would be replaced.
53
+ path = self.path_of(name)
54
+ return path.is_file() or path.is_symlink()
55
+
56
+ def read(self, name: str | Path, model: type[DocumentT]) -> DocumentT | None:
57
+ # None means absent and nothing else. A present document that is oversized, not JSON,
58
+ # or not an instance of the model raises, so broken state is never read as empty state.
59
+ path = self.path_of(name)
60
+ try:
61
+ size = path.stat().st_size
62
+ except FileNotFoundError:
63
+ return None
64
+ except OSError as error:
65
+ raise LocalFileReadFailed(path.name, LocalFileStore.reason_for(error), error) from error
66
+ if size > self._max_bytes:
67
+ raise LocalDocumentInvalid(path.name, f"it exceeds {self._max_bytes} bytes")
68
+ parsed = self._files.read_json(path)
69
+ if parsed is None:
70
+ return None
71
+ try:
72
+ return model.model_validate(parsed)
73
+ except ValidationError as error:
74
+ raise LocalDocumentInvalid(
75
+ path.name, f"it does not match the {model.__name__} schema", error
76
+ ) from error
77
+
78
+ def write(self, name: str | Path, document: BaseModel) -> Path:
79
+ # Sorted, indented, newline-terminated output keeps documents diffable by hand and
80
+ # byte-stable across runs; the file store replaces the target atomically.
81
+ path = self.path_of(name)
82
+ body = json.dumps(document.model_dump(mode="json"), indent=2, sort_keys=True) + "\n"
83
+ return self._files.write_text(path, body)
84
+
85
+ def remove(self, name: str | Path) -> None:
86
+ # Used to roll back a document written earlier in the same mutation; an already absent
87
+ # document is the desired end state, not a failure.
88
+ path = self.path_of(name)
89
+ try:
90
+ path.unlink(missing_ok=True)
91
+ except OSError as error:
92
+ raise LocalFileWriteFailed(
93
+ path.name, LocalFileStore.reason_for(error), error
94
+ ) from error
@@ -0,0 +1,156 @@
1
+ """Product-neutral file I/O behind one store rooted at one directory.
2
+
3
+ Any command or runtime primitive that keeps local files — checkpoints, logs, reports,
4
+ exported documents — reads and writes them through this class, so atomicity, encoding, and
5
+ path resolution are decided once instead of at each call site. It knows paths and bytes only:
6
+ which file a record belongs in, and what its contents mean, is always the caller's decision.
7
+ Owner-only CLI state such as config and credentials goes through `lib/config/atomic.py`.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import errno
13
+ import json
14
+ import os
15
+ import shutil
16
+ import tempfile
17
+ from pathlib import Path
18
+
19
+ from ..errors.failures import LocalFileReadFailed, LocalFileWriteFailed
20
+
21
+
22
+ class LocalFileStore:
23
+ """Resolves, reads, and atomically writes files under one root directory."""
24
+
25
+ def __init__(self, root: Path) -> None:
26
+ # Resolving once at construction is what makes every path this store returns absolute,
27
+ # so a file a result reports stays addressable from another working directory.
28
+ self._root = root.expanduser().resolve()
29
+
30
+ @property
31
+ def root(self) -> Path:
32
+ # The absolute directory every relative name in this store resolves against.
33
+ return self._root
34
+
35
+ def path_of(self, name: str | Path) -> Path:
36
+ # One join site. An absolute name passes through unchanged, which is how a caller
37
+ # writes a destination it was handed without building a second store for it.
38
+ return self._root / name
39
+
40
+ def exists(self, name: str | Path) -> bool:
41
+ # Presence check for a regular file; a directory of the same name is not a file.
42
+ return self.path_of(name).is_file()
43
+
44
+ def subdirectories(self) -> tuple[Path, ...]:
45
+ # Sorted so a listing is stable across runs and filesystems. An absent root holds
46
+ # nothing, while a root that exists but cannot be scanned is a failure worth naming.
47
+ if not self._root.is_dir():
48
+ return ()
49
+ try:
50
+ return tuple(sorted(child for child in self._root.iterdir() if child.is_dir()))
51
+ except OSError as error:
52
+ raise LocalFileReadFailed(self._root.name, self.reason_for(error), error) from error
53
+
54
+ def read_text(self, name: str | Path) -> str | None:
55
+ # None means absent and nothing else; a file that exists but cannot be read or
56
+ # decoded raises, so a caller never mistakes a permission problem for a missing file.
57
+ path = self.path_of(name)
58
+ try:
59
+ return path.read_text(encoding="utf-8")
60
+ except FileNotFoundError:
61
+ return None
62
+ except (OSError, UnicodeError) as error:
63
+ raise LocalFileReadFailed(path.name, self.reason_for(error), error) from error
64
+
65
+ def read_json(self, name: str | Path) -> object | None:
66
+ # Same absent-versus-broken contract as read_text, with invalid JSON counted as broken.
67
+ body = self.read_text(name)
68
+ if body is None:
69
+ return None
70
+ try:
71
+ parsed: object = json.loads(body)
72
+ except ValueError as error:
73
+ path = self.path_of(name)
74
+ raise LocalFileReadFailed(path.name, self.reason_for(error), error) from error
75
+ return parsed
76
+
77
+ def write_json(self, name: str | Path, payload: object) -> Path:
78
+ # Serializes first so a malformed payload fails before the target file is touched.
79
+ return self.write_text(name, json.dumps(payload))
80
+
81
+ def write_text(self, name: str | Path, body: str) -> Path:
82
+ # Writes through a temp sibling plus os.replace, so a reader never sees a partial file
83
+ # and a crash mid-write leaves the previous version intact rather than a truncated one.
84
+ target = self.path_of(name)
85
+ temporary: str | None = None
86
+ try:
87
+ target.parent.mkdir(parents=True, exist_ok=True)
88
+ handle, temporary = tempfile.mkstemp(
89
+ dir=str(target.parent), prefix=f".{target.name}.", suffix=".tmp"
90
+ )
91
+ with os.fdopen(handle, "w", encoding="utf-8") as stream:
92
+ stream.write(body)
93
+ os.replace(temporary, target)
94
+ return target
95
+ except (OSError, UnicodeError) as error:
96
+ self._discard(temporary)
97
+ raise LocalFileWriteFailed(target.name, self.reason_for(error), error) from error
98
+
99
+ def append_line(self, name: str | Path, body: str) -> Path:
100
+ # Append is deliberately not atomic: an append-only log is read by outside tools that
101
+ # tail it, and rewriting it per record would break every reader holding an offset.
102
+ path = self.path_of(name)
103
+ try:
104
+ path.parent.mkdir(parents=True, exist_ok=True)
105
+ with path.open("a", encoding="utf-8") as stream:
106
+ stream.write(f"{body}\n")
107
+ except (OSError, UnicodeError) as error:
108
+ raise LocalFileWriteFailed(path.name, self.reason_for(error), error) from error
109
+ return path
110
+
111
+ def copy_into(self, name: str | Path, destination: Path) -> Path:
112
+ # Duplicates rather than moves or links, so the source stays readable and unchanged
113
+ # whatever the copy goes on to become.
114
+ try:
115
+ destination.parent.mkdir(parents=True, exist_ok=True)
116
+ shutil.copyfile(self.path_of(name), destination)
117
+ except OSError as error:
118
+ raise LocalFileWriteFailed(destination.name, self.reason_for(error), error) from error
119
+ return destination
120
+
121
+ @staticmethod
122
+ def reason_for(error: BaseException) -> str:
123
+ # A fixed category rather than the operating system's own message, so no absolute
124
+ # path or platform wording reaches an error, and the category alone names the repair.
125
+ # UnicodeError precedes ValueError because it is the more specific subclass.
126
+ match error:
127
+ case FileNotFoundError():
128
+ return "the file or one of its parent directories does not exist"
129
+ case PermissionError():
130
+ return "permission was denied"
131
+ case IsADirectoryError():
132
+ return "the path names a directory, not a file"
133
+ case NotADirectoryError() | FileExistsError():
134
+ return "a parent of the path is a file, not a directory"
135
+ case UnicodeError():
136
+ return "the content is not valid UTF-8 text"
137
+ case ValueError():
138
+ return "the content is not valid JSON"
139
+ case OSError() if error.errno == errno.ENOSPC:
140
+ return "the volume is full"
141
+ case OSError() if error.errno == errno.EROFS:
142
+ return "the volume is read-only"
143
+ case RuntimeError():
144
+ return "the path could not be resolved, such as an unexpandable ~ or a link loop"
145
+ case _:
146
+ return "the operating system refused the operation"
147
+
148
+ @staticmethod
149
+ def _discard(temporary: str | None) -> None:
150
+ # Only this call's own temp sibling is ever removed, never the destination.
151
+ if temporary is None:
152
+ return
153
+ try:
154
+ os.unlink(temporary)
155
+ except OSError:
156
+ pass
@@ -0,0 +1,16 @@
1
+ """Public process-I/O contracts: streams, terminal capabilities, prompts, and attachments."""
2
+
3
+ from .attachments import AttachmentResolver
4
+ from .codex_attachments import CodexAttachmentInputBuilder
5
+ from .prompt import PromptInputResolver
6
+ from .streams import IOStreams
7
+ from .terminal import TerminalCapabilities, TerminalPolicy
8
+
9
+ __all__ = [
10
+ "AttachmentResolver",
11
+ "CodexAttachmentInputBuilder",
12
+ "IOStreams",
13
+ "PromptInputResolver",
14
+ "TerminalCapabilities",
15
+ "TerminalPolicy",
16
+ ]
@@ -0,0 +1,129 @@
1
+ """Resolves explicit local agent attachments into bounded immutable snapshots.
2
+
3
+ This module owns generic filesystem access for future agent commands. It does not attach an
4
+ option to a command, launch a provider, or expand directories and globs.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ from pathlib import Path
11
+
12
+ from ...types.attachments import AgentAttachment, AttachmentBundle, AttachmentKind
13
+ from ..constants.runtime import AttachmentImageSuffix, AttachmentLimit
14
+ from ..errors.failures import (
15
+ AttachmentDirectory,
16
+ AttachmentDuplicate,
17
+ AttachmentEmpty,
18
+ AttachmentFileNotFound,
19
+ AttachmentInputInvalid,
20
+ AttachmentLimitExceeded,
21
+ AttachmentTooLarge,
22
+ AttachmentUnreadable,
23
+ AttachmentUnsupported,
24
+ )
25
+
26
+
27
+ class AttachmentResolver:
28
+ """Reads an ordered, explicit set of files into one validated attachment bundle."""
29
+
30
+ def resolve(self, paths: tuple[Path, ...]) -> AttachmentBundle:
31
+ # Validates collection shape and limits before touching any caller path.
32
+ self._validate_input(paths)
33
+ if not paths:
34
+ return AttachmentBundle()
35
+ items: list[AgentAttachment] = []
36
+ seen: set[Path] = set()
37
+ total_bytes = 0
38
+ for path in paths:
39
+ item = self._resolve_one(path, seen, total_bytes)
40
+ total_bytes += item.size_bytes
41
+ if total_bytes > int(AttachmentLimit.MAX_TOTAL_BYTES):
42
+ raise AttachmentLimitExceeded()
43
+ items.append(item)
44
+ return AttachmentBundle(items=tuple(items), total_bytes=total_bytes)
45
+
46
+ def _validate_input(self, paths: tuple[Path, ...]) -> None:
47
+ # Rejects values outside Click's tuple-of-Path contract before accidental iteration.
48
+ if not isinstance(paths, tuple) or any(not isinstance(path, Path) for path in paths):
49
+ raise AttachmentInputInvalid()
50
+ if len(paths) > int(AttachmentLimit.MAX_FILES):
51
+ raise AttachmentLimitExceeded()
52
+
53
+ def _resolve_one(self, path: Path, seen: set[Path], total_bytes: int) -> AgentAttachment:
54
+ # Resolves one file, hashes its exact bytes, and classifies it without a second read.
55
+ resolved = self._resolved_path(path)
56
+ if resolved in seen:
57
+ raise AttachmentDuplicate()
58
+ seen.add(resolved)
59
+ raw = self._read_bytes(resolved)
60
+ size_bytes = len(raw)
61
+ if size_bytes == 0:
62
+ raise AttachmentEmpty()
63
+ if size_bytes > int(AttachmentLimit.MAX_FILE_BYTES):
64
+ raise AttachmentTooLarge()
65
+ if total_bytes + size_bytes > int(AttachmentLimit.MAX_TOTAL_BYTES):
66
+ raise AttachmentLimitExceeded()
67
+ digest = hashlib.sha256(raw).hexdigest()
68
+ if self._is_image(resolved):
69
+ return AgentAttachment(
70
+ supplied_path=path,
71
+ resolved_path=resolved,
72
+ name=path.name,
73
+ kind=AttachmentKind.IMAGE,
74
+ size_bytes=size_bytes,
75
+ sha256=digest,
76
+ )
77
+ content = self._decode_text(raw)
78
+ return AgentAttachment(
79
+ supplied_path=path,
80
+ resolved_path=resolved,
81
+ name=path.name,
82
+ kind=AttachmentKind.TEXT,
83
+ size_bytes=size_bytes,
84
+ sha256=digest,
85
+ content=content,
86
+ )
87
+
88
+ def _resolved_path(self, path: Path) -> Path:
89
+ # Converts path errors into a stable typed failure without exposing private paths.
90
+ try:
91
+ resolved = path.expanduser().resolve(strict=False)
92
+ except (OSError, RuntimeError) as error:
93
+ raise AttachmentUnreadable() from error
94
+ try:
95
+ if not resolved.exists():
96
+ raise AttachmentFileNotFound()
97
+ if resolved.is_dir():
98
+ raise AttachmentDirectory()
99
+ if not resolved.is_file():
100
+ raise AttachmentUnreadable()
101
+ except OSError as error:
102
+ raise AttachmentUnreadable() from error
103
+ return resolved
104
+
105
+ def _read_bytes(self, path: Path) -> bytes:
106
+ # Reads one exact snapshot and hides filesystem details from the public failure.
107
+ try:
108
+ return path.read_bytes()
109
+ except OSError as error:
110
+ raise AttachmentUnreadable() from error
111
+
112
+ def _is_image(self, path: Path) -> bool:
113
+ # Uses the closed suffix enum so provider-native image support cannot drift silently.
114
+ return path.suffix.lower() in {item.value for item in AttachmentImageSuffix}
115
+
116
+ def _decode_text(self, raw: bytes) -> str:
117
+ # Accepts UTF-8 with an optional BOM and rejects binary-looking text with NUL bytes.
118
+ try:
119
+ content = raw.decode("utf-8-sig")
120
+ except UnicodeDecodeError as error:
121
+ raise AttachmentUnsupported() from error
122
+ if "\x00" in content:
123
+ raise AttachmentUnsupported()
124
+ if not content:
125
+ raise AttachmentEmpty()
126
+ return content
127
+
128
+
129
+ __all__ = ["AttachmentResolver"]
@@ -0,0 +1,47 @@
1
+ """Converts provider-neutral attachment snapshots into native Codex run inputs.
2
+
3
+ The SDK stays behind a lazy import boundary so importing the shared attachment layer never
4
+ starts provider work or makes `--help` depend on Codex being installed.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ from ...types.attachments import AgentAttachment, AttachmentBundle, AttachmentKind
12
+
13
+
14
+ class CodexAttachmentInputBuilder:
15
+ """Builds one CodexRunInput from a prompt and an already-resolved attachment bundle."""
16
+
17
+ def build(self, prompt: str, bundle: AttachmentBundle) -> Any:
18
+ # Imports occur only when a future Codex service actually starts an agent turn.
19
+ from vidbyte.context.primitives import FileContextItem
20
+ from vidbyte.lib.dataclasses.codex import (
21
+ CodexLocalImageInput,
22
+ CodexRunInput,
23
+ CodexTextInput,
24
+ )
25
+
26
+ inputs: list[Any] = [CodexTextInput(prompt)]
27
+ context_items: list[Any] = []
28
+ for attachment in bundle.items:
29
+ if attachment.kind is AttachmentKind.IMAGE:
30
+ inputs.append(CodexLocalImageInput(str(attachment.resolved_path)))
31
+ else:
32
+ context_items.append(self._file_context(FileContextItem, attachment))
33
+ return CodexRunInput(items=tuple(inputs), context_items=tuple(context_items))
34
+
35
+ def _file_context(self, file_type: type[Any], attachment: AgentAttachment) -> Any:
36
+ # Reuses the resolver's bytes and hash rather than reading a changed file again.
37
+ return file_type(
38
+ path=str(attachment.supplied_path),
39
+ absolute_path=str(attachment.resolved_path),
40
+ size_bytes=attachment.size_bytes,
41
+ content=attachment.content,
42
+ language=attachment.resolved_path.suffix.lstrip(".") or None,
43
+ metadata={"sha256": attachment.sha256},
44
+ )
45
+
46
+
47
+ __all__ = ["CodexAttachmentInputBuilder"]
@@ -0,0 +1,71 @@
1
+ """Exactly one prompt source per run: a positional value, a UTF-8 file, or explicit stdin.
2
+
3
+ Two rules drive the shape. Sources are exclusive and checked before anything is opened, so
4
+ an ambiguous command line can never quietly resolve one way. And stdin is read only for the
5
+ literal `-` marker, so a command whose input happens to be redirected fails fast instead of
6
+ blocking on a read that will never complete.
7
+
8
+ Reads take one character past the limit: that detects oversized input without pulling an
9
+ arbitrarily large file into memory. Error prose reports lengths, never prompt content.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from pathlib import Path
15
+
16
+ from ..errors.failures import (
17
+ AmbiguousPromptSource,
18
+ EmptyPrompt,
19
+ MissingPrompt,
20
+ PromptFileNotUtf8,
21
+ PromptFileUnreadable,
22
+ PromptTooLong,
23
+ StandardInputNotText,
24
+ )
25
+ from .streams import IOStreams
26
+
27
+ _DEFAULT_MAX_CHARS = 20_000
28
+
29
+
30
+ class PromptInputResolver:
31
+ """Bounded resolver for explicit text, file, or stdin prompt input."""
32
+
33
+ def __init__(self, streams: IOStreams, max_chars: int = _DEFAULT_MAX_CHARS) -> None:
34
+ self._streams = streams
35
+ self._max_chars = max_chars
36
+
37
+ def resolve(self, positional: str | None, prompt_file: str | None) -> str:
38
+ # Reject ambiguity before reading either external source.
39
+ if positional is not None and prompt_file is not None:
40
+ raise AmbiguousPromptSource()
41
+ if positional is None and prompt_file is None:
42
+ raise MissingPrompt()
43
+ if prompt_file is not None:
44
+ return self._read_file(Path(prompt_file))
45
+ if positional == "-":
46
+ return self._read_stdin()
47
+ return self._validate(positional or "")
48
+
49
+ def _read_file(self, path: Path) -> str:
50
+ # UTF-8 is explicit so machine behavior does not depend on the host locale.
51
+ try:
52
+ with path.open("r", encoding="utf-8") as handle:
53
+ return self._validate(handle.read(self._max_chars + 1))
54
+ except UnicodeDecodeError as error:
55
+ raise PromptFileNotUtf8(error) from error
56
+ except (OSError, ValueError) as error:
57
+ raise PromptFileUnreadable(str(path), error) from error
58
+
59
+ def _read_stdin(self) -> str:
60
+ # Explicit stdin remains bounded and decoding failures stay safe usage errors.
61
+ try:
62
+ return self._validate(self._streams.stdin.read(self._max_chars + 1))
63
+ except UnicodeError as error:
64
+ raise StandardInputNotText(error) from error
65
+
66
+ def _validate(self, value: str) -> str:
67
+ if not value.strip():
68
+ raise EmptyPrompt()
69
+ if len(value) > self._max_chars:
70
+ raise PromptTooLong(self._max_chars)
71
+ return value
@@ -0,0 +1,37 @@
1
+ """The process channels one CLI invocation writes to, as an injected dependency.
2
+
3
+ Reusable code never touches `sys.stdout`/`sys.stderr` directly, so the CLI can be embedded,
4
+ redirected, or captured without monkey-patching `sys`. Formatting policy (human vs JSON,
5
+ color) is deliberately NOT here — `lib/output` owns it.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import sys
11
+ from dataclasses import dataclass
12
+ from typing import TextIO
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class IOStreams:
17
+ """The stdin/stdout/stderr triple owned by one invocation."""
18
+
19
+ stdin: TextIO
20
+ stdout: TextIO
21
+ stderr: TextIO
22
+
23
+ @classmethod
24
+ def system(cls) -> IOStreams:
25
+ # Bound at invocation time, not import time, so redirection set up by an embedding
26
+ # caller after `import vidbyte_cli` is still honoured.
27
+ return cls(stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr)
28
+
29
+ def write_output(self, message: str) -> None:
30
+ # Results go to stdout so shell pipelines stay dependable.
31
+ self.stdout.write(f"{message}\n")
32
+ self.stdout.flush()
33
+
34
+ def write_error(self, message: str) -> None:
35
+ # Diagnostics go to stderr so a failure can't corrupt structured stdout.
36
+ self.stderr.write(f"{message}\n")
37
+ self.stderr.flush()