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.
- {oto_cli-1.2.0 → oto_cli-1.3.0}/CLAUDE.md +2 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/PKG-INFO +1 -1
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/__init__.py +1 -1
- oto_cli-1.3.0/oto/commands/reddit.py +62 -0
- oto_cli-1.3.0/oto/tools/reddit/__init__.py +5 -0
- oto_cli-1.3.0/oto/tools/reddit/client.py +212 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/.claude/settings.local.json +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/.gitignore +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/LICENSE +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/README.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/TODO.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/docs/concepts.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/docs/create-connector.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/docs/gmail-oauth-setup.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/docs/gmail.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/docs/google-docs.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/docs/google-service-account-setup.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/docs/installation.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/docs/zoho-desk-oauth-setup.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/logo.svg +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/cli.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/anthropic.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/attio.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/audio.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/browser.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/company.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/config.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/enrichment.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/folk.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/gemini.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/google.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/notion.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/pdf.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/pennylane.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/search.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/serper.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/sirene.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/skills.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/whatsapp.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/zoho.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/commands/zohodesk.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/config.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/scaleway_secrets.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/anthropic/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/anthropic/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/anthropic_batch/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/anthropic_batch/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/apollo/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/apollo/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/attio/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/attio/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/audio/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/audio/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/browser/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/browser/crunchbase.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/browser/g2.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/browser/google.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/browser/indeed.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/browser/linkedin/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/browser/linkedin/_js.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/browser/linkedin/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/browser/linkedin/scrape.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/browser/linkedin/search.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/browser/pappers.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/browser/sncf.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/clearbit/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/clearbit/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/collective/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/collective/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/common/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/common/rate_limiter.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/figma/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/figma/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/folk/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/folk/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/gemini/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/gemini/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/calendar/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/calendar/lib/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/calendar/lib/calendar_client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/credentials.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/docs/get_section.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/docs/insert_section.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/docs/insert_text.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/docs/lib/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/docs/lib/docs_client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/docs/lib/markdown_to_docs.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/docs/lib/markdown_to_html.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/docs/list_headings.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/docs/move_section.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/docs/replace_section.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/docs/sync/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/docs/sync/gdoc_sync.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/drive/README.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/drive/check_quota.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/drive/copy_file.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/drive/create_folder.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/drive/download_file.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/drive/export_doc.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/drive/export_slides.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/drive/extract_slides_content.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/drive/lib/drive_client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/drive/list_files.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/drive/list_shared_drives.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/drive/move_file.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/drive/requirements.txt +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/drive/tool.yaml +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/drive/upload_file.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/gmail/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/gmail/get_message.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/gmail/lib/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/gmail/lib/gmail_client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/gmail/list_messages.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/gmail/requirements.txt +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/gmail/search.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/gmail/send.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/gmail/tool.yaml +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/keep/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/keep/create_note.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/keep/get_master_token.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/keep/get_note.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/keep/lib/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/keep/lib/keep_client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/keep/list_notes.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/keep/search.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/sheets/create_sheet_direct.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/sheets/create_sheet_from_csv.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/sheets/create_sheet_in_folder.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/sheets/create_sheet_via_drive.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/sheets/lib/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/sheets/lib/sheets_client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/sheets/tool.yaml +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/slides/.folders +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/slides/321-LAYOUTS.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/slides/README.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/slides/USAGE-EDIT.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/slides/USAGE.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/slides/create-demo-presentation.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/slides/generate_slides.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/slides/lib/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/slides/lib/content_filler.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/slides/lib/layout_mappings.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/slides/lib/slides_client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/slides/requirements.txt +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/slides/test-copy-and-edit.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/slides/test-copy-slide.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/slides/test-edit.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/google/slides/tool.yaml +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/groq/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/groq/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/hithorizons/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/hithorizons/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/hunter/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/hunter/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/kaspr/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/kaspr/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/lemlist/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/lemlist/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/mistral/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/mistral/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/naf/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/naf/suggester.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/notion/README.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/notion/append_blocks.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/notion/append_blocks_chunked.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/notion/count_database_entries.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/notion/create_database_from_csv.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/notion/create_page.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/notion/get_database.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/notion/get_page.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/notion/known-pages.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/notion/lib/markdown_converter.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/notion/lib/notion_client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/notion/list_teamspaces.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/notion/notion +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/notion/query_database.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/notion/requirements.txt +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/notion/search.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/notion/tool.yaml +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/notion/update_page.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/pdf/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/pdf/templates/default.css +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/pennylane/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/pennylane/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/phantombuster/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/phantombuster/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/resend/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/resend/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/serpapi/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/serpapi/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/serper/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/serper/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/sirene/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/sirene/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/sirene/data/naf_codes.txt +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/sirene/entreprises.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/sirene/stock.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/slack/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/slack/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/unsplash/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/unsplash/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/whatsapp/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/whatsapp/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/whatsapp/node/package-lock.json +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/whatsapp/node/package.json +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/whatsapp/node/whatsapp.mjs +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/wttj/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/wttj/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/zerobounce/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/zerobounce/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/zoho/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/zoho/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/zohodesk/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/oto/tools/zohodesk/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/pyproject.toml +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/skills/oto-anthropic/SKILL.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/skills/oto-browser/SKILL.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/skills/oto-cli/SKILL.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/skills/oto-enrichment/SKILL.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/skills/oto-google/SKILL.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/skills/oto-notion/SKILL.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/skills/oto-pennylane/SKILL.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/skills/oto-search/SKILL.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/skills/oto-sirene/SKILL.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/skills/oto-task/SKILL.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/skills/oto-whatsapp/SKILL.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.3.0}/skills/oto-zoho/SKILL.md +0 -0
- {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)
|
|
@@ -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,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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|