oto-cli 1.2.0__tar.gz → 1.3.0__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.
Files changed (231) hide show
  1. {oto_cli-1.2.0 → oto_cli-1.3.0}/CLAUDE.md +2 -0
  2. {oto_cli-1.2.0 → oto_cli-1.3.0}/PKG-INFO +1 -1
  3. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/__init__.py +1 -1
  4. oto_cli-1.3.0/oto/commands/reddit.py +62 -0
  5. oto_cli-1.3.0/oto/tools/reddit/__init__.py +5 -0
  6. oto_cli-1.3.0/oto/tools/reddit/client.py +212 -0
  7. {oto_cli-1.2.0 → oto_cli-1.3.0}/.claude/settings.local.json +0 -0
  8. {oto_cli-1.2.0 → oto_cli-1.3.0}/.gitignore +0 -0
  9. {oto_cli-1.2.0 → oto_cli-1.3.0}/LICENSE +0 -0
  10. {oto_cli-1.2.0 → oto_cli-1.3.0}/README.md +0 -0
  11. {oto_cli-1.2.0 → oto_cli-1.3.0}/TODO.md +0 -0
  12. {oto_cli-1.2.0 → oto_cli-1.3.0}/docs/concepts.md +0 -0
  13. {oto_cli-1.2.0 → oto_cli-1.3.0}/docs/create-connector.md +0 -0
  14. {oto_cli-1.2.0 → oto_cli-1.3.0}/docs/gmail-oauth-setup.md +0 -0
  15. {oto_cli-1.2.0 → oto_cli-1.3.0}/docs/gmail.md +0 -0
  16. {oto_cli-1.2.0 → oto_cli-1.3.0}/docs/google-docs.md +0 -0
  17. {oto_cli-1.2.0 → oto_cli-1.3.0}/docs/google-service-account-setup.md +0 -0
  18. {oto_cli-1.2.0 → oto_cli-1.3.0}/docs/installation.md +0 -0
  19. {oto_cli-1.2.0 → oto_cli-1.3.0}/docs/zoho-desk-oauth-setup.md +0 -0
  20. {oto_cli-1.2.0 → oto_cli-1.3.0}/logo.svg +0 -0
  21. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/cli.py +0 -0
  22. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/__init__.py +0 -0
  23. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/anthropic.py +0 -0
  24. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/attio.py +0 -0
  25. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/audio.py +0 -0
  26. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/browser.py +0 -0
  27. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/company.py +0 -0
  28. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/config.py +0 -0
  29. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/enrichment.py +0 -0
  30. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/folk.py +0 -0
  31. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/gemini.py +0 -0
  32. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/google.py +0 -0
  33. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/notion.py +0 -0
  34. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/pdf.py +0 -0
  35. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/pennylane.py +0 -0
  36. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/search.py +0 -0
  37. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/serper.py +0 -0
  38. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/sirene.py +0 -0
  39. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/skills.py +0 -0
  40. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/whatsapp.py +0 -0
  41. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/zoho.py +0 -0
  42. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/zohodesk.py +0 -0
  43. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/config.py +0 -0
  44. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/scaleway_secrets.py +0 -0
  45. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/__init__.py +0 -0
  46. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/anthropic/__init__.py +0 -0
  47. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/anthropic/client.py +0 -0
  48. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/anthropic_batch/__init__.py +0 -0
  49. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/anthropic_batch/client.py +0 -0
  50. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/apollo/__init__.py +0 -0
  51. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/apollo/client.py +0 -0
  52. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/attio/__init__.py +0 -0
  53. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/attio/client.py +0 -0
  54. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/audio/__init__.py +0 -0
  55. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/audio/client.py +0 -0
  56. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/browser/__init__.py +0 -0
  57. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/browser/crunchbase.py +0 -0
  58. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/browser/g2.py +0 -0
  59. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/browser/google.py +0 -0
  60. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/browser/indeed.py +0 -0
  61. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/browser/linkedin/__init__.py +0 -0
  62. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/browser/linkedin/_js.py +0 -0
  63. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/browser/linkedin/client.py +0 -0
  64. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/browser/linkedin/scrape.py +0 -0
  65. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/browser/linkedin/search.py +0 -0
  66. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/browser/pappers.py +0 -0
  67. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/browser/sncf.py +0 -0
  68. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/clearbit/__init__.py +0 -0
  69. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/clearbit/client.py +0 -0
  70. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/collective/__init__.py +0 -0
  71. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/collective/client.py +0 -0
  72. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/common/__init__.py +0 -0
  73. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/common/rate_limiter.py +0 -0
  74. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/figma/__init__.py +0 -0
  75. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/figma/client.py +0 -0
  76. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/folk/__init__.py +0 -0
  77. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/folk/client.py +0 -0
  78. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/gemini/__init__.py +0 -0
  79. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/gemini/client.py +0 -0
  80. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/__init__.py +0 -0
  81. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/calendar/__init__.py +0 -0
  82. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/calendar/lib/__init__.py +0 -0
  83. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/calendar/lib/calendar_client.py +0 -0
  84. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/credentials.py +0 -0
  85. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/docs/get_section.py +0 -0
  86. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/docs/insert_section.py +0 -0
  87. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/docs/insert_text.py +0 -0
  88. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/docs/lib/__init__.py +0 -0
  89. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/docs/lib/docs_client.py +0 -0
  90. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/docs/lib/markdown_to_docs.py +0 -0
  91. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/docs/lib/markdown_to_html.py +0 -0
  92. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/docs/list_headings.py +0 -0
  93. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/docs/move_section.py +0 -0
  94. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/docs/replace_section.py +0 -0
  95. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/docs/sync/__init__.py +0 -0
  96. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/docs/sync/gdoc_sync.py +0 -0
  97. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/drive/README.md +0 -0
  98. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/drive/check_quota.py +0 -0
  99. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/drive/copy_file.py +0 -0
  100. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/drive/create_folder.py +0 -0
  101. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/drive/download_file.py +0 -0
  102. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/drive/export_doc.py +0 -0
  103. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/drive/export_slides.py +0 -0
  104. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/drive/extract_slides_content.py +0 -0
  105. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/drive/lib/drive_client.py +0 -0
  106. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/drive/list_files.py +0 -0
  107. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/drive/list_shared_drives.py +0 -0
  108. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/drive/move_file.py +0 -0
  109. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/drive/requirements.txt +0 -0
  110. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/drive/tool.yaml +0 -0
  111. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/drive/upload_file.py +0 -0
  112. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/gmail/__init__.py +0 -0
  113. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/gmail/get_message.py +0 -0
  114. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/gmail/lib/__init__.py +0 -0
  115. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/gmail/lib/gmail_client.py +0 -0
  116. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/gmail/list_messages.py +0 -0
  117. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/gmail/requirements.txt +0 -0
  118. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/gmail/search.py +0 -0
  119. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/gmail/send.py +0 -0
  120. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/gmail/tool.yaml +0 -0
  121. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/keep/__init__.py +0 -0
  122. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/keep/create_note.py +0 -0
  123. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/keep/get_master_token.py +0 -0
  124. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/keep/get_note.py +0 -0
  125. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/keep/lib/__init__.py +0 -0
  126. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/keep/lib/keep_client.py +0 -0
  127. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/keep/list_notes.py +0 -0
  128. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/keep/search.py +0 -0
  129. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/sheets/create_sheet_direct.py +0 -0
  130. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/sheets/create_sheet_from_csv.py +0 -0
  131. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/sheets/create_sheet_in_folder.py +0 -0
  132. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/sheets/create_sheet_via_drive.py +0 -0
  133. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/sheets/lib/__init__.py +0 -0
  134. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/sheets/lib/sheets_client.py +0 -0
  135. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/sheets/tool.yaml +0 -0
  136. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/slides/.folders +0 -0
  137. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/slides/321-LAYOUTS.md +0 -0
  138. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/slides/README.md +0 -0
  139. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/slides/USAGE-EDIT.md +0 -0
  140. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/slides/USAGE.md +0 -0
  141. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/slides/create-demo-presentation.py +0 -0
  142. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/slides/generate_slides.py +0 -0
  143. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/slides/lib/__init__.py +0 -0
  144. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/slides/lib/content_filler.py +0 -0
  145. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/slides/lib/layout_mappings.py +0 -0
  146. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/slides/lib/slides_client.py +0 -0
  147. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/slides/requirements.txt +0 -0
  148. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/slides/test-copy-and-edit.py +0 -0
  149. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/slides/test-copy-slide.py +0 -0
  150. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/slides/test-edit.py +0 -0
  151. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/slides/tool.yaml +0 -0
  152. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/groq/__init__.py +0 -0
  153. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/groq/client.py +0 -0
  154. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/hithorizons/__init__.py +0 -0
  155. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/hithorizons/client.py +0 -0
  156. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/hunter/__init__.py +0 -0
  157. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/hunter/client.py +0 -0
  158. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/kaspr/__init__.py +0 -0
  159. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/kaspr/client.py +0 -0
  160. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/lemlist/__init__.py +0 -0
  161. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/lemlist/client.py +0 -0
  162. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/mistral/__init__.py +0 -0
  163. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/mistral/client.py +0 -0
  164. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/naf/__init__.py +0 -0
  165. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/naf/suggester.py +0 -0
  166. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/notion/README.md +0 -0
  167. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/notion/append_blocks.py +0 -0
  168. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/notion/append_blocks_chunked.py +0 -0
  169. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/notion/count_database_entries.py +0 -0
  170. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/notion/create_database_from_csv.py +0 -0
  171. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/notion/create_page.py +0 -0
  172. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/notion/get_database.py +0 -0
  173. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/notion/get_page.py +0 -0
  174. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/notion/known-pages.md +0 -0
  175. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/notion/lib/markdown_converter.py +0 -0
  176. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/notion/lib/notion_client.py +0 -0
  177. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/notion/list_teamspaces.py +0 -0
  178. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/notion/notion +0 -0
  179. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/notion/query_database.py +0 -0
  180. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/notion/requirements.txt +0 -0
  181. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/notion/search.py +0 -0
  182. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/notion/tool.yaml +0 -0
  183. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/notion/update_page.py +0 -0
  184. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/pdf/__init__.py +0 -0
  185. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/pdf/templates/default.css +0 -0
  186. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/pennylane/__init__.py +0 -0
  187. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/pennylane/client.py +0 -0
  188. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/phantombuster/__init__.py +0 -0
  189. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/phantombuster/client.py +0 -0
  190. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/resend/__init__.py +0 -0
  191. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/resend/client.py +0 -0
  192. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/serpapi/__init__.py +0 -0
  193. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/serpapi/client.py +0 -0
  194. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/serper/__init__.py +0 -0
  195. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/serper/client.py +0 -0
  196. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/sirene/__init__.py +0 -0
  197. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/sirene/client.py +0 -0
  198. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/sirene/data/naf_codes.txt +0 -0
  199. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/sirene/entreprises.py +0 -0
  200. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/sirene/stock.py +0 -0
  201. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/slack/__init__.py +0 -0
  202. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/slack/client.py +0 -0
  203. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/unsplash/__init__.py +0 -0
  204. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/unsplash/client.py +0 -0
  205. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/whatsapp/__init__.py +0 -0
  206. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/whatsapp/client.py +0 -0
  207. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/whatsapp/node/package-lock.json +0 -0
  208. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/whatsapp/node/package.json +0 -0
  209. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/whatsapp/node/whatsapp.mjs +0 -0
  210. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/wttj/__init__.py +0 -0
  211. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/wttj/client.py +0 -0
  212. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/zerobounce/__init__.py +0 -0
  213. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/zerobounce/client.py +0 -0
  214. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/zoho/__init__.py +0 -0
  215. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/zoho/client.py +0 -0
  216. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/zohodesk/__init__.py +0 -0
  217. {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/zohodesk/client.py +0 -0
  218. {oto_cli-1.2.0 → oto_cli-1.3.0}/pyproject.toml +0 -0
  219. {oto_cli-1.2.0 → oto_cli-1.3.0}/skills/oto-anthropic/SKILL.md +0 -0
  220. {oto_cli-1.2.0 → oto_cli-1.3.0}/skills/oto-browser/SKILL.md +0 -0
  221. {oto_cli-1.2.0 → oto_cli-1.3.0}/skills/oto-cli/SKILL.md +0 -0
  222. {oto_cli-1.2.0 → oto_cli-1.3.0}/skills/oto-enrichment/SKILL.md +0 -0
  223. {oto_cli-1.2.0 → oto_cli-1.3.0}/skills/oto-google/SKILL.md +0 -0
  224. {oto_cli-1.2.0 → oto_cli-1.3.0}/skills/oto-notion/SKILL.md +0 -0
  225. {oto_cli-1.2.0 → oto_cli-1.3.0}/skills/oto-pennylane/SKILL.md +0 -0
  226. {oto_cli-1.2.0 → oto_cli-1.3.0}/skills/oto-search/SKILL.md +0 -0
  227. {oto_cli-1.2.0 → oto_cli-1.3.0}/skills/oto-sirene/SKILL.md +0 -0
  228. {oto_cli-1.2.0 → oto_cli-1.3.0}/skills/oto-task/SKILL.md +0 -0
  229. {oto_cli-1.2.0 → oto_cli-1.3.0}/skills/oto-whatsapp/SKILL.md +0 -0
  230. {oto_cli-1.2.0 → oto_cli-1.3.0}/skills/oto-zoho/SKILL.md +0 -0
  231. {oto_cli-1.2.0 → oto_cli-1.3.0}/skills/oto-zohodesk/SKILL.md +0 -0
@@ -29,6 +29,7 @@ oto/
29
29
  │ │ ├── google.py # drive, docs, sheets, slides, gmail, calendar, auth
30
30
  │ │ ├── notion.py # search, page, database
31
31
  │ │ ├── browser.py # linkedin, crunchbase, pappers, indeed, g2, google
32
+ │ │ ├── reddit.py # Reddit JSON API (subreddit, search, post)
32
33
  │ │ ├── sirene.py # SIRENE API (search, get, stock)
33
34
  │ │ ├── search.py # facade: dispatches to serper or browser via config
34
35
  │ │ ├── serper.py # direct Serper API (web, news, scrape, suggestions)
@@ -50,6 +51,7 @@ oto/
50
51
  │ ├── google/ # gmail, drive, docs, sheets, slides, calendar, keep
51
52
  │ ├── notion/ # pages, databases, search
52
53
  │ ├── browser/ # linkedin, crunchbase, pappers, indeed, g2, google
54
+ │ ├── reddit/ # Reddit JSON API (no auth)
53
55
  │ ├── whatsapp/ # Node.js bridge (whatsapp-web.js)
54
56
  │ ├── sirene/ # INSEE SIRENE API
55
57
  │ ├── serper/ # Google search (web, news)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oto-cli
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: CLI toolkit for AI agents — covers the long tail of SaaS APIs that don't have a CLI
5
5
  Project-URL: Repository, https://github.com/AlexisLaporte/oto
6
6
  Author: Alexis Laporte
@@ -1,3 +1,3 @@
1
1
  """Oto - CLI tools for automation."""
2
2
 
3
- __version__ = "1.2.0"
3
+ __version__ = "1.3.0"
@@ -0,0 +1,62 @@
1
+ """Reddit JSON API commands (subreddit, search, post)."""
2
+
3
+ import json
4
+ from typing import Optional
5
+
6
+ import typer
7
+
8
+ app = typer.Typer(help="Reddit (public JSON API, no auth)")
9
+
10
+
11
+ @app.command("subreddit")
12
+ def subreddit(
13
+ name: str = typer.Argument(..., help="Subreddit name (without /r/)"),
14
+ sort: str = typer.Option("hot", help="hot|new|top|rising|controversial"),
15
+ limit: int = typer.Option(25, "--limit", "-n", help="Max posts (max 100)"),
16
+ time: Optional[str] = typer.Option(None, help="hour|day|week|month|year|all (top/controversial only)"),
17
+ after: Optional[str] = typer.Option(None, help="Pagination cursor"),
18
+ ):
19
+ """List posts from a subreddit."""
20
+ from oto.tools.reddit import RedditClient
21
+ result = RedditClient().subreddit(name, sort=sort, limit=limit, time=time, after=after)
22
+ print(json.dumps(result, indent=2, ensure_ascii=False))
23
+
24
+
25
+ @app.command("search")
26
+ def search(
27
+ query: str = typer.Argument(..., help="Search query"),
28
+ subreddit: Optional[str] = typer.Option(None, "--sub", "-s", help="Restrict to one subreddit"),
29
+ sort: str = typer.Option("relevance", help="relevance|hot|top|new|comments"),
30
+ time: str = typer.Option("all", help="hour|day|week|month|year|all"),
31
+ limit: int = typer.Option(25, "--limit", "-n", help="Max results (max 100)"),
32
+ after: Optional[str] = typer.Option(None, help="Pagination cursor"),
33
+ ):
34
+ """Search Reddit posts (globally or in one sub)."""
35
+ from oto.tools.reddit import RedditClient
36
+ result = RedditClient().search(
37
+ query, subreddit=subreddit, sort=sort, time=time, limit=limit, after=after
38
+ )
39
+ print(json.dumps(result, indent=2, ensure_ascii=False))
40
+
41
+
42
+ @app.command("search-subs")
43
+ def search_subs(
44
+ query: str = typer.Argument(..., help="Search query"),
45
+ limit: int = typer.Option(25, "--limit", "-n", help="Max results"),
46
+ ):
47
+ """Search subreddits by name/description."""
48
+ from oto.tools.reddit import RedditClient
49
+ result = RedditClient().search_subreddits(query, limit=limit)
50
+ print(json.dumps(result, indent=2, ensure_ascii=False))
51
+
52
+
53
+ @app.command("post")
54
+ def post(
55
+ url_or_id: str = typer.Argument(..., help="Reddit post URL, permalink or id"),
56
+ comments: int = typer.Option(100, "--comments", "-c", help="Max comments"),
57
+ depth: int = typer.Option(5, "--depth", "-d", help="Comment tree depth"),
58
+ ):
59
+ """Fetch a post and its comments tree."""
60
+ from oto.tools.reddit import RedditClient
61
+ result = RedditClient().post(url_or_id, comment_limit=comments, depth=depth)
62
+ print(json.dumps(result, indent=2, ensure_ascii=False))
@@ -0,0 +1,5 @@
1
+ """Reddit JSON API client (no auth, public reads)."""
2
+
3
+ from .client import RedditClient
4
+
5
+ __all__ = ["RedditClient"]
@@ -0,0 +1,212 @@
1
+ """
2
+ Reddit public JSON API client.
3
+
4
+ Uses the unauthenticated `*.json` endpoints exposed by reddit.com — no OAuth,
5
+ no API key. Reddit blocks the default `python-requests` User-Agent, so a
6
+ custom UA is sent on every call.
7
+ """
8
+
9
+ from typing import Any, Dict, List, Optional
10
+ from urllib.parse import quote_plus
11
+
12
+ import requests
13
+
14
+ from ... import __version__
15
+
16
+
17
+ class RedditClient:
18
+ """
19
+ Reddit JSON API (public reads only).
20
+
21
+ Covers:
22
+ - subreddit feeds (hot/new/top/rising)
23
+ - search across all of Reddit or restricted to a subreddit
24
+ - post + flat comments tree
25
+ - subreddit discovery search
26
+ """
27
+
28
+ BASE_URL = "https://www.reddit.com"
29
+ DEFAULT_UA = f"oto-cli/{__version__} (by /u/oto-bot)"
30
+
31
+ def __init__(self, user_agent: Optional[str] = None, timeout: int = 15):
32
+ self.session = requests.Session()
33
+ self.session.headers["User-Agent"] = user_agent or self.DEFAULT_UA
34
+ self.timeout = timeout
35
+
36
+ def _get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
37
+ url = f"{self.BASE_URL}{path}" if path.startswith("/") else path
38
+ r = self.session.get(url, params=params, timeout=self.timeout, allow_redirects=True)
39
+ r.raise_for_status()
40
+ return r.json()
41
+
42
+ # ── Listings ──────────────────────────────────────────────────────────
43
+
44
+ def subreddit(
45
+ self,
46
+ name: str,
47
+ sort: str = "hot",
48
+ limit: int = 25,
49
+ time: Optional[str] = None,
50
+ after: Optional[str] = None,
51
+ ) -> Dict[str, Any]:
52
+ """
53
+ List posts from a subreddit.
54
+
55
+ sort: hot|new|top|rising|controversial
56
+ time: hour|day|week|month|year|all (only for top/controversial)
57
+ """
58
+ if sort not in {"hot", "new", "top", "rising", "controversial"}:
59
+ raise ValueError(f"invalid sort: {sort}")
60
+ params: Dict[str, Any] = {"limit": min(limit, 100), "raw_json": 1}
61
+ if time and sort in {"top", "controversial"}:
62
+ params["t"] = time
63
+ if after:
64
+ params["after"] = after
65
+ data = self._get(f"/r/{name}/{sort}.json", params)
66
+ return _parse_listing(data)
67
+
68
+ def search(
69
+ self,
70
+ query: str,
71
+ subreddit: Optional[str] = None,
72
+ sort: str = "relevance",
73
+ time: str = "all",
74
+ limit: int = 25,
75
+ after: Optional[str] = None,
76
+ ) -> Dict[str, Any]:
77
+ """
78
+ Search posts. If `subreddit` is set, restricts to that sub.
79
+
80
+ sort: relevance|hot|top|new|comments
81
+ time: hour|day|week|month|year|all
82
+ """
83
+ params: Dict[str, Any] = {
84
+ "q": query,
85
+ "sort": sort,
86
+ "t": time,
87
+ "limit": min(limit, 100),
88
+ "raw_json": 1,
89
+ "type": "link",
90
+ }
91
+ if after:
92
+ params["after"] = after
93
+ if subreddit:
94
+ params["restrict_sr"] = "1"
95
+ path = f"/r/{subreddit}/search.json"
96
+ else:
97
+ path = "/search.json"
98
+ return _parse_listing(self._get(path, params))
99
+
100
+ def search_subreddits(self, query: str, limit: int = 25) -> Dict[str, Any]:
101
+ """Discover subreddits by name/description match."""
102
+ data = self._get(
103
+ "/subreddits/search.json",
104
+ {"q": query, "limit": min(limit, 100), "raw_json": 1},
105
+ )
106
+ return _parse_listing(data)
107
+
108
+ # ── Post + comments ───────────────────────────────────────────────────
109
+
110
+ def post(self, url_or_id: str, comment_limit: int = 100, depth: int = 5) -> Dict[str, Any]:
111
+ """
112
+ Fetch a post and its comments tree.
113
+
114
+ Accepts a full reddit URL, a permalink (/r/x/comments/id/...) or just the post id.
115
+ """
116
+ path = _post_path(url_or_id)
117
+ data = self._get(
118
+ f"{path}.json",
119
+ {"limit": comment_limit, "depth": depth, "raw_json": 1},
120
+ )
121
+ if not isinstance(data, list) or len(data) < 2:
122
+ raise RuntimeError(f"unexpected post payload: {type(data).__name__}")
123
+ post = _parse_post_data(data[0]["data"]["children"][0]["data"])
124
+ comments = _parse_comments(data[1]["data"]["children"])
125
+ return {"post": post, "comments": comments}
126
+
127
+
128
+ # ── Parsing helpers ───────────────────────────────────────────────────────
129
+
130
+
131
+ def _parse_listing(data: Dict[str, Any]) -> Dict[str, Any]:
132
+ payload = data.get("data", {})
133
+ children = payload.get("children", [])
134
+ items = [_parse_child(c) for c in children]
135
+ return {
136
+ "items": [i for i in items if i],
137
+ "after": payload.get("after"),
138
+ "before": payload.get("before"),
139
+ }
140
+
141
+
142
+ def _parse_child(child: Dict[str, Any]) -> Optional[Dict[str, Any]]:
143
+ kind = child.get("kind")
144
+ d = child.get("data") or {}
145
+ if kind == "t3":
146
+ return _parse_post_data(d)
147
+ if kind == "t5":
148
+ return {
149
+ "kind": "subreddit",
150
+ "name": d.get("display_name"),
151
+ "title": d.get("title"),
152
+ "subscribers": d.get("subscribers"),
153
+ "description": d.get("public_description"),
154
+ "url": f"https://www.reddit.com{d.get('url', '')}",
155
+ "over_18": d.get("over18"),
156
+ }
157
+ return None
158
+
159
+
160
+ def _parse_post_data(d: Dict[str, Any]) -> Dict[str, Any]:
161
+ return {
162
+ "kind": "post",
163
+ "id": d.get("id"),
164
+ "title": d.get("title"),
165
+ "author": d.get("author"),
166
+ "subreddit": d.get("subreddit"),
167
+ "score": d.get("score"),
168
+ "upvote_ratio": d.get("upvote_ratio"),
169
+ "num_comments": d.get("num_comments"),
170
+ "created_utc": d.get("created_utc"),
171
+ "permalink": f"https://www.reddit.com{d.get('permalink', '')}",
172
+ "url": d.get("url_overridden_by_dest") or d.get("url"),
173
+ "is_self": d.get("is_self"),
174
+ "selftext": d.get("selftext") or None,
175
+ "flair": d.get("link_flair_text"),
176
+ "over_18": d.get("over_18"),
177
+ }
178
+
179
+
180
+ def _parse_comments(children: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
181
+ out: List[Dict[str, Any]] = []
182
+ for c in children:
183
+ if c.get("kind") != "t1":
184
+ continue
185
+ d = c.get("data") or {}
186
+ replies = d.get("replies")
187
+ sub = []
188
+ if isinstance(replies, dict):
189
+ sub = _parse_comments(replies.get("data", {}).get("children", []))
190
+ out.append({
191
+ "id": d.get("id"),
192
+ "author": d.get("author"),
193
+ "score": d.get("score"),
194
+ "created_utc": d.get("created_utc"),
195
+ "body": d.get("body"),
196
+ "permalink": f"https://www.reddit.com{d.get('permalink', '')}",
197
+ "replies": sub,
198
+ })
199
+ return out
200
+
201
+
202
+ def _post_path(url_or_id: str) -> str:
203
+ s = url_or_id.strip()
204
+ if s.startswith("http"):
205
+ # strip protocol+host, drop trailing slash
206
+ from urllib.parse import urlparse
207
+ path = urlparse(s).path.rstrip("/")
208
+ return path
209
+ if s.startswith("/r/"):
210
+ return s.rstrip("/")
211
+ # bare id
212
+ return f"/comments/{quote_plus(s)}"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes