oto-cli 1.3.0__tar.gz → 1.4.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 (267) hide show
  1. oto_cli-1.4.0/.github/workflows/deploy.yml +34 -0
  2. {oto_cli-1.3.0 → oto_cli-1.4.0}/CLAUDE.md +30 -10
  3. {oto_cli-1.3.0 → oto_cli-1.4.0}/PKG-INFO +10 -6
  4. {oto_cli-1.3.0 → oto_cli-1.4.0}/README.md +8 -4
  5. oto_cli-1.4.0/docs/installation.md +135 -0
  6. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/__init__.py +1 -1
  7. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/commands/attio.py +66 -0
  8. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/commands/browser.py +77 -0
  9. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/commands/config.py +1 -0
  10. oto_cli-1.4.0/oto/commands/culture.py +107 -0
  11. oto_cli-1.4.0/oto/commands/data.py +148 -0
  12. oto_cli-1.4.0/oto/commands/dvf.py +60 -0
  13. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/commands/enrichment.py +4 -2
  14. oto_cli-1.4.0/oto/commands/fr.py +263 -0
  15. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/commands/gemini.py +6 -6
  16. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/commands/google.py +47 -2
  17. oto_cli-1.4.0/oto/commands/ninja.py +208 -0
  18. oto_cli-1.4.0/oto/commands/openai.py +157 -0
  19. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/commands/pennylane.py +25 -3
  20. oto_cli-1.4.0/oto/commands/slack.py +197 -0
  21. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/config.py +20 -6
  22. oto_cli-1.4.0/oto/sops_secrets.py +125 -0
  23. oto_cli-1.4.0/oto/tools/attio/client.py +601 -0
  24. oto_cli-1.4.0/oto/tools/boamp/__init__.py +3 -0
  25. oto_cli-1.4.0/oto/tools/boamp/client.py +69 -0
  26. oto_cli-1.4.0/oto/tools/bodacc/__init__.py +3 -0
  27. oto_cli-1.4.0/oto/tools/bodacc/client.py +101 -0
  28. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/browser/linkedin/_js.py +31 -0
  29. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/browser/linkedin/client.py +28 -1
  30. oto_cli-1.4.0/oto/tools/browser/linkedin/outreach.py +165 -0
  31. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/browser/linkedin/scrape.py +25 -7
  32. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/browser/linkedin/search.py +5 -0
  33. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/common/rate_limiter.py +25 -7
  34. oto_cli-1.4.0/oto/tools/culture/__init__.py +10 -0
  35. oto_cli-1.4.0/oto/tools/culture/opendatasoft.py +117 -0
  36. oto_cli-1.4.0/oto/tools/culture/spectacle.py +174 -0
  37. oto_cli-1.4.0/oto/tools/datastore/__init__.py +4 -0
  38. oto_cli-1.4.0/oto/tools/datastore/client.py +99 -0
  39. oto_cli-1.4.0/oto/tools/dvf/__init__.py +3 -0
  40. oto_cli-1.4.0/oto/tools/dvf/client.py +300 -0
  41. oto_cli-1.4.0/oto/tools/fullenrich/client.py +184 -0
  42. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/gemini/client.py +4 -4
  43. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/docs/lib/markdown_to_html.py +12 -1
  44. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/drive/lib/drive_client.py +59 -0
  45. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/gmail/lib/gmail_client.py +65 -7
  46. oto_cli-1.4.0/oto/tools/google/sheets/lib/__init__.py +0 -0
  47. oto_cli-1.4.0/oto/tools/inpi/__init__.py +3 -0
  48. oto_cli-1.4.0/oto/tools/inpi/client.py +44 -0
  49. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/kaspr/client.py +25 -8
  50. oto_cli-1.4.0/oto/tools/ninja/__init__.py +4 -0
  51. oto_cli-1.4.0/oto/tools/ninja/client.py +99 -0
  52. oto_cli-1.4.0/oto/tools/openai/__init__.py +5 -0
  53. oto_cli-1.4.0/oto/tools/openai/client.py +167 -0
  54. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/pennylane/client.py +34 -0
  55. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/serper/client.py +15 -3
  56. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/sirene/entreprises.py +6 -1
  57. oto_cli-1.4.0/oto/tools/sirene/stock.py +184 -0
  58. oto_cli-1.4.0/oto/tools/slack/client.py +383 -0
  59. oto_cli-1.4.0/oto/tools/supabase/__init__.py +4 -0
  60. oto_cli-1.4.0/oto/tools/supabase/client.py +98 -0
  61. {oto_cli-1.3.0 → oto_cli-1.4.0}/pyproject.toml +1 -1
  62. oto_cli-1.4.0/scripts/migrate_timetrack.py +103 -0
  63. {oto_cli-1.3.0 → oto_cli-1.4.0}/skills/oto-browser/SKILL.md +30 -1
  64. oto_cli-1.3.0/docs/installation.md +0 -134
  65. oto_cli-1.3.0/oto/commands/company.py +0 -23
  66. oto_cli-1.3.0/oto/commands/sirene.py +0 -201
  67. oto_cli-1.3.0/oto/tools/attio/client.py +0 -259
  68. oto_cli-1.3.0/oto/tools/sirene/stock.py +0 -377
  69. oto_cli-1.3.0/oto/tools/slack/client.py +0 -202
  70. {oto_cli-1.3.0 → oto_cli-1.4.0}/.claude/settings.local.json +0 -0
  71. {oto_cli-1.3.0 → oto_cli-1.4.0}/.gitignore +0 -0
  72. {oto_cli-1.3.0 → oto_cli-1.4.0}/LICENSE +0 -0
  73. {oto_cli-1.3.0 → oto_cli-1.4.0}/TODO.md +0 -0
  74. {oto_cli-1.3.0 → oto_cli-1.4.0}/docs/concepts.md +0 -0
  75. {oto_cli-1.3.0 → oto_cli-1.4.0}/docs/create-connector.md +0 -0
  76. {oto_cli-1.3.0 → oto_cli-1.4.0}/docs/gmail-oauth-setup.md +0 -0
  77. {oto_cli-1.3.0 → oto_cli-1.4.0}/docs/gmail.md +0 -0
  78. {oto_cli-1.3.0 → oto_cli-1.4.0}/docs/google-docs.md +0 -0
  79. {oto_cli-1.3.0 → oto_cli-1.4.0}/docs/google-service-account-setup.md +0 -0
  80. {oto_cli-1.3.0 → oto_cli-1.4.0}/docs/zoho-desk-oauth-setup.md +0 -0
  81. {oto_cli-1.3.0 → oto_cli-1.4.0}/logo.svg +0 -0
  82. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/cli.py +0 -0
  83. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/commands/__init__.py +0 -0
  84. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/commands/anthropic.py +0 -0
  85. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/commands/audio.py +0 -0
  86. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/commands/folk.py +0 -0
  87. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/commands/notion.py +0 -0
  88. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/commands/pdf.py +0 -0
  89. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/commands/reddit.py +0 -0
  90. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/commands/search.py +0 -0
  91. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/commands/serper.py +0 -0
  92. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/commands/skills.py +0 -0
  93. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/commands/whatsapp.py +0 -0
  94. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/commands/zoho.py +0 -0
  95. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/commands/zohodesk.py +0 -0
  96. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/scaleway_secrets.py +0 -0
  97. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/__init__.py +0 -0
  98. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/anthropic/__init__.py +0 -0
  99. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/anthropic/client.py +0 -0
  100. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/anthropic_batch/__init__.py +0 -0
  101. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/anthropic_batch/client.py +0 -0
  102. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/apollo/__init__.py +0 -0
  103. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/apollo/client.py +0 -0
  104. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/attio/__init__.py +0 -0
  105. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/audio/__init__.py +0 -0
  106. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/audio/client.py +0 -0
  107. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/browser/__init__.py +0 -0
  108. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/browser/crunchbase.py +0 -0
  109. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/browser/g2.py +0 -0
  110. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/browser/google.py +0 -0
  111. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/browser/indeed.py +0 -0
  112. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/browser/linkedin/__init__.py +0 -0
  113. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/browser/pappers.py +0 -0
  114. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/browser/sncf.py +0 -0
  115. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/clearbit/__init__.py +0 -0
  116. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/clearbit/client.py +0 -0
  117. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/collective/__init__.py +0 -0
  118. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/collective/client.py +0 -0
  119. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/common/__init__.py +0 -0
  120. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/figma/__init__.py +0 -0
  121. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/figma/client.py +0 -0
  122. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/folk/__init__.py +0 -0
  123. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/folk/client.py +0 -0
  124. {oto_cli-1.3.0/oto/tools/google/calendar → oto_cli-1.4.0/oto/tools/fullenrich}/__init__.py +0 -0
  125. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/gemini/__init__.py +0 -0
  126. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/__init__.py +0 -0
  127. {oto_cli-1.3.0/oto/tools/google/calendar/lib → oto_cli-1.4.0/oto/tools/google/calendar}/__init__.py +0 -0
  128. {oto_cli-1.3.0/oto/tools/google/docs → oto_cli-1.4.0/oto/tools/google/calendar}/lib/__init__.py +0 -0
  129. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/calendar/lib/calendar_client.py +0 -0
  130. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/credentials.py +0 -0
  131. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/docs/get_section.py +0 -0
  132. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/docs/insert_section.py +0 -0
  133. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/docs/insert_text.py +0 -0
  134. {oto_cli-1.3.0/oto/tools/google/keep → oto_cli-1.4.0/oto/tools/google/docs/lib}/__init__.py +0 -0
  135. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/docs/lib/docs_client.py +0 -0
  136. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/docs/lib/markdown_to_docs.py +0 -0
  137. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/docs/list_headings.py +0 -0
  138. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/docs/move_section.py +0 -0
  139. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/docs/replace_section.py +0 -0
  140. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/docs/sync/__init__.py +0 -0
  141. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/docs/sync/gdoc_sync.py +0 -0
  142. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/drive/README.md +0 -0
  143. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/drive/check_quota.py +0 -0
  144. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/drive/copy_file.py +0 -0
  145. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/drive/create_folder.py +0 -0
  146. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/drive/download_file.py +0 -0
  147. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/drive/export_doc.py +0 -0
  148. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/drive/export_slides.py +0 -0
  149. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/drive/extract_slides_content.py +0 -0
  150. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/drive/list_files.py +0 -0
  151. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/drive/list_shared_drives.py +0 -0
  152. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/drive/move_file.py +0 -0
  153. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/drive/requirements.txt +0 -0
  154. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/drive/tool.yaml +0 -0
  155. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/drive/upload_file.py +0 -0
  156. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/gmail/__init__.py +0 -0
  157. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/gmail/get_message.py +0 -0
  158. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/gmail/lib/__init__.py +0 -0
  159. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/gmail/list_messages.py +0 -0
  160. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/gmail/requirements.txt +0 -0
  161. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/gmail/search.py +0 -0
  162. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/gmail/send.py +0 -0
  163. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/gmail/tool.yaml +0 -0
  164. {oto_cli-1.3.0/oto/tools/google/keep/lib → oto_cli-1.4.0/oto/tools/google/keep}/__init__.py +0 -0
  165. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/keep/create_note.py +0 -0
  166. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/keep/get_master_token.py +0 -0
  167. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/keep/get_note.py +0 -0
  168. {oto_cli-1.3.0/oto/tools/google/sheets → oto_cli-1.4.0/oto/tools/google/keep}/lib/__init__.py +0 -0
  169. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/keep/lib/keep_client.py +0 -0
  170. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/keep/list_notes.py +0 -0
  171. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/keep/search.py +0 -0
  172. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/sheets/create_sheet_direct.py +0 -0
  173. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/sheets/create_sheet_from_csv.py +0 -0
  174. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/sheets/create_sheet_in_folder.py +0 -0
  175. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/sheets/create_sheet_via_drive.py +0 -0
  176. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/sheets/lib/sheets_client.py +0 -0
  177. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/sheets/tool.yaml +0 -0
  178. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/slides/.folders +0 -0
  179. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/slides/321-LAYOUTS.md +0 -0
  180. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/slides/README.md +0 -0
  181. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/slides/USAGE-EDIT.md +0 -0
  182. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/slides/USAGE.md +0 -0
  183. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/slides/create-demo-presentation.py +0 -0
  184. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/slides/generate_slides.py +0 -0
  185. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/slides/lib/__init__.py +0 -0
  186. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/slides/lib/content_filler.py +0 -0
  187. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/slides/lib/layout_mappings.py +0 -0
  188. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/slides/lib/slides_client.py +0 -0
  189. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/slides/requirements.txt +0 -0
  190. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/slides/test-copy-and-edit.py +0 -0
  191. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/slides/test-copy-slide.py +0 -0
  192. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/slides/test-edit.py +0 -0
  193. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/google/slides/tool.yaml +0 -0
  194. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/groq/__init__.py +0 -0
  195. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/groq/client.py +0 -0
  196. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/hithorizons/__init__.py +0 -0
  197. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/hithorizons/client.py +0 -0
  198. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/hunter/__init__.py +0 -0
  199. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/hunter/client.py +0 -0
  200. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/kaspr/__init__.py +0 -0
  201. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/lemlist/__init__.py +0 -0
  202. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/lemlist/client.py +0 -0
  203. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/mistral/__init__.py +0 -0
  204. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/mistral/client.py +0 -0
  205. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/naf/__init__.py +0 -0
  206. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/naf/suggester.py +0 -0
  207. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/notion/README.md +0 -0
  208. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/notion/append_blocks.py +0 -0
  209. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/notion/append_blocks_chunked.py +0 -0
  210. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/notion/count_database_entries.py +0 -0
  211. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/notion/create_database_from_csv.py +0 -0
  212. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/notion/create_page.py +0 -0
  213. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/notion/get_database.py +0 -0
  214. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/notion/get_page.py +0 -0
  215. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/notion/known-pages.md +0 -0
  216. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/notion/lib/markdown_converter.py +0 -0
  217. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/notion/lib/notion_client.py +0 -0
  218. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/notion/list_teamspaces.py +0 -0
  219. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/notion/notion +0 -0
  220. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/notion/query_database.py +0 -0
  221. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/notion/requirements.txt +0 -0
  222. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/notion/search.py +0 -0
  223. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/notion/tool.yaml +0 -0
  224. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/notion/update_page.py +0 -0
  225. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/pdf/__init__.py +0 -0
  226. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/pdf/templates/default.css +0 -0
  227. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/pennylane/__init__.py +0 -0
  228. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/phantombuster/__init__.py +0 -0
  229. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/phantombuster/client.py +0 -0
  230. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/reddit/__init__.py +0 -0
  231. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/reddit/client.py +0 -0
  232. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/resend/__init__.py +0 -0
  233. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/resend/client.py +0 -0
  234. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/serpapi/__init__.py +0 -0
  235. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/serpapi/client.py +0 -0
  236. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/serper/__init__.py +0 -0
  237. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/sirene/__init__.py +0 -0
  238. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/sirene/client.py +0 -0
  239. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/sirene/data/naf_codes.txt +0 -0
  240. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/slack/__init__.py +0 -0
  241. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/unsplash/__init__.py +0 -0
  242. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/unsplash/client.py +0 -0
  243. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/whatsapp/__init__.py +0 -0
  244. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/whatsapp/client.py +0 -0
  245. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/whatsapp/node/package-lock.json +0 -0
  246. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/whatsapp/node/package.json +0 -0
  247. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/whatsapp/node/whatsapp.mjs +0 -0
  248. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/wttj/__init__.py +0 -0
  249. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/wttj/client.py +0 -0
  250. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/zerobounce/__init__.py +0 -0
  251. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/zerobounce/client.py +0 -0
  252. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/zoho/__init__.py +0 -0
  253. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/zoho/client.py +0 -0
  254. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/zohodesk/__init__.py +0 -0
  255. {oto_cli-1.3.0 → oto_cli-1.4.0}/oto/tools/zohodesk/client.py +0 -0
  256. {oto_cli-1.3.0 → oto_cli-1.4.0}/skills/oto-anthropic/SKILL.md +0 -0
  257. {oto_cli-1.3.0 → oto_cli-1.4.0}/skills/oto-cli/SKILL.md +0 -0
  258. {oto_cli-1.3.0 → oto_cli-1.4.0}/skills/oto-enrichment/SKILL.md +0 -0
  259. {oto_cli-1.3.0 → oto_cli-1.4.0}/skills/oto-google/SKILL.md +0 -0
  260. {oto_cli-1.3.0 → oto_cli-1.4.0}/skills/oto-notion/SKILL.md +0 -0
  261. {oto_cli-1.3.0 → oto_cli-1.4.0}/skills/oto-pennylane/SKILL.md +0 -0
  262. {oto_cli-1.3.0 → oto_cli-1.4.0}/skills/oto-search/SKILL.md +0 -0
  263. {oto_cli-1.3.0 → oto_cli-1.4.0}/skills/oto-sirene/SKILL.md +0 -0
  264. {oto_cli-1.3.0 → oto_cli-1.4.0}/skills/oto-task/SKILL.md +0 -0
  265. {oto_cli-1.3.0 → oto_cli-1.4.0}/skills/oto-whatsapp/SKILL.md +0 -0
  266. {oto_cli-1.3.0 → oto_cli-1.4.0}/skills/oto-zoho/SKILL.md +0 -0
  267. {oto_cli-1.3.0 → oto_cli-1.4.0}/skills/oto-zohodesk/SKILL.md +0 -0
@@ -0,0 +1,34 @@
1
+ name: Deploy
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ workflow_dispatch: {}
7
+
8
+ concurrency:
9
+ group: deploy-prod
10
+ cancel-in-progress: false
11
+
12
+ jobs:
13
+ deploy:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - name: Deploy to tuls.me
17
+ uses: appleboy/ssh-action@v1.0.3
18
+ with:
19
+ host: 51.15.225.121
20
+ username: root
21
+ key: ${{ secrets.SSH_PRIVATE_KEY }}
22
+ script: |
23
+ set -e
24
+ cd /opt/oto-cli
25
+ git fetch origin main
26
+ git reset --hard origin/main
27
+ # oto-cli is installed editable via `pip install -e .` in oto-mcp's
28
+ # venv — no reinstall needed on pull unless deps change. We still
29
+ # restart oto-mcp so new modules (e.g. oto.tools.culture) are
30
+ # picked up by the registered MCP tools.
31
+ systemctl restart oto-mcp
32
+ sleep 3
33
+ systemctl is-active --quiet oto-mcp || (journalctl -u oto-mcp -n 50 --no-pager; exit 1)
34
+ journalctl -u oto-mcp -n 10 --no-pager
@@ -2,7 +2,7 @@
2
2
 
3
3
  CLI toolkit for AI agents — covers the long tail of SaaS APIs that don't have a CLI.
4
4
 
5
- Repo: `AlexisLaporte/oto`. Package: `oto-cli` on PyPI (v1.1.0). Command: `oto`.
5
+ Repo: `otomata-tech/oto-cli`. Package: `oto-cli` on PyPI (v1.1.0). Command: `oto`.
6
6
 
7
7
  ## Philosophy
8
8
 
@@ -30,7 +30,9 @@ oto/
30
30
  │ │ ├── notion.py # search, page, database
31
31
  │ │ ├── browser.py # linkedin, crunchbase, pappers, indeed, g2, google
32
32
  │ │ ├── reddit.py # Reddit JSON API (subreddit, search, post)
33
- │ │ ├── sirene.py # SIRENE API (search, get, stock)
33
+ │ │ ├── fr.py # données entreprise FR (fr_*) : recherche, bilans INPI, BODACC + sirene stock
34
+ │ │ ├── dvf.py # valeurs foncières (immobilier) : stats/comparables €/m² par commune ou adresse
35
+ │ │ ├── culture.py # Min. Culture open data (Opendatasoft) — sub-namespace `spectacle` (LES)
34
36
  │ │ ├── search.py # facade: dispatches to serper or browser via config
35
37
  │ │ ├── serper.py # direct Serper API (web, news, scrape, suggestions)
36
38
  │ │ ├── enrichment.py # kaspr, hunter, lemlist
@@ -42,9 +44,13 @@ oto/
42
44
  │ │ ├── zohodesk.py # Zoho Desk (tickets/support)
43
45
  │ │ ├── company.py # SIREN lookup multi-source
44
46
  │ │ ├── whatsapp.py # WhatsApp messaging
47
+ │ │ ├── slack.py # Slack — send/read/list-channels/dm (bot+user tokens)
45
48
  │ │ ├── audio.py # audio recording, transcription
46
- │ │ ├── gemini.py # Gemini image generation
49
+ │ │ ├── gemini.py # Gemini image generation (gemini-3-pro-image)
50
+ │ │ ├── openai.py # OpenAI image generation (gpt-image-2)
47
51
  │ │ ├── pdf.py # markdown → PDF via pandoc + weasyprint (bundled template)
52
+ │ │ ├── data.py # Datastore (per-user Google Sheets via mcp.oto.ninja, OTO_API_KEY)
53
+ │ │ ├── ninja.py # façade mcp.oto.ninja: secrets per-user (LinkedIn/Crunchbase/API keys), OTO_API_KEY
48
54
  │ │ ├── config.py # config & secrets management
49
55
  │ │ └── skills.py # Claude Code skills (enable/disable)
50
56
  │ └── tools/ # API clients
@@ -54,12 +60,16 @@ oto/
54
60
  │ ├── reddit/ # Reddit JSON API (no auth)
55
61
  │ ├── whatsapp/ # Node.js bridge (whatsapp-web.js)
56
62
  │ ├── sirene/ # INSEE SIRENE API
63
+ │ ├── culture/ # OpendatasoftClient générique + SpectacleClient (LES)
57
64
  │ ├── serper/ # Google search (web, news)
58
65
  │ ├── anthropic/ # Admin API (usage, costs)
59
66
  │ ├── pennylane/ # Accounting
60
67
  │ ├── attio/ # Attio CRM
68
+ │ ├── datastore/ # HTTP client → mcp.oto.ninja /api/datastore/*
69
+ │ ├── ninja/ # HTTP client → mcp.oto.ninja /api/settings/* (secrets per-user)
61
70
  │ ├── kaspr/, hunter/, lemlist/ # Enrichment & outreach
62
71
  │ ├── zohodesk/ # Zoho Desk (tickets/support)
72
+ │ ├── gemini/, openai/ # Image generation (Gemini 3 Pro, gpt-image-2)
63
73
  │ ├── pdf/ # pandoc+weasyprint wrapper, bundled CSS template (sober editorial)
64
74
  │ └── folk/, zoho/, slack/, resend/ # CRM & messaging
65
75
  ├── skills/ # Claude Code skills
@@ -107,14 +117,19 @@ Key rules:
107
117
 
108
118
  ## Secrets & Config
109
119
 
110
- Provider-based resolution, configured via `oto config provider secrets <file|scaleway>`:
111
- 1. Environment variables (always, highest priority)
112
- 2. Configured provider: **file** (`.otomata/secrets.env` project → user) or **Scaleway** Secret Manager
120
+ Provider-based resolution (`oto config provider secrets <sops|file|scaleway>`) :
121
+ 1. Env vars (always, highest priority)
122
+ 2. Configured provider:
123
+ - **sops** (default) — SOPS+age. `sops_dir` (multi-file, walks `*.yaml`
124
+ recursively, merges flat with warning on duplicate keys) or `sops_file`
125
+ (mono-file legacy). Default dir: `~/.otomata/secrets/`.
126
+ - **file** — `.otomata/secrets.env` project → user
127
+ - **scaleway** — Secret Manager
113
128
  3. Default value
114
129
 
115
130
  ```bash
116
131
  oto config # show providers + secrets status
117
- oto config provider secrets file # switch to file-based secrets
132
+ oto config provider secrets sops # switch to SOPS (default)
118
133
  oto config provider search serper # switch search to serper (default) or browser
119
134
  oto config secrets-push # upload local secrets.env → Scaleway
120
135
  oto config secrets-pull # download Scaleway → local secrets.env
@@ -149,13 +164,18 @@ oto skills enable oto-google # enable one
149
164
  oto skills disable oto-pennylane # disable one
150
165
  ```
151
166
 
152
- ## Release
167
+ ## Deploy
153
168
 
154
- Package: `oto-cli` on PyPI. PyPI token in `pass otomata/PYPI_TOKEN`.
169
+ Push main déclenche `.github/workflows/deploy.yml` qui SSH tuls.me, `git reset --hard origin/main` dans `/opt/oto-cli`, puis `systemctl restart oto-mcp` (oto-cli est installé editable dans le venv d'oto-mcp ; sans restart les modules déjà importés ne pickent pas les nouveaux). Pas de release PyPI requise pour propager un nouveau connecteur.
170
+
171
+ ## Release PyPI (rare)
172
+
173
+ Pour publier sur PyPI (autres utilisateurs hors infra Otomata). PyPI token in SOPS (`PYPI_TOKEN`).
155
174
 
156
175
  ```bash
157
176
  # Bump version in oto/__init__.py, then:
158
- hatch build && hatch publish -u __token__ -a "$(pass otomata/PYPI_TOKEN)"
177
+ hatch build && hatch publish -u __token__ \
178
+ -a "$(sops --decrypt --extract '["PYPI_TOKEN"]' ~/.otomata/secrets/secrets/secrets.yaml)"
159
179
  gh release create vX.Y.Z --generate-notes dist/*
160
180
  ```
161
181
 
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oto-cli
3
- Version: 1.3.0
3
+ Version: 1.4.0
4
4
  Summary: CLI toolkit for AI agents — covers the long tail of SaaS APIs that don't have a CLI
5
- Project-URL: Repository, https://github.com/AlexisLaporte/oto
5
+ Project-URL: Repository, https://github.com/otomata-tech/oto-cli
6
6
  Author: Alexis Laporte
7
7
  License-Expression: MIT
8
8
  License-File: LICENSE
@@ -71,8 +71,8 @@ pipx install "oto-cli[google,browser]"
71
71
  pipx install "oto-cli[all]"
72
72
 
73
73
  # Development
74
- git clone https://github.com/AlexisLaporte/oto.git
75
- cd oto && pip install -e ".[all]"
74
+ git clone https://github.com/otomata-tech/oto-cli.git
75
+ cd oto-cli && pip install -e ".[all]"
76
76
  ```
77
77
 
78
78
  ## Connectors
@@ -95,7 +95,8 @@ cd oto && pip install -e ".[all]"
95
95
  | `oto zohodesk` | Zoho Desk (tickets, threads, contacts, departments) | — |
96
96
  | `oto company` | French company lookup (multi-source) | — |
97
97
  | `oto audio` | Audio recording, transcription, summaries | — |
98
- | `oto gemini` | Image generation (text-to-image, editing) | — |
98
+ | `oto gemini` | Image generation via Gemini 3 Pro (text-to-image, editing) | — |
99
+ | `oto openai` | Image generation via gpt-image-2 (text-to-image, editing) | — |
99
100
  | `oto config` | Configuration & secrets management | — |
100
101
 
101
102
  Connectors without an "Extra" only need `requests` (included in base install).
@@ -127,8 +128,11 @@ oto google gmail-search "from:bob" -a myaccount
127
128
  # Browse LinkedIn
128
129
  oto browser linkedin profile https://linkedin.com/in/someone
129
130
 
130
- # French company data
131
+ # French company data — INSEE SIRENE
131
132
  oto sirene search "fintech"
133
+
134
+ # Enriched data via data.gouv recherche-entreprises (with --idcc filter by convention collective)
135
+ oto sirene entreprises --idcc 1285,3090 --naf 90.01Z --dept 75
132
136
  ```
133
137
 
134
138
  ## Skills for AI agents
@@ -28,8 +28,8 @@ pipx install "oto-cli[google,browser]"
28
28
  pipx install "oto-cli[all]"
29
29
 
30
30
  # Development
31
- git clone https://github.com/AlexisLaporte/oto.git
32
- cd oto && pip install -e ".[all]"
31
+ git clone https://github.com/otomata-tech/oto-cli.git
32
+ cd oto-cli && pip install -e ".[all]"
33
33
  ```
34
34
 
35
35
  ## Connectors
@@ -52,7 +52,8 @@ cd oto && pip install -e ".[all]"
52
52
  | `oto zohodesk` | Zoho Desk (tickets, threads, contacts, departments) | — |
53
53
  | `oto company` | French company lookup (multi-source) | — |
54
54
  | `oto audio` | Audio recording, transcription, summaries | — |
55
- | `oto gemini` | Image generation (text-to-image, editing) | — |
55
+ | `oto gemini` | Image generation via Gemini 3 Pro (text-to-image, editing) | — |
56
+ | `oto openai` | Image generation via gpt-image-2 (text-to-image, editing) | — |
56
57
  | `oto config` | Configuration & secrets management | — |
57
58
 
58
59
  Connectors without an "Extra" only need `requests` (included in base install).
@@ -84,8 +85,11 @@ oto google gmail-search "from:bob" -a myaccount
84
85
  # Browse LinkedIn
85
86
  oto browser linkedin profile https://linkedin.com/in/someone
86
87
 
87
- # French company data
88
+ # French company data — INSEE SIRENE
88
89
  oto sirene search "fintech"
90
+
91
+ # Enriched data via data.gouv recherche-entreprises (with --idcc filter by convention collective)
92
+ oto sirene entreprises --idcc 1285,3090 --naf 90.01Z --dept 75
89
93
  ```
90
94
 
91
95
  ## Skills for AI agents
@@ -0,0 +1,135 @@
1
+ # Installation
2
+
3
+ `oto` is a CLI toolkit. Install it once with `pipx`, then every connector is a
4
+ sub-command (`oto google`, `oto linkedin`, …). Each connector also ships a
5
+ `SKILL.md` so your AI agent (Claude Code, Cursor, …) knows how to drive it.
6
+
7
+ ## Prerequisites
8
+
9
+ - **Python 3.10+**
10
+ - **pipx** (recommended) — isolates the CLI in its own venv
11
+ - **Google Chrome** — required only for the `browser` connectors (LinkedIn,
12
+ Crunchbase, Indeed, …). `oto` drives your installed Chrome; if Chrome is
13
+ absent see [Troubleshooting](#browser-no-chromechromium-found).
14
+
15
+ ## Install pipx
16
+
17
+ ```bash
18
+ # Debian/Ubuntu
19
+ sudo apt install pipx && pipx ensurepath
20
+ # Fedora
21
+ sudo dnf install pipx && pipx ensurepath
22
+ # macOS
23
+ brew install pipx && pipx ensurepath
24
+ # Windows (PowerShell)
25
+ pip install --user pipx && python -m pipx ensurepath
26
+ ```
27
+
28
+ Restart your terminal after `ensurepath`.
29
+
30
+ ## Install oto
31
+
32
+ ```bash
33
+ # Base CLI (API connectors that only need HTTP)
34
+ pipx install oto-cli
35
+
36
+ # With the browser connectors (LinkedIn, Crunchbase, …)
37
+ pipx install "oto-cli[browser]"
38
+
39
+ # With Google (Gmail, Drive, Sheets, …)
40
+ pipx install "oto-cli[google,browser]"
41
+
42
+ # Everything
43
+ pipx install "oto-cli[all]"
44
+ ```
45
+
46
+ Verify:
47
+
48
+ ```bash
49
+ oto --help
50
+ ```
51
+
52
+ ## LinkedIn setup
53
+
54
+ LinkedIn no longer accepts an injected `li_at` cookie (its TLS fingerprinting
55
+ rejects a session that wasn't created by the same browser). The reliable method
56
+ is a **persistent browser profile** you log into once.
57
+
58
+ ```bash
59
+ # One-time: opens a real Chrome window — log in by hand, then CLOSE the window.
60
+ oto linkedin login --profile ~/.config/browser/linkedin
61
+ ```
62
+
63
+ > `login` opens a visible (headed) browser, so it needs a graphical session
64
+ > (a desktop, or VNC on a headless server). Log in fully (including 2FA), confirm
65
+ > you land on your feed, then close the window — the session is saved in the
66
+ > profile directory.
67
+
68
+ Afterwards, pass that profile to every LinkedIn command:
69
+
70
+ ```bash
71
+ oto linkedin search-people "head of finance" --profile ~/.config/browser/linkedin
72
+ oto linkedin connect "https://www.linkedin.com/in/john-doe/" --note "Bonjour …" \
73
+ --profile ~/.config/browser/linkedin
74
+ ```
75
+
76
+ No API key or secret is required for LinkedIn when you use a logged-in profile.
77
+ See the `oto-browser` skill for the full command set (`oto skills show oto-browser`).
78
+
79
+ ## Configuration & secrets
80
+
81
+ Connectors that hit third-party APIs (Serper, Hunter, Attio, Pennylane, …) need
82
+ credentials. `oto` resolves a secret in this order: **environment variable →
83
+ configured provider → default**. The simplest provider is a flat file:
84
+
85
+ ```bash
86
+ mkdir -p ~/.otomata
87
+ printf 'SERPER_API_KEY=xxx\nHUNTER_API_KEY=yyy\n' >> ~/.otomata/secrets.env
88
+ oto config provider secrets file # use the file provider
89
+ oto config # show providers + which secrets are set
90
+ ```
91
+
92
+ LinkedIn-via-profile needs none of this.
93
+
94
+ ## Skills (for AI agents)
95
+
96
+ Each connector's `SKILL.md` is an instruction manual for your agent. Symlink them
97
+ into Claude Code:
98
+
99
+ ```bash
100
+ oto skills enable --all # or: oto skills enable oto-browser
101
+ oto skills list
102
+ ```
103
+
104
+ ## Update / uninstall
105
+
106
+ ```bash
107
+ pipx upgrade oto-cli
108
+ pipx uninstall oto-cli
109
+ ```
110
+
111
+ ## Troubleshooting
112
+
113
+ ### `oto: command not found`
114
+ Run `pipx ensurepath` and restart the terminal.
115
+
116
+ ### Browser: no Chrome/Chromium found
117
+ `oto` prefers your installed Google Chrome. If you don't have Chrome, install a
118
+ Chromium for Patchright inside the CLI's venv:
119
+
120
+ ```bash
121
+ ~/.local/share/pipx/venvs/oto-cli/bin/patchright install chromium
122
+ ```
123
+
124
+ (On macOS/Windows the path under `pipx environment --value PIPX_LOCAL_VENVS` is
125
+ the equivalent.)
126
+
127
+ ### LinkedIn: "session expired — cookie li_at is no longer valid"
128
+ The profile's session lapsed. Re-run the login:
129
+
130
+ ```bash
131
+ oto linkedin login --profile ~/.config/browser/linkedin
132
+ ```
133
+
134
+ ### Python version error
135
+ Ensure Python 3.10+: `python3 --version`.
@@ -1,3 +1,3 @@
1
1
  """Oto - CLI tools for automation."""
2
2
 
3
- __version__ = "1.3.0"
3
+ __version__ = "1.4.0"
@@ -134,6 +134,49 @@ def add_person(
134
134
  _out(result.get("data", {}))
135
135
 
136
136
 
137
+ @app.command("update-person")
138
+ def update_person(
139
+ record_id: str = typer.Argument(..., help="Record ID"),
140
+ first_name: Optional[str] = typer.Option(None, "--first", help="First name (requires --last)"),
141
+ last_name: Optional[str] = typer.Option(None, "--last", "-l", help="Last name (requires --first)"),
142
+ email: Optional[str] = typer.Option(None, "--email", "-e"),
143
+ phone: Optional[str] = typer.Option(None, "--phone"),
144
+ job_title: Optional[str] = typer.Option(None, "--title", "-t"),
145
+ company: Optional[str] = typer.Option(None, "--company", "-c", help="Company name (matches existing)"),
146
+ linkedin: Optional[str] = typer.Option(None, "--linkedin", help="LinkedIn URL"),
147
+ ):
148
+ """Update a contact. Multi-value fields (email/phone) are replaced."""
149
+ values = {}
150
+ if first_name or last_name:
151
+ if not (first_name and last_name):
152
+ print("To change the name, pass both --first and --last")
153
+ raise typer.Exit(1)
154
+ full = f"{first_name} {last_name}".strip()
155
+ values["name"] = [{"first_name": first_name, "last_name": last_name, "full_name": full}]
156
+ if email:
157
+ values["email_addresses"] = [{"email_address": email}]
158
+ if phone:
159
+ values["phone_numbers"] = [{"phone_number": phone}]
160
+ if job_title:
161
+ values["job_title"] = [{"value": job_title}]
162
+ if linkedin:
163
+ values["linkedin"] = [{"value": linkedin}]
164
+ if company:
165
+ co = _request("POST", "objects/companies/records/query", json={
166
+ "filter": {"name": {"$eq": company}}, "limit": 1,
167
+ }).get("data", [])
168
+ if co:
169
+ values["company"] = [{"target_object": "companies", "target_record_id": co[0]["id"]["record_id"]}]
170
+ else:
171
+ print(f"WARN: company '{company}' not found")
172
+
173
+ if not values:
174
+ print("Nothing to update")
175
+ return
176
+ result = _request("PATCH", f"objects/people/records/{record_id}", json={"data": {"values": values}})
177
+ _out(result.get("data", {}))
178
+
179
+
137
180
  @app.command("delete-person")
138
181
  def delete_person(record_id: str = typer.Argument(..., help="Record ID")):
139
182
  """Delete a contact."""
@@ -183,6 +226,29 @@ def add_company(
183
226
  _out(result.get("data", {}))
184
227
 
185
228
 
229
+ @app.command("update-company")
230
+ def update_company(
231
+ record_id: str = typer.Argument(..., help="Record ID"),
232
+ name: Optional[str] = typer.Option(None, "--name", "-n"),
233
+ domain: Optional[str] = typer.Option(None, "--domain", "-d"),
234
+ description: Optional[str] = typer.Option(None, "--desc"),
235
+ ):
236
+ """Update a company. Domains is replaced."""
237
+ values = {}
238
+ if name:
239
+ values["name"] = [{"value": name}]
240
+ if domain:
241
+ values["domains"] = [{"domain": domain}]
242
+ if description:
243
+ values["description"] = [{"value": description}]
244
+
245
+ if not values:
246
+ print("Nothing to update")
247
+ return
248
+ result = _request("PATCH", f"objects/companies/records/{record_id}", json={"data": {"values": values}})
249
+ _out(result.get("data", {}))
250
+
251
+
186
252
  @app.command("delete-company")
187
253
  def delete_company(record_id: str = typer.Argument(..., help="Record ID")):
188
254
  """Delete a company."""
@@ -208,6 +208,83 @@ def linkedin_messages(
208
208
  print(json.dumps(result, indent=2, ensure_ascii=False))
209
209
 
210
210
 
211
+ @linkedin_app.command("login")
212
+ def linkedin_login(
213
+ profile: str = typer.Option(..., help="Chrome profile directory to provision/refresh (e.g. ~/.config/browser/linkedin)"),
214
+ channel: Optional[str] = typer.Option(None, envvar="BROWSER_CHANNEL", help="Chrome channel"),
215
+ ):
216
+ """Open a headed browser to log into LinkedIn; the session persists in <profile>.
217
+
218
+ Run once per profile. Cookie injection is blocked by LinkedIn's TLS
219
+ fingerprinting — a session created inside this same browser is the only
220
+ reliable way to authenticate scraping/outreach afterwards.
221
+ """
222
+ import asyncio
223
+ import json
224
+ import sys
225
+ from o_browser import BrowserClient
226
+
227
+ async def run():
228
+ async with BrowserClient(profile_path=profile, interactive=True, channel=channel) as browser:
229
+ await browser.goto("https://www.linkedin.com/login")
230
+ print(
231
+ "→ Log into LinkedIn in the opened window, then CLOSE it to save the session.",
232
+ file=sys.stderr,
233
+ )
234
+ await browser.wait_closed()
235
+ return {"status": "session_saved", "profile": profile}
236
+
237
+ print(json.dumps(asyncio.run(run()), indent=2, ensure_ascii=False))
238
+
239
+
240
+ @linkedin_app.command("send")
241
+ def linkedin_send(
242
+ url: str = typer.Argument(..., help="Recipient profile URL (must be a 1st-degree connection)"),
243
+ message: str = typer.Argument(..., help="Message body"),
244
+ dry_run: bool = typer.Option(False, "--dry-run", help="Type the message but do NOT click send (saves a screenshot)"),
245
+ cookie: Optional[str] = typer.Option(None, envvar="LINKEDIN_COOKIE", help="li_at cookie"),
246
+ cdp_url: Optional[str] = typer.Option(None, "--cdp-url", help="Connect to existing Chrome via CDP"),
247
+ identity: str = typer.Option("default", help="Identity for rate limiting"),
248
+ profile: Optional[str] = typer.Option(None, help="Chrome profile directory path"),
249
+ channel: Optional[str] = typer.Option(None, envvar="BROWSER_CHANNEL", help="Chrome channel"),
250
+ no_rate_limit: bool = typer.Option(False, "--no-rate-limit", help="Disable rate limiting"),
251
+ headless: bool = typer.Option(True, help="Run headless"),
252
+ ):
253
+ """Send a direct message to a 1st-degree connection."""
254
+ import asyncio
255
+ import json
256
+
257
+ async def run():
258
+ async with _linkedin_client(cookie=cookie, cdp_url=cdp_url, identity=identity, profile=profile, channel=channel, headless=headless, rate_limit=not no_rate_limit) as client:
259
+ return await client.send_message(url, message, dry_run=dry_run)
260
+
261
+ print(json.dumps(asyncio.run(run()), indent=2, ensure_ascii=False))
262
+
263
+
264
+ @linkedin_app.command("connect")
265
+ def linkedin_connect(
266
+ url: str = typer.Argument(..., help="Profile URL to send a connection request to"),
267
+ note: Optional[str] = typer.Option(None, "--note", help="Optional note (<=300 chars)"),
268
+ dry_run: bool = typer.Option(False, "--dry-run", help="Open the invite dialog but do NOT click send (saves a screenshot)"),
269
+ cookie: Optional[str] = typer.Option(None, envvar="LINKEDIN_COOKIE", help="li_at cookie"),
270
+ cdp_url: Optional[str] = typer.Option(None, "--cdp-url", help="Connect to existing Chrome via CDP"),
271
+ identity: str = typer.Option("default", help="Identity for rate limiting"),
272
+ profile: Optional[str] = typer.Option(None, help="Chrome profile directory path"),
273
+ channel: Optional[str] = typer.Option(None, envvar="BROWSER_CHANNEL", help="Chrome channel"),
274
+ no_rate_limit: bool = typer.Option(False, "--no-rate-limit", help="Disable rate limiting"),
275
+ headless: bool = typer.Option(True, help="Run headless"),
276
+ ):
277
+ """Send a connection invitation (cold-outreach primitive), optionally with a note."""
278
+ import asyncio
279
+ import json
280
+
281
+ async def run():
282
+ async with _linkedin_client(cookie=cookie, cdp_url=cdp_url, identity=identity, profile=profile, channel=channel, headless=headless, rate_limit=not no_rate_limit) as client:
283
+ return await client.send_invitation(url, note=note, dry_run=dry_run)
284
+
285
+ print(json.dumps(asyncio.run(run()), indent=2, ensure_ascii=False))
286
+
287
+
211
288
  @app.command("google")
212
289
  def google_search(
213
290
  query: str = typer.Option(..., "--query", "-q", help="Search query"),
@@ -21,6 +21,7 @@ TRACKED_SECRETS = [
21
21
  "FOLK_API_KEY",
22
22
  "ATTIO_API_KEY",
23
23
  "TULS_API_TOKEN",
24
+ "OTO_API_KEY",
24
25
  ]
25
26
 
26
27
 
@@ -0,0 +1,107 @@
1
+ """Culture (Ministère de la Culture open data) commands.
2
+
3
+ Sub-namespaces by dataset:
4
+ - spectacle: Licences entrepreneurs de spectacles vivants (LES)
5
+
6
+ Future: festivals, cnc, adsv, …
7
+ """
8
+
9
+ import json
10
+ import typer
11
+ from typing import Optional
12
+
13
+ app = typer.Typer(help="French Ministry of Culture open data (data.culture.gouv.fr)")
14
+
15
+ # spectacle sub-namespace
16
+ spectacle_app = typer.Typer(help="Licences entrepreneurs de spectacles vivants (LES)")
17
+ app.add_typer(spectacle_app, name="spectacle")
18
+
19
+
20
+ def _print(obj):
21
+ print(json.dumps(obj, indent=2, ensure_ascii=False, default=str))
22
+
23
+
24
+ @spectacle_app.command("search")
25
+ def spectacle_search(
26
+ status: str = typer.Option("Valide", "--status", help="Valide|Invalide|Expiré|Invalidé|En instruction (case-sensitive)"),
27
+ categorie: Optional[str] = typer.Option(None, "--categorie", "-c", help="1 (lieu), 2 (producteur), 3 (diffuseur)"),
28
+ naf: Optional[str] = typer.Option(None, "--naf", help="NAF prefix (e.g. 90.01Z or 9001Z) — handles unnormalized field"),
29
+ region: Optional[str] = typer.Option(None, "--region", help="Région SIRET (e.g. 'Île-de-France')"),
30
+ dept: Optional[str] = typer.Option(None, "--dept", help="Département SIRET (e.g. 'Paris', 'Bouches-du-Rhône')"),
31
+ cp: Optional[str] = typer.Option(None, "--cp", help="Code postal SIRET"),
32
+ siren: Optional[str] = typer.Option(None, "--siren", help="SIREN (9) or SIRET (14) prefix — matches all records starting with it"),
33
+ declarant: Optional[str] = typer.Option(None, "--declarant", help="Substring match on type_declarant (e.g. 'privé', 'association', 'public')"),
34
+ since: Optional[str] = typer.Option(None, "--since", help="Filed since YYYY-MM-DD"),
35
+ raw_where: Optional[str] = typer.Option(None, "--where", help="Raw Opendatasoft where clause (advanced)"),
36
+ order: str = typer.Option("date_depot_dossier desc", "--order", help="order_by clause"),
37
+ limit: int = typer.Option(20, "--limit", "-n", help="Max results (1-100 per page)"),
38
+ offset: int = typer.Option(0, "--offset", help="Pagination offset"),
39
+ ):
40
+ """Search LES with composed AND filters (statut + catégorie + NAF + région…)."""
41
+ from oto.tools.culture import SpectacleClient
42
+ client = SpectacleClient()
43
+ try:
44
+ result = client.search(
45
+ status=status, categorie=categorie, naf=naf, region=region,
46
+ departement=dept, code_postal=cp, siren=siren,
47
+ type_declarant_like=declarant, deposited_since=since,
48
+ raw_where=raw_where, order_by=order, limit=limit, offset=offset,
49
+ )
50
+ except ValueError as e:
51
+ raise typer.BadParameter(str(e))
52
+ _print(result)
53
+
54
+
55
+ @spectacle_app.command("get")
56
+ def spectacle_get(
57
+ siren: str = typer.Argument(..., help="SIREN (9) or SIRET (14) — returns all récépissés for that entity"),
58
+ ):
59
+ """Fetch all récépissés (L1/L2/L3) for a SIREN/SIRET."""
60
+ from oto.tools.culture import SpectacleClient
61
+ client = SpectacleClient()
62
+ _print(client.get(siren))
63
+
64
+
65
+ @spectacle_app.command("stats")
66
+ def spectacle_stats(
67
+ by: str = typer.Argument(..., help="Field to group on: code_naf_ape|region_siret|departement_siret|categorie|type_declarant"),
68
+ status: str = typer.Option("Valide", "--status"),
69
+ categorie: Optional[str] = typer.Option(None, "--categorie", "-c"),
70
+ naf: Optional[str] = typer.Option(None, "--naf"),
71
+ region: Optional[str] = typer.Option(None, "--region"),
72
+ dept: Optional[str] = typer.Option(None, "--dept"),
73
+ limit: int = typer.Option(20, "--limit", "-n"),
74
+ ):
75
+ """Group-by aggregate (fills the gap of the official datagouv MCP)."""
76
+ from oto.tools.culture import SpectacleClient
77
+ client = SpectacleClient()
78
+ try:
79
+ result = client.stats(
80
+ by,
81
+ where_filters={
82
+ "status": status, "categorie": categorie, "naf": naf,
83
+ "region": region, "departement": dept,
84
+ },
85
+ limit=limit,
86
+ )
87
+ except ValueError as e:
88
+ raise typer.BadParameter(str(e))
89
+ _print(result)
90
+
91
+
92
+ @spectacle_app.command("export-url")
93
+ def spectacle_export_url(
94
+ fmt: str = typer.Option("csv", "--format", "-f", help="csv|json|parquet|xlsx"),
95
+ status: Optional[str] = typer.Option("Valide", "--status"),
96
+ categorie: Optional[str] = typer.Option(None, "--categorie", "-c"),
97
+ naf: Optional[str] = typer.Option(None, "--naf"),
98
+ region: Optional[str] = typer.Option(None, "--region"),
99
+ dept: Optional[str] = typer.Option(None, "--dept"),
100
+ ):
101
+ """Build a direct export URL — caller streams it (~6 MB CSV for full valid set)."""
102
+ from oto.tools.culture import SpectacleClient
103
+ client = SpectacleClient()
104
+ print(client.export_url(
105
+ fmt=fmt, status=status, categorie=categorie, naf=naf,
106
+ region=region, departement=dept,
107
+ ))