socialcli 0.3.0__tar.gz → 0.3.1__tar.gz

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.

Potentially problematic release.


This version of socialcli might be problematic. Click here for more details.

Files changed (225) hide show
  1. {socialcli-0.3.0 → socialcli-0.3.1}/PKG-INFO +5 -5
  2. {socialcli-0.3.0 → socialcli-0.3.1}/README.md +4 -4
  3. {socialcli-0.3.0 → socialcli-0.3.1}/pyproject.toml +1 -1
  4. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/agent_errors.py +75 -4
  5. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/agent_guide.py +4 -4
  6. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/engagement.py +69 -8
  7. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/engagement_cli.py +23 -3
  8. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/identity.py +30 -13
  9. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/inbox.py +5 -1
  10. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/inventory.py +4 -0
  11. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/batches.py +20 -10
  12. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/changes.py +30 -15
  13. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/community_changes.py +4 -2
  14. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/snippet_readback.py +9 -0
  15. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/youtube_community.py +4 -4
  16. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/youtube_resources.py +13 -3
  17. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/media_preflight.py +7 -2
  18. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/operation_output.py +3 -0
  19. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/postfile.py +12 -14
  20. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/read_reports.py +4 -1
  21. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/connections.py +1 -0
  22. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/lifecycle.py +6 -3
  23. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/pages.py +1 -1
  24. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/pairing.py +5 -3
  25. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/permissions.py +23 -2
  26. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/settings.py +11 -1
  27. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/ui/assets/agent-manual.md +83 -20
  28. socialcli-0.3.1/socialctl/ui/assets/agents.md +33 -0
  29. socialcli-0.3.0/socialctl/ui/assets/agents.md +0 -27
  30. {socialcli-0.3.0 → socialcli-0.3.1}/.gitignore +0 -0
  31. {socialcli-0.3.0 → socialcli-0.3.1}/LICENSE +0 -0
  32. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/__init__.py +0 -0
  33. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/__main__.py +0 -0
  34. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/adapters/__init__.py +0 -0
  35. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/adapters/base.py +0 -0
  36. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/adapters/errores.py +0 -0
  37. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/adapters/facebook.py +0 -0
  38. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/adapters/instagram.py +0 -0
  39. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/adapters/tiktok.py +0 -0
  40. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/adapters/youtube.py +0 -0
  41. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/approval_cli.py +0 -0
  42. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/auth.py +0 -0
  43. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/authflow.py +0 -0
  44. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/brands.py +0 -0
  45. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/capabilities.py +0 -0
  46. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/capabilities.yml +0 -0
  47. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/cli.py +0 -0
  48. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connection_service/__init__.py +0 -0
  49. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connection_service/__main__.py +0 -0
  50. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connection_service/app.py +0 -0
  51. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connection_service/assets/PLATFORM-LOGOS-LICENSE.txt +0 -0
  52. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connection_service/assets/connection.css +0 -0
  53. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connection_service/assets/facebook.webp +0 -0
  54. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connection_service/assets/instagram.svg +0 -0
  55. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connection_service/assets/socialcli-logo.png +0 -0
  56. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connection_service/assets/tiktok.png +0 -0
  57. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connection_service/assets/youtube.svg +0 -0
  58. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connection_service/landing.py +0 -0
  59. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connection_service/legacy.py +0 -0
  60. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connection_service/peer.py +0 -0
  61. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connection_service/providers.py +0 -0
  62. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connection_service/settings.py +0 -0
  63. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connection_service/store.py +0 -0
  64. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connections/__init__.py +0 -0
  65. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connections/account_cli.py +0 -0
  66. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connections/cli.py +0 -0
  67. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connections/client.py +0 -0
  68. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connections/credential_cli.py +0 -0
  69. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connections/credential_store.py +0 -0
  70. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connections/file_store.py +0 -0
  71. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connections/keychain.py +0 -0
  72. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connections/meta_user.py +0 -0
  73. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connections/rotation_cli.py +0 -0
  74. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connections/saas_client.py +0 -0
  75. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/connections/temporary_media.py +0 -0
  76. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/delivery_options.py +0 -0
  77. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/executor.py +0 -0
  78. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/formatter.py +0 -0
  79. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/hosted_media.py +0 -0
  80. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/inbox_cli.py +0 -0
  81. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/inventory_cli.py +0 -0
  82. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/inventory_sources.py +0 -0
  83. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/__init__.py +0 -0
  84. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/batch_cli.py +0 -0
  85. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/cli.py +0 -0
  86. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/comments_cli.py +0 -0
  87. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/community_batch_cli.py +0 -0
  88. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/community_batches.py +0 -0
  89. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/community_cli.py +0 -0
  90. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/community_schema.py +0 -0
  91. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/facebook_groups.py +0 -0
  92. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/groups_cli.py +0 -0
  93. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/inbox_readers.py +0 -0
  94. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/meta_changes.py +0 -0
  95. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/meta_cli.py +0 -0
  96. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/meta_client.py +0 -0
  97. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/meta_comment_inbox.py +0 -0
  98. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/meta_comments.py +0 -0
  99. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/meta_errors.py +0 -0
  100. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/meta_pagination.py +0 -0
  101. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/meta_schema.py +0 -0
  102. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/meta_webhooks.py +0 -0
  103. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/metadata_parts.py +0 -0
  104. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/owned_assets.py +0 -0
  105. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/owned_changes.py +0 -0
  106. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/owned_cli.py +0 -0
  107. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/owned_reorder.py +0 -0
  108. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/owned_schema.py +0 -0
  109. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/reply_context.py +0 -0
  110. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/reply_evidence.py +0 -0
  111. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/resource_changes.py +0 -0
  112. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/resource_cli.py +0 -0
  113. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/stories.py +0 -0
  114. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/stories_cli.py +0 -0
  115. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/youtube.py +0 -0
  116. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/youtube_metadata.py +0 -0
  117. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/management/youtube_owned.py +0 -0
  118. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/media.py +0 -0
  119. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/media_cli.py +0 -0
  120. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/media_ingress.py +0 -0
  121. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/media_registry.py +0 -0
  122. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/media_requirements.py +0 -0
  123. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/__init__.py +0 -0
  124. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/almacen.py +0 -0
  125. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/analytics_changes.py +0 -0
  126. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/analytics_cli.py +0 -0
  127. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/analytics_client.py +0 -0
  128. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/analytics_ingest.py +0 -0
  129. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/analytics_schema.py +0 -0
  130. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/base.py +0 -0
  131. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/facebook.py +0 -0
  132. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/insights.py +0 -0
  133. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/instagram.py +0 -0
  134. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/modelos.py +0 -0
  135. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/piezas.py +0 -0
  136. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/reach.py +0 -0
  137. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/reporting_client.py +0 -0
  138. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/resumen.py +0 -0
  139. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/slugs.py +0 -0
  140. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/tiktok.py +0 -0
  141. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/metricas/youtube.py +0 -0
  142. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/migration_cli.py +0 -0
  143. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/migration_files.py +0 -0
  144. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/models.py +0 -0
  145. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/__init__.py +0 -0
  146. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/api_delivery.py +0 -0
  147. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/api_reconcile.py +0 -0
  148. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/approval.py +0 -0
  149. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/cadence.py +0 -0
  150. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/cli.py +0 -0
  151. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/dispatcher.py +0 -0
  152. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/facebook.py +0 -0
  153. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/facebook_reels.py +0 -0
  154. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/first_comment.py +0 -0
  155. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/handoff.py +0 -0
  156. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/instagram_delivery.py +0 -0
  157. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/instagram_runtime.py +0 -0
  158. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/inventory.py +0 -0
  159. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/migration.py +0 -0
  160. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/models.py +0 -0
  161. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/reconcile.py +0 -0
  162. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/remote_data.py +0 -0
  163. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/report.py +0 -0
  164. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/store.py +0 -0
  165. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/verification.py +0 -0
  166. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/native_schedule/youtube.py +0 -0
  167. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/plantillas/accounts.yml +0 -0
  168. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/plantillas/brand.md +0 -0
  169. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/plantillas/estrategia.md +0 -0
  170. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/preflight.py +0 -0
  171. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/publication_policy.py +0 -0
  172. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/publication_status.py +0 -0
  173. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/publication_steps.py +0 -0
  174. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/publish_output.py +0 -0
  175. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/publisher.py +0 -0
  176. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/queue_migration.py +0 -0
  177. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/read_budget.py +0 -0
  178. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/rutas.py +0 -0
  179. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/__init__.py +0 -0
  180. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/access.py +0 -0
  181. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/auth.py +0 -0
  182. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/backup.py +0 -0
  183. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/billing.py +0 -0
  184. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/billing_enrollment.py +0 -0
  185. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/billing_enrollment_schema.py +0 -0
  186. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/billing_inventory.py +0 -0
  187. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/billing_reconcile.py +0 -0
  188. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/billing_routes.py +0 -0
  189. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/budget.py +0 -0
  190. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/consent.py +0 -0
  191. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/mail.py +0 -0
  192. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/maintenance.py +0 -0
  193. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/meta_user.py +0 -0
  194. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/meta_webhooks.py +0 -0
  195. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/migration.py +0 -0
  196. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/models.py +0 -0
  197. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/operations.py +0 -0
  198. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/reminders.py +0 -0
  199. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/renewal.py +0 -0
  200. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/restore_guard.py +0 -0
  201. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/revocation.py +0 -0
  202. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/rotation.py +0 -0
  203. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/routes.py +0 -0
  204. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/store.py +0 -0
  205. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/temporary_media.py +0 -0
  206. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/saas/webhooks.py +0 -0
  207. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/schedule_remote.py +0 -0
  208. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/schedule_sqlite.py +0 -0
  209. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/scheduler.py +0 -0
  210. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/scheduler_launchd.py +0 -0
  211. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/scheduler_systemd.py +0 -0
  212. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/source_links.py +0 -0
  213. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/ui/__init__.py +0 -0
  214. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/ui/assets/components.css +0 -0
  215. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/ui/assets/connection-layout.css +0 -0
  216. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/ui/assets/otp.template.html +0 -0
  217. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/ui/assets/tokens.css +0 -0
  218. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/ui/assets/trial.template.html +0 -0
  219. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/ui/assets/web-layout.css +0 -0
  220. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/ui/email/otp.html +0 -0
  221. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/ui/email/trial.html +0 -0
  222. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/upload_consent.py +0 -0
  223. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/workspace.py +0 -0
  224. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/youtube_data.py +0 -0
  225. {socialcli-0.3.0 → socialcli-0.3.1}/socialctl/youtube_data_service.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: socialcli
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: SocialCli: publish and manage creator-owned social content from the terminal.
5
5
  Project-URL: Homepage, https://socialcli.app
6
6
  Project-URL: Repository, https://github.com/engels-florentino/SocialCli
@@ -25,7 +25,7 @@ Description-Content-Type: text/markdown
25
25
 
26
26
  # SocialCli
27
27
 
28
- **Current source version: 0.3.0.** SocialCli SaaS is the current SocialCli
28
+ **Current source version: 0.3.1 (release preparation).** SocialCli SaaS is the current SocialCli
29
29
  product. The repository root on `main` is its single development workspace;
30
30
  the CLI, hosted connection service and account/billing components live together
31
31
  here. Earlier versions remain in Git history for reference and migration.
@@ -61,9 +61,9 @@ Stripe uses test mode. Planned access costs
61
61
  USD 10/month after 14 free days from the first successful connection. No real
62
62
  subscriptions are collected in this pilot.
63
63
 
64
- The current client release target is **0.3.0** (published first to TestPyPI and
65
- then to PyPI through the reviewed release workflow). The compatible hosted
66
- runtime is `socialcli-saas-candidate:45fda63`. The client's wheel and
64
+ The next client release target is **0.3.1**, pending reviewed publication through
65
+ TestPyPI and PyPI. The currently published stable client remains **0.3.0**.
66
+ The hosted service is managed and verified separately from the CLI release. The client's wheel and
67
67
  source archive are verified against the
68
68
  reviewed build hashes before publication. Provider commercial approvals and live
69
69
  billing cutover remain separate from package publication. See the
@@ -2,7 +2,7 @@
2
2
 
3
3
  # SocialCli
4
4
 
5
- **Current source version: 0.3.0.** SocialCli SaaS is the current SocialCli
5
+ **Current source version: 0.3.1 (release preparation).** SocialCli SaaS is the current SocialCli
6
6
  product. The repository root on `main` is its single development workspace;
7
7
  the CLI, hosted connection service and account/billing components live together
8
8
  here. Earlier versions remain in Git history for reference and migration.
@@ -38,9 +38,9 @@ Stripe uses test mode. Planned access costs
38
38
  USD 10/month after 14 free days from the first successful connection. No real
39
39
  subscriptions are collected in this pilot.
40
40
 
41
- The current client release target is **0.3.0** (published first to TestPyPI and
42
- then to PyPI through the reviewed release workflow). The compatible hosted
43
- runtime is `socialcli-saas-candidate:45fda63`. The client's wheel and
41
+ The next client release target is **0.3.1**, pending reviewed publication through
42
+ TestPyPI and PyPI. The currently published stable client remains **0.3.0**.
43
+ The hosted service is managed and verified separately from the CLI release. The client's wheel and
44
44
  source archive are verified against the
45
45
  reviewed build hashes before publication. Provider commercial approvals and live
46
46
  billing cutover remain separate from package publication. See the
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "socialcli"
3
- version = "0.3.0"
3
+ version = "0.3.1"
4
4
  description = "SocialCli: publish and manage creator-owned social content from the terminal."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -32,6 +32,10 @@ _ACTIONS = {
32
32
  # Only fixed internal codes acquire action semantics. Provider text is diagnostic,
33
33
  # never a command, an expiry signal or evidence that a write was not accepted.
34
34
  _CATALOG = {
35
+ 'reply_history_purged': ('user_action', 'Prior reply evidence was removed by retention. Review the preserved journal and platform before any new draft. Do not delete replay-blocking markers or repeat an uncertain write.'),
36
+ 'dry_run_required': ('correct_input', 'inbox-draft requires --dry-run to prepare a preview. No provider content write was started.'),
37
+ 'reply_input_invalid': ('correct_input', 'Use a version 1 reply file with a nonempty replies list (1–100 items). Each reply requires platform, account_id, comment_id and text. media_id is read from the inbox, not supplied in this file.'),
38
+ 'target_validation_failed': ('inspect', 'Local target verification failed. Inspect the diagnostic reason; this does not establish a provider rejection or invalid credentials.'),
35
39
  'approval_mismatch': ('user_action', 'The supplied approval does not match this exact preview. Obtain the owner approval before applying.'),
36
40
  'approval_required': ('user_action', 'Show the complete preview and obtain explicit approval of its exact fingerprint.'),
37
41
  'operation_cancelled': ('inspect', 'Approval was cancelled. No write was started by this command.'),
@@ -108,6 +112,7 @@ def error_document(exc: BaseException, *, read_only: bool) -> dict:
108
112
  from socialctl.management.meta_schema import MetaError, MetaUncertain
109
113
  from socialctl.metricas.base import SinPermiso
110
114
  from socialctl.youtube_data import YouTubeDataBusy, YouTubeDataCleanupError
115
+ from socialctl.identity import ReadError
111
116
 
112
117
  # A read flow may refresh a token via POST. A lost response to that operation
113
118
  # cannot be classified as a harmless GET retry merely from the CLI command.
@@ -124,7 +129,7 @@ def error_document(exc: BaseException, *, read_only: bool) -> dict:
124
129
  429:'rate_limited',503:'provider_unavailable'}.get(exc.status,'service_rejected')
125
130
  elif isinstance(exc, (ResourceUncertain, YouTubeUpdateUncertain, CommentUncertain, MetaUncertain)):
126
131
  code = 'operation_uncertain'
127
- elif isinstance(exc, (AuthError, YouTubeManagementError, ResourceError, CommentError, MetaError)) and getattr(exc, '_socialcli_error_code', None) is not None:
132
+ elif isinstance(exc, (AuthError, YouTubeManagementError, ChangeError, ResourceError, CommentError, MetaError, ReadError)) and getattr(exc, '_socialcli_error_code', None) is not None:
128
133
  code = exc._socialcli_error_code
129
134
  elif isinstance(exc, ReadDeadlineExceeded): code = 'read_deadline'
130
135
  elif isinstance(exc, ReadResponseTooLarge): code = 'response_too_large'
@@ -144,21 +149,66 @@ def error_document(exc: BaseException, *, read_only: bool) -> dict:
144
149
  429:'rate_limited'}.get(status,'provider_unavailable' if status>=500 else 'request_rejected')
145
150
  if type(status) is int else 'operation_rejected')
146
151
  elif isinstance(exc, httpx.RequestError): code = 'provider_response_invalid'
152
+ elif isinstance(exc, ReadError):
153
+ code = {'credentials_missing': 'authorization_unavailable', 'credentials_expired': 'authorization_unavailable',
154
+ 'permission_denied': 'permissions_required', 'rate_limited': 'rate_limited',
155
+ 'timeout': 'transport_failure', 'transport_error': 'transport_failure',
156
+ 'invalid_response': 'provider_response_invalid', 'provider_error': 'provider_unavailable',
157
+ 'configuration_missing': 'account_not_configured'}.get(exc.code, 'target_validation_failed')
158
+ elif isinstance(exc, ResourceError): code = 'target_validation_failed'
147
159
  elif isinstance(exc, ChangeError): code = 'operation_rejected'
148
160
  elif isinstance(exc, YouTubeDataBusy): code = 'local_data_busy'
149
161
  elif isinstance(exc, (OSError, YouTubeDataCleanupError)): code = 'storage_failure'
150
162
  elif isinstance(exc, ValueError): code = 'invalid_local_state'
151
163
  else: code = 'operation_failed'
152
- return error_for_code(code,read_only=read_only)
164
+ result = error_for_code(code,read_only=read_only)
165
+ # Local validation is not evidence of a rejection by the hosted service.
166
+ if code == 'operation_rejected' and not isinstance(exc, ServiceRejected):
167
+ result['message'] = _CATALOG[code][1]
168
+ diagnostic = safe_diagnostic(getattr(exc, '_socialcli_diagnostic', None))
169
+ if isinstance(exc, ChangeError) and not diagnostic:
170
+ diagnostic = {'source': 'local'}
171
+ if isinstance(exc, ServiceRejected):
172
+ diagnostic.update(source='socialcli', http_status=exc.status)
173
+ elif isinstance(exc, httpx.HTTPStatusError):
174
+ diagnostic.update(http_status=exc.response.status_code, request_method=exc.request.method)
175
+ if diagnostic:
176
+ result['diagnostic'] = safe_diagnostic(diagnostic)
177
+ return result
178
+
179
+
180
+ def safe_diagnostic(value):
181
+ """Allow only fixed machine labels, never URLs, bodies, headers or prose."""
182
+ if not isinstance(value, dict):
183
+ return {}
184
+ allowed = {
185
+ 'source': {'local', 'socialcli', 'youtube'},
186
+ 'stage': {'input_validation', 'workspace_read', 'file_read', 'credential_read', 'identity_read', 'provider_read', 'provider_write', 'target_read', 'reply_coverage', 'proposal_preparation', 'baseline_check', 'readback'},
187
+ 'request_method': {'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE'},
188
+ 'operation': {'channels', 'videos', 'comments', 'commentThreads', 'playlistItems'},
189
+ 'write_state': {'not_started', 'unknown', 'accepted'},
190
+ 'content_write_state': {'not_started'},
191
+ 'reason': {'thread_unverifiable', 'thread_identity_mismatch', 'thread_total_unverifiable', 'comment_binding_mismatch', 'parent_mismatch', 'replies_not_allowed', 'incomplete_reply_list', 'readback_mismatch'},
192
+ 'provider_reason': {'commentsDisabled', 'quotaExceeded', 'dailyLimitExceeded', 'rateLimitExceeded', 'userRateLimitExceeded', 'insufficientPermissions', 'forbidden', 'authError', 'invalidValue', 'videoNotFound', 'commentNotFound', 'commentThreadNotFound', 'processingFailure', 'operationNotSupported'},
193
+ }
194
+ clean = {key: val for key, val in value.items()
195
+ if key in allowed and isinstance(val, str) and val in allowed[key]}
196
+ status = value.get('http_status')
197
+ if type(status) is int and 100 <= status <= 599:
198
+ clean['http_status'] = status
199
+ return clean
153
200
 
154
201
 
155
202
  def with_error_code(wrapper: Exception, cause: BaseException, *, read_only: bool,
156
203
  fallback: str | None = None) -> Exception:
157
204
  """Keep only a fixed code across a sanitized wrapper, never raw exception data."""
158
- code = error_document(cause, read_only=read_only)['code']
205
+ detail = error_document(cause, read_only=read_only)
206
+ code = detail['code']
159
207
  if code == 'operation_failed' and fallback is not None:
160
208
  code = error_for_code(fallback, read_only=read_only)['code']
161
209
  wrapper._socialcli_error_code = code
210
+ if detail.get('diagnostic'):
211
+ wrapper._socialcli_diagnostic = detail['diagnostic']
162
212
  return wrapper
163
213
 
164
214
 
@@ -166,7 +216,28 @@ def with_http_error_code(wrapper: Exception, response, *, read_only: bool) -> Ex
166
216
  import httpx
167
217
  cause = httpx.HTTPStatusError('Provider request rejected', request=response.request,
168
218
  response=response)
169
- return with_error_code(wrapper, cause, read_only=read_only)
219
+ wrapper = with_error_code(wrapper, cause, read_only=read_only)
220
+ if response.request.url.host == 'www.googleapis.com':
221
+ diagnostic = {'source': 'youtube', 'http_status': response.status_code,
222
+ 'request_method': response.request.method,
223
+ 'stage': 'provider_read' if read_only else 'provider_write',
224
+ 'operation': response.request.url.path.rsplit('/', 1)[-1],
225
+ 'write_state': 'not_started' if read_only else 'unknown'}
226
+ try:
227
+ reasons = response.json().get('error', {}).get('errors', [])
228
+ for row in reasons[:16]:
229
+ reason = safe_diagnostic({'provider_reason': row.get('reason')}) if isinstance(row, dict) else {}
230
+ if reason:
231
+ diagnostic.update(reason)
232
+ if reason['provider_reason'] in {'quotaExceeded', 'dailyLimitExceeded', 'rateLimitExceeded', 'userRateLimitExceeded'}:
233
+ wrapper._socialcli_error_code = 'rate_limited' if read_only else 'operation_uncertain'
234
+ elif reason['provider_reason'] == 'commentsDisabled' and read_only:
235
+ wrapper._socialcli_error_code = 'comments_disabled'
236
+ break
237
+ except (ValueError, TypeError, AttributeError):
238
+ pass
239
+ wrapper._socialcli_diagnostic = safe_diagnostic(diagnostic)
240
+ return wrapper
170
241
 
171
242
 
172
243
  def emit_error(exc: Exception, *, read_only: bool) -> None:
@@ -3,11 +3,11 @@ from importlib.resources import files
3
3
  import re
4
4
 
5
5
  TOPICS={
6
- 'setup':('Install the reviewed candidate','Authorize the installation, then the social account','Persist and recover','Publication gate','Explicit approval and preflight'),
6
+ 'setup':('What agents can do','Install the reviewed release','Authorize the installation, then the social account','Persist and recover','Publication gate','Explicit approval and preflight'),
7
7
  'publish':('Publication gate','Temporary Instagram media','Canonical publishing workflow','Check supplied media locally','Explicit approval and preflight'),
8
8
  'schedule':('Publication gate','Temporary Instagram media','Scheduling and temporary media','Native scheduling input examples (offline)','Explicit approval and preflight'),
9
- 'inbox':('Publication gate','Read metrics and prepare engagement','Read diagnostics','Comment API corrections','Explicit approval and preflight','Resumable batches and reply context'),
10
- 'errors':('Persist and recover','Recovery and error actions','Read diagnostics','Comment API corrections','Explicit approval and preflight','Resumable batches and reply context'),
9
+ 'inbox':('Publication gate','Read metrics and prepare engagement','Read diagnostics','Comment API evidence and limits','Explicit approval and preflight','Resumable batches and reply context','Purged reply history'),
10
+ 'errors':('Persist and recover','Recovery and error actions','Read diagnostics','Comment API evidence and limits','Explicit approval and preflight','Resumable batches and reply context','Purged reply history'),
11
11
  'permissions':('Installation connection access',),
12
12
  }
13
13
 
@@ -20,4 +20,4 @@ def guide(topic):
20
20
  sections=re.split(r'(?=^## )',full,flags=re.MULTILINE)
21
21
  chosen=[section for section in sections if any(section.startswith('## '+heading) for heading in TOPICS[topic])]
22
22
  return ('# SocialCli agent guide: '+topic+'\n\n'+''.join(chosen)+
23
- '\nNext: socialcli agent-guide publish; socialcli agent-guide schedule; socialcli agent-guide inbox; socialcli agent-guide errors.\n')
23
+ '\nNext: socialcli agent-guide publish; socialcli agent-guide schedule; socialcli agent-guide inbox; socialcli agent-guide errors; socialcli agent-guide permissions; socialcli agent-guide all.\n')
@@ -1,4 +1,5 @@
1
1
  """Join observed comments to existing immutable reply journals; never send replies."""
2
+ import json
2
3
  import uuid
3
4
 
4
5
  from socialctl.inbox import UnifiedInboxStore
@@ -17,17 +18,25 @@ class DraftSelectionError(ChangeError):
17
18
  def __init__(self, reason, message):
18
19
  super().__init__(message)
19
20
  self.reason = reason
21
+ if reason in {'reply_file_schema', 'reply_count', 'invalid_reply_selection'}:
22
+ self._socialcli_error_code = 'reply_input_invalid'
23
+ elif reason == 'reply_history_purged':
24
+ self._socialcli_error_code = reason
20
25
 
21
26
 
22
27
  def draft_failure(exc, row, index, stage):
23
28
  # Fixed local reason codes and target IDs only. Provider/storage exception
24
29
  # text can contain credentials or private diagnostics and is not copied.
25
- exc.draft_failure = {'child_index': index, 'stage': stage,
30
+ exc.draft_failure = {'child_index': index, 'stage': 'selection' if stage == 'selection' else 'preparation', 'step': stage,
26
31
  **{key: row[key] for key in ('platform', 'account_id', 'comment_id', 'media_id')
27
32
  if isinstance(row, dict) and isinstance(row.get(key), str)},
28
33
  'reason': getattr(exc, 'reason', 'target_validation_failed' if stage == 'selection' else 'draft_preparation_failed')}
29
34
  if isinstance(exc, (DraftSelectionError, ReplyCoverageError)):
30
35
  exc.draft_failure['message'] = str(exc)
36
+ from socialctl.agent_errors import safe_diagnostic, error_document
37
+ write_state = 'unknown' if error_document(exc, read_only=True)['code'] == 'operation_uncertain' else 'not_started'
38
+ exc._socialcli_diagnostic = {**safe_diagnostic(getattr(exc, '_socialcli_diagnostic', None)),
39
+ 'stage': stage, 'write_state': write_state, 'content_write_state': 'not_started'}
31
40
  return exc
32
41
 
33
42
 
@@ -35,12 +44,46 @@ def http_client(budget):
35
44
  return read_client(budget)
36
45
 
37
46
 
47
+ def purged_history(store, path):
48
+ """Read only a retention marker's supplied intent, never resurrect its receipt.
49
+
50
+ Markers intentionally no longer prove account identity or a write outcome.
51
+ A matching parent therefore blocks a new draft conservatively across accounts;
52
+ an unbound marker blocks YouTube drafts, but not the read-only review.
53
+ """
54
+ raw = json.loads(path.read_bytes())
55
+ if not isinstance(raw, dict) or raw.get('data_lifecycle') != 'purged':
56
+ return None
57
+ store._validate_id(path.stem)
58
+ if (raw.get('id') != path.stem or raw.get('platform') != 'youtube'
59
+ or raw.get('replay_blocked') is not True or not isinstance(raw.get('user_input'), dict)):
60
+ exc = ChangeError('Invalid purged journal; preserve the file for manual review.')
61
+ exc._socialcli_error_code = 'invalid_local_state'
62
+ raise exc
63
+ from socialctl.management.community_schema import ACTIONS, opaque_id
64
+ edit = raw['user_input'].get('edit')
65
+ action = edit.get('action') if isinstance(edit, dict) else None
66
+ if isinstance(action, str) and action in ACTIONS and action != 'reply':
67
+ return {'change_id': path.stem, 'reply': False, 'parent_id': None}
68
+ parent = edit.get('parent_id') if isinstance(edit, dict) and action == 'reply' else None
69
+ try:
70
+ parent = opaque_id(parent)
71
+ except ValueError:
72
+ parent = None
73
+ return {'change_id': path.stem, 'reply': True, 'parent_id': parent}
74
+
75
+
38
76
  def review_inbox(brand):
39
77
  data=UnifiedInboxStore(brand.raiz).load()
40
- links={};excluded_proposals=0
78
+ links={};excluded_proposals=0;purged=[]
41
79
  for provider in ('youtube-community','meta-comments'):
42
80
  store=child_store(brand,provider)
43
81
  for path in sorted(store.root.glob('*.json')):
82
+ if provider == 'youtube-community':
83
+ history = purged_history(store, path)
84
+ if history is not None:
85
+ purged.append(history)
86
+ continue
44
87
  child=store.load(path.stem)
45
88
  if provider=='youtube-community':
46
89
  if child.edit['action']!='reply':continue
@@ -67,6 +110,7 @@ def review_inbox(brand):
67
110
  context_key = UnifiedInboxStore.key(context)
68
111
  links.setdefault(context_key, []).append({**links[key][-1], 'reply_parent_id':parent})
69
112
  items=[];excluded=0
113
+ unbound = [p for p in purged if p['reply'] and p['parent_id'] is None]
70
114
  for key,row in sorted(data['items'].items()):
71
115
  field=ACCOUNT_FIELDS.get(row['platform'])
72
116
  if not field or row['account_id']!=(brand.cuentas.get(row['platform']) or {}).get(field):
@@ -79,23 +123,32 @@ def review_inbox(brand):
79
123
  states={p['status'] for p in proposals}
80
124
  evidence=bound_evidence(row)
81
125
  platform_replied=bool(evidence and evidence['state']=='owner_reply_found')
82
- state=('uncertain' if states & UNCERTAIN else 'replied' if 'verified' in states or platform_replied
126
+ history = [p['change_id'] for p in purged if row['platform'] == 'youtube' and p['reply']
127
+ and (p['parent_id'] is None or p['parent_id'] in {row['comment_id'], row.get('parent_id')})]
128
+ state=('blocked' if history else 'uncertain' if states & UNCERTAIN else 'replied' if 'verified' in states or platform_replied
83
129
  else 'draft' if 'proposed' in states else 'blocked' if states else 'observed')
84
- items.append({**row,'reply_state':state,'platform_reply_state':evidence['state'] if evidence else 'unknown','proposals':proposals})
130
+ items.append({**row,'reply_state':state,'platform_reply_state':evidence['state'] if evidence else 'unknown','proposals':proposals,
131
+ 'history_review_required':bool(history),'purged_proposals':history})
85
132
  return {'version':1,'brand':brand.nombre,'status':'ok','items':items,
86
133
  'coverage':{'source':'saved_observations','live_verified':False,'complete':False,
87
134
  'excluded_accounts':excluded,'excluded_proposals':excluded_proposals,'watermarks':data['watermarks'],
135
+ 'purged_proposals':len(purged),'unbound_purged_proposals':len(unbound),
88
136
  'limitations':['This is saved evidence, not a complete live inbox.',
89
137
  'Replied means a stored verified write or scoped owner reply observed on the platform; present remote visibility is not rechecked. Unknown platform reply state never proves absence.',
138
+ 'Purged journals remain replay-blocked. Matching or unbound reply history requires manual review before a new draft; unrelated comments remain usable.',
90
139
  'TikTok comments are unsupported. Replying in an existing reply thread requires an explicit reply_parent_id mapping to its observed top-level comment; deeper nesting is not promised.']}}
91
140
 
92
141
 
93
142
  def selected_replies(brand,raw):
94
143
  if not isinstance(raw,dict) or set(raw)!={'version','replies'} or type(raw['version']) is not int or raw['version']!=1:
95
- raise ChangeError('expected version 1 and explicit replies')
144
+ raise DraftSelectionError('reply_file_schema', 'expected version 1 and explicit replies')
96
145
  replies=raw['replies']
97
- if not isinstance(replies,list) or not 1<=len(replies)<=100:raise ChangeError('select 1–100 explicit replies')
98
- rows={UnifiedInboxStore.key(row):row for row in review_inbox(brand)['items']}
146
+ if not isinstance(replies,list) or not 1<=len(replies)<=100:raise DraftSelectionError('reply_count', 'select 1–100 explicit replies')
147
+ try:
148
+ rows={UnifiedInboxStore.key(row):row for row in review_inbox(brand)['items']}
149
+ except Exception as exc:
150
+ exc._socialcli_diagnostic={'source':'local', 'stage':'workspace_read', 'write_state':'not_started'}
151
+ raise
99
152
  result=[];seen=set()
100
153
  for index, reply in enumerate(replies, 1):
101
154
  try:
@@ -128,6 +181,9 @@ def selected_replies(brand,raw):
128
181
  seen.add(parent_key)
129
182
  elif reply.get('reply_parent_id'):
130
183
  raise DraftSelectionError('reply_parent_mismatch', 'reply_parent_id is only valid for an observed nested context')
184
+ if row.get('history_review_required') or (source and source.get('history_review_required')):
185
+ raise DraftSelectionError('reply_history_purged',
186
+ 'Prior reply evidence was removed by retention. Review the preserved journal and platform before any new draft; do not delete the replay-blocking marker.')
131
187
  if row['reply_state'] in {'replied','uncertain'}:raise DraftSelectionError('existing_reply_state', 'existing replied/uncertain target; inspect or reconcile before another draft')
132
188
  if row.get('author_id') == row['account_id']:
133
189
  raise DraftSelectionError('own_comment', 'target is an own comment')
@@ -155,6 +211,7 @@ def prepare_replies(brand,raw,*,budget,prepared,progress=None):
155
211
  selected=selected_replies(brand,raw)
156
212
  with http_client(budget) as http:
157
213
  for index, (row,text) in enumerate(selected, 1):
214
+ stage = 'target_read'
158
215
  try:
159
216
  budget.check()
160
217
  if progress is not None:
@@ -168,7 +225,9 @@ def prepare_replies(brand,raw,*,budget,prepared,progress=None):
168
225
  snippet=parent['snippet']
169
226
  same_parent(row,{'id':parent['id'],'author_id':(snippet.get('authorChannelId') or {}).get('value'),
170
227
  'text':snippet.get('textOriginal',snippet.get('textDisplay'))})
228
+ stage = 'reply_coverage'
171
229
  check_reply_coverage(client, row)
230
+ stage = 'proposal_preparation'
172
231
  child=prepare_community(client,child_store(brand,'youtube-community'),
173
232
  dict(action='reply',video_id=row['media_id'],thread_id=row['thread_id'],parent_id=row['comment_id'],text=text),
174
233
  reply_context=row.get('reply_context'))
@@ -178,6 +237,7 @@ def prepare_replies(brand,raw,*,budget,prepared,progress=None):
178
237
  from socialctl.management.meta_comment_inbox import CommentInboxStore,prepare_inbox_draft
179
238
  client=MetaCommentsClient(brand,Platform(row['platform']),http)
180
239
  same_parent(row,client.find_comment(row['media_id'],row['comment_id']))
240
+ stage = 'proposal_preparation'
181
241
  child,_=prepare_inbox_draft(client,child_store(brand,'meta-comments'),CommentInboxStore(brand.raiz),
182
242
  media_id=row['media_id'],comment_id=row['comment_id'],text=text,
183
243
  reply_context=row.get('reply_context'),
@@ -188,6 +248,7 @@ def prepare_replies(brand,raw,*,budget,prepared,progress=None):
188
248
  # Recheck the exact baseline captured by preparation against the
189
249
  # selected observation, not just the earlier network read.
190
250
  parent=child.before['parent']
251
+ stage = 'baseline_check'
191
252
  fresh=parent if provider=='meta-comments' else {'id':parent['id'],
192
253
  'author_id':(parent['snippet'].get('authorChannelId') or {}).get('value'),
193
254
  'text':parent['snippet'].get('textOriginal',parent['snippet'].get('textDisplay'))}
@@ -196,5 +257,5 @@ def prepare_replies(brand,raw,*,budget,prepared,progress=None):
196
257
  progress({'child_index':index, 'total':len(selected), 'platform':row['platform'],
197
258
  'comment_id':row['comment_id'], 'status':'prepared'})
198
259
  except (Exception, KeyboardInterrupt) as exc:
199
- raise draft_failure(exc, row, index, 'preparation')
260
+ raise draft_failure(exc, row, index, stage)
200
261
  return prepare_community_batch(brand,prepared)
@@ -32,14 +32,34 @@ def register(app,default_root):
32
32
  @app.command('inbox-draft')
33
33
  def draft(file:Path=typer.Option(...,'--file'),brand:str=typer.Option(...,'--brand'),root:Path=typer.Option(default_root,'--root'),dry_run:bool=typer.Option(False,'--dry-run'),json_output:bool=typer.Option(False,'--json'),timeout:float=typer.Option(120,'--timeout')):
34
34
  prepared=[]
35
+ stage='input_validation'
35
36
  try:
36
- if not dry_run:raise ChangeError('drafting requires --dry-run and complete preview before separate approval')
37
+ if not dry_run:
38
+ exc=ChangeError('drafting requires --dry-run and complete preview before separate approval')
39
+ exc._socialcli_error_code='dry_run_required'
40
+ raise exc
41
+ stage='workspace_read'
37
42
  selected=cargar_brand(root,brand)
43
+ stage='file_read'
38
44
  with file.open('rb') as stream:
39
45
  raw=stream.read(262145)
40
- if len(raw)>262144:raise ChangeError('reply file exceeds 256 KiB')
41
- batch=prepare_replies(selected,yaml.safe_load(raw),budget=ReadBudget(timeout),prepared=prepared,progress=progress)
46
+ if len(raw)>262144:
47
+ exc=ChangeError('reply file exceeds 256 KiB')
48
+ exc._socialcli_error_code='reply_input_invalid'
49
+ raise exc
50
+ stage='input_validation'
51
+ try:
52
+ document=yaml.safe_load(raw)
53
+ except (yaml.YAMLError, UnicodeError):
54
+ exc=ChangeError('reply file must be valid YAML')
55
+ exc._socialcli_error_code='reply_input_invalid'
56
+ raise exc from None
57
+ batch=prepare_replies(selected,document,budget=ReadBudget(timeout),prepared=prepared,progress=progress)
42
58
  except (Exception,KeyboardInterrupt) as exc:
59
+ if not getattr(exc, 'draft_failure', None):
60
+ from socialctl.agent_errors import safe_diagnostic
61
+ exc._socialcli_diagnostic={'source':'local', 'stage':stage, 'write_state':'not_started',
62
+ **safe_diagnostic(getattr(exc, '_socialcli_diagnostic', None)), 'content_write_state':'not_started'}
43
63
  value={'version':1,'status':'partial' if prepared else 'error','brand':brand,
44
64
  'batch':None,'prepared':prepared,'error':error_document(exc,read_only=True)}
45
65
  if getattr(exc, 'draft_failure', None):
@@ -79,8 +79,12 @@ class IdentityClient:
79
79
  self._token = broker_access_token(brand, platform, client, refresh=False)
80
80
  else:
81
81
  self._token = secret.get("access_token")
82
- except Exception:
83
- raise ReadError("credentials_invalid") from None
82
+ except Exception as exc:
83
+ from socialctl.agent_errors import error_document, with_error_code
84
+ detail = error_document(exc, read_only=True)
85
+ error = with_error_code(ReadError(detail['code']), exc, read_only=True)
86
+ error._socialcli_diagnostic = {**detail.get('diagnostic', {}), 'stage': 'credential_read'}
87
+ raise error from None
84
88
  if not isinstance(self._token, str) or not self._token:
85
89
  raise ReadError("credentials_missing")
86
90
  if self.metadata["expiry"]["expired"]:
@@ -102,12 +106,20 @@ class IdentityClient:
102
106
  response = self.client.get(f"{base}/{endpoint}", params=params,
103
107
  headers={"Authorization": f"Bearer {self._token}"}, timeout=30,
104
108
  follow_redirects=False)
105
- except httpx.TimeoutException:
106
- raise ReadError("timeout") from None
107
- except Exception:
108
- raise ReadError("transport_error") from None
109
+ except Exception as exc:
110
+ from socialctl.agent_errors import error_document, with_error_code
111
+ detail = error_document(exc, read_only=True)
112
+ legacy_code = 'timeout' if isinstance(exc, httpx.TimeoutException) else detail['code']
113
+ error = with_error_code(ReadError(legacy_code), exc, read_only=True)
114
+ error._socialcli_diagnostic = {**detail.get('diagnostic', {}),
115
+ 'stage': 'provider_read', 'request_method': 'GET'}
116
+ raise error from None
117
+ def rejected(code):
118
+ from socialctl.agent_errors import with_http_error_code
119
+ error = with_http_error_code(ReadError(code), response, read_only=True)
120
+ return error
109
121
  if response.status_code == 429:
110
- raise ReadError("rate_limited")
122
+ raise rejected("rate_limited")
111
123
  if response.status_code in {401, 403}:
112
124
  # YouTube quota exhaustion commonly uses HTTP403.
113
125
  try:
@@ -116,12 +128,12 @@ class IdentityClient:
116
128
  except Exception:
117
129
  reasons = []
118
130
  if set(reasons) & {"quotaExceeded", "dailyLimitExceeded", "rateLimitExceeded", "userRateLimitExceeded"}:
119
- raise ReadError("rate_limited")
120
- raise ReadError("permission_denied")
131
+ raise rejected("rate_limited")
132
+ raise rejected("permission_denied")
121
133
  if response.status_code == 404:
122
- raise ReadError("not_found_or_inaccessible")
134
+ raise rejected("not_found_or_inaccessible")
123
135
  if not response.is_success:
124
- raise ReadError("provider_error")
136
+ raise rejected("provider_error")
125
137
  try:
126
138
  data = response.json()
127
139
  if not isinstance(data, dict):
@@ -193,8 +205,13 @@ def auth_status(brand, platform, client):
193
205
  result["identity"] = probe.verify()
194
206
  except ReadError as exc:
195
207
  result["failure_class"] = exc.code
208
+ from socialctl.agent_errors import error_document
209
+ if getattr(exc, '_socialcli_error_code', None):
210
+ result['error_detail'] = error_document(exc, read_only=True)
196
211
  if probe is not None and probe.identity is not None:
197
212
  result["identity"] = probe.identity
198
- except Exception:
199
- result["failure_class"] = "credentials_invalid"
213
+ except Exception as exc:
214
+ from socialctl.agent_errors import error_document
215
+ result['error_detail'] = error_document(exc, read_only=True)
216
+ result["failure_class"] = result['error_detail']['code']
200
217
  return result
@@ -80,7 +80,7 @@ class InboxInterrupted(KeyboardInterrupt):
80
80
 
81
81
  def _partial_problem(report):
82
82
  detail = annotate_partial_read({**report, 'complete': False})['error_detail']
83
- return {'code': detail['code'], 'message': safe_error(report.get('error') or 'Read incomplete')}
83
+ return {**detail, 'message': safe_error(report.get('error') or 'Read incomplete')}
84
84
 
85
85
 
86
86
  def _exception_problem(exc):
@@ -90,6 +90,7 @@ def _exception_problem(exc):
90
90
  def _problem_document(problem):
91
91
  if isinstance(problem, dict):
92
92
  return {'code': problem.get('code', 'read_partial'),
93
+ **({'diagnostic': problem['diagnostic']} if problem.get('diagnostic') else {}),
93
94
  'message': safe_error(problem.get('message', 'Read incomplete'))}
94
95
  return {'message': safe_error(str(problem))}
95
96
 
@@ -202,6 +203,9 @@ def sync_brand_inbox(brand,*,since,budget,only=None):
202
203
  if resume and inventory['failure_class'] in {'invalid_cursor','provider_error'}:
203
204
  budget.check()
204
205
  inventory=sync_content(brand,platform,http,resume=False)
206
+ if inventory.get('failure_class'):
207
+ errors.append({'platform': 'youtube',
208
+ **inventory.get('error_detail', {'code': 'read_partial', 'message': 'Video inventory incomplete.'})})
205
209
  media=[v['remote_id'] for v in InventoryStore(brand,platform,account).list_items() if v.get('last_sync_id')==inventory['sync_id']]
206
210
  complete=inventory['complete'] and inventory['details_complete']
207
211
  client=YouTubeInboxClient(brand,http)
@@ -103,6 +103,7 @@ def sync_content(brand, platform, client, *, max_pages=20, resume=False):
103
103
  "next_cursor": None, "cursors": [], "seen_ids": [], "pages": 0,
104
104
  "missing_resource_ids": [], "resumable": True}
105
105
  run["failure_class"] = None
106
+ run.pop('error_detail', None)
106
107
  run["identity"] = identity
107
108
  for _ in range(max_pages):
108
109
  requested = run["next_cursor"]
@@ -110,6 +111,8 @@ def sync_content(brand, platform, client, *, max_pages=20, resume=False):
110
111
  items, following, missing, source = read_page(probe, requested)
111
112
  except ReadError as exc:
112
113
  run["failure_class"] = exc.code
114
+ from socialctl.agent_errors import error_document
115
+ run['error_detail'] = error_document(exc, read_only=True)
113
116
  break
114
117
  observed_at = now()
115
118
  for row in items:
@@ -147,6 +150,7 @@ def sync_content(brand, platform, client, *, max_pages=20, resume=False):
147
150
  return {"version": 1, "brand": brand.nombre, "platform": platform.value,
148
151
  "account_id": probe.account_id, "identity": identity, "sync_id": run["id"],
149
152
  "complete": run["complete"], "failure_class": run["failure_class"],
153
+ **({'error_detail': run['error_detail']} if run.get('failure_class') and run.get('error_detail') else {}),
150
154
  "pagination_complete": run["complete"],
151
155
  "details_complete": run["complete"] and not run["missing_resource_ids"],
152
156
  "resume_available": run["resumable"], "pages": run["pages"],
@@ -258,18 +258,28 @@ def _apply_operation(client, store, batch, op):
258
258
  # validation rejections get a fixed message, never arbitrary exception text.
259
259
  diagnostic = exc.diagnostic or {"error": "Metadata validation rejected the update before PUT."}
260
260
  _event(op, "write_rejected", **diagnostic)
261
- except Exception:
261
+ except Exception as exc:
262
262
  op.status = "uncertain"
263
- _event(op, "write_uncertain")
263
+ from socialctl.agent_errors import error_document
264
+ _event(op, "write_uncertain", error_detail_json=json.dumps(error_document(exc, read_only=False), sort_keys=True))
264
265
  else:
265
- try:
266
- verified = client.inspect(op.video_id)
267
- mismatches = _mismatch_paths(verified, op.after)
268
- op.status = "uncertain" if mismatches else "applied"
269
- _event(op, "read_back_" + op.status, mismatch_paths=", ".join(mismatches))
270
- except Exception:
271
- op.status = "uncertain"
272
- _event(op, "read_back_unavailable")
266
+ from socialctl.management.snippet_readback import readback_attempts
267
+ for attempt in readback_attempts():
268
+ try:
269
+ verified = client.inspect(op.video_id)
270
+ mismatches = _mismatch_paths(verified, op.after)
271
+ op.status = "uncertain" if mismatches else "applied"
272
+ _event(op, "read_back_" + op.status, mismatch_paths=", ".join(mismatches), attempt=str(attempt))
273
+ store.save(batch)
274
+ if not mismatches:
275
+ break
276
+ except Exception as exc:
277
+ from socialctl.agent_errors import error_document
278
+ detail = error_document(exc, read_only=True)
279
+ op.status = "uncertain"
280
+ _event(op, "read_back_unavailable", error_detail_json=json.dumps(detail, sort_keys=True), attempt=str(attempt))
281
+ if not detail['retryable']:
282
+ break
273
283
  store.save(batch)
274
284
 
275
285