oto-cli 1.2.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.
- oto_cli-1.4.0/.github/workflows/deploy.yml +34 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/CLAUDE.md +32 -10
- {oto_cli-1.2.0 → oto_cli-1.4.0}/PKG-INFO +10 -6
- {oto_cli-1.2.0 → oto_cli-1.4.0}/README.md +8 -4
- oto_cli-1.4.0/docs/installation.md +135 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/__init__.py +1 -1
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/commands/attio.py +66 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/commands/browser.py +77 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/commands/config.py +1 -0
- oto_cli-1.4.0/oto/commands/culture.py +107 -0
- oto_cli-1.4.0/oto/commands/data.py +148 -0
- oto_cli-1.4.0/oto/commands/dvf.py +60 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/commands/enrichment.py +4 -2
- oto_cli-1.4.0/oto/commands/fr.py +263 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/commands/gemini.py +6 -6
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/commands/google.py +47 -2
- oto_cli-1.4.0/oto/commands/ninja.py +208 -0
- oto_cli-1.4.0/oto/commands/openai.py +157 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/commands/pennylane.py +25 -3
- oto_cli-1.4.0/oto/commands/reddit.py +62 -0
- oto_cli-1.4.0/oto/commands/slack.py +197 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/config.py +20 -6
- oto_cli-1.4.0/oto/sops_secrets.py +125 -0
- oto_cli-1.4.0/oto/tools/attio/client.py +601 -0
- oto_cli-1.4.0/oto/tools/boamp/__init__.py +3 -0
- oto_cli-1.4.0/oto/tools/boamp/client.py +69 -0
- oto_cli-1.4.0/oto/tools/bodacc/__init__.py +3 -0
- oto_cli-1.4.0/oto/tools/bodacc/client.py +101 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/browser/linkedin/_js.py +31 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/browser/linkedin/client.py +28 -1
- oto_cli-1.4.0/oto/tools/browser/linkedin/outreach.py +165 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/browser/linkedin/scrape.py +25 -7
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/browser/linkedin/search.py +5 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/common/rate_limiter.py +25 -7
- oto_cli-1.4.0/oto/tools/culture/__init__.py +10 -0
- oto_cli-1.4.0/oto/tools/culture/opendatasoft.py +117 -0
- oto_cli-1.4.0/oto/tools/culture/spectacle.py +174 -0
- oto_cli-1.4.0/oto/tools/datastore/__init__.py +4 -0
- oto_cli-1.4.0/oto/tools/datastore/client.py +99 -0
- oto_cli-1.4.0/oto/tools/dvf/__init__.py +3 -0
- oto_cli-1.4.0/oto/tools/dvf/client.py +300 -0
- oto_cli-1.4.0/oto/tools/fullenrich/client.py +184 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/gemini/client.py +4 -4
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/docs/lib/markdown_to_html.py +12 -1
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/drive/lib/drive_client.py +59 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/gmail/lib/gmail_client.py +65 -7
- oto_cli-1.4.0/oto/tools/google/sheets/lib/__init__.py +0 -0
- oto_cli-1.4.0/oto/tools/inpi/__init__.py +3 -0
- oto_cli-1.4.0/oto/tools/inpi/client.py +44 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/kaspr/client.py +25 -8
- oto_cli-1.4.0/oto/tools/ninja/__init__.py +4 -0
- oto_cli-1.4.0/oto/tools/ninja/client.py +99 -0
- oto_cli-1.4.0/oto/tools/openai/__init__.py +5 -0
- oto_cli-1.4.0/oto/tools/openai/client.py +167 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/pennylane/client.py +34 -0
- oto_cli-1.4.0/oto/tools/reddit/__init__.py +5 -0
- oto_cli-1.4.0/oto/tools/reddit/client.py +212 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/serper/client.py +15 -3
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/sirene/entreprises.py +6 -1
- oto_cli-1.4.0/oto/tools/sirene/stock.py +184 -0
- oto_cli-1.4.0/oto/tools/slack/client.py +383 -0
- oto_cli-1.4.0/oto/tools/supabase/__init__.py +4 -0
- oto_cli-1.4.0/oto/tools/supabase/client.py +98 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/pyproject.toml +1 -1
- oto_cli-1.4.0/scripts/migrate_timetrack.py +103 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/skills/oto-browser/SKILL.md +30 -1
- oto_cli-1.2.0/docs/installation.md +0 -134
- oto_cli-1.2.0/oto/commands/company.py +0 -23
- oto_cli-1.2.0/oto/commands/sirene.py +0 -201
- oto_cli-1.2.0/oto/tools/attio/client.py +0 -259
- oto_cli-1.2.0/oto/tools/sirene/stock.py +0 -377
- oto_cli-1.2.0/oto/tools/slack/client.py +0 -202
- {oto_cli-1.2.0 → oto_cli-1.4.0}/.claude/settings.local.json +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/.gitignore +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/LICENSE +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/TODO.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/docs/concepts.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/docs/create-connector.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/docs/gmail-oauth-setup.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/docs/gmail.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/docs/google-docs.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/docs/google-service-account-setup.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/docs/zoho-desk-oauth-setup.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/logo.svg +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/cli.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/commands/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/commands/anthropic.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/commands/audio.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/commands/folk.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/commands/notion.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/commands/pdf.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/commands/search.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/commands/serper.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/commands/skills.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/commands/whatsapp.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/commands/zoho.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/commands/zohodesk.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/scaleway_secrets.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/anthropic/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/anthropic/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/anthropic_batch/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/anthropic_batch/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/apollo/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/apollo/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/attio/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/audio/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/audio/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/browser/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/browser/crunchbase.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/browser/g2.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/browser/google.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/browser/indeed.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/browser/linkedin/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/browser/pappers.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/browser/sncf.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/clearbit/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/clearbit/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/collective/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/collective/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/common/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/figma/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/figma/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/folk/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/folk/client.py +0 -0
- {oto_cli-1.2.0/oto/tools/google/calendar → oto_cli-1.4.0/oto/tools/fullenrich}/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/gemini/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/__init__.py +0 -0
- {oto_cli-1.2.0/oto/tools/google/calendar/lib → oto_cli-1.4.0/oto/tools/google/calendar}/__init__.py +0 -0
- {oto_cli-1.2.0/oto/tools/google/docs → oto_cli-1.4.0/oto/tools/google/calendar}/lib/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/calendar/lib/calendar_client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/credentials.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/docs/get_section.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/docs/insert_section.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/docs/insert_text.py +0 -0
- {oto_cli-1.2.0/oto/tools/google/keep → oto_cli-1.4.0/oto/tools/google/docs/lib}/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/docs/lib/docs_client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/docs/lib/markdown_to_docs.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/docs/list_headings.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/docs/move_section.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/docs/replace_section.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/docs/sync/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/docs/sync/gdoc_sync.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/drive/README.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/drive/check_quota.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/drive/copy_file.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/drive/create_folder.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/drive/download_file.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/drive/export_doc.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/drive/export_slides.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/drive/extract_slides_content.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/drive/list_files.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/drive/list_shared_drives.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/drive/move_file.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/drive/requirements.txt +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/drive/tool.yaml +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/drive/upload_file.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/gmail/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/gmail/get_message.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/gmail/lib/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/gmail/list_messages.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/gmail/requirements.txt +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/gmail/search.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/gmail/send.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/gmail/tool.yaml +0 -0
- {oto_cli-1.2.0/oto/tools/google/keep/lib → oto_cli-1.4.0/oto/tools/google/keep}/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/keep/create_note.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/keep/get_master_token.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/keep/get_note.py +0 -0
- {oto_cli-1.2.0/oto/tools/google/sheets → oto_cli-1.4.0/oto/tools/google/keep}/lib/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/keep/lib/keep_client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/keep/list_notes.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/keep/search.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/sheets/create_sheet_direct.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/sheets/create_sheet_from_csv.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/sheets/create_sheet_in_folder.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/sheets/create_sheet_via_drive.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/sheets/lib/sheets_client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/sheets/tool.yaml +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/slides/.folders +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/slides/321-LAYOUTS.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/slides/README.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/slides/USAGE-EDIT.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/slides/USAGE.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/slides/create-demo-presentation.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/slides/generate_slides.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/slides/lib/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/slides/lib/content_filler.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/slides/lib/layout_mappings.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/slides/lib/slides_client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/slides/requirements.txt +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/slides/test-copy-and-edit.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/slides/test-copy-slide.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/slides/test-edit.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/google/slides/tool.yaml +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/groq/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/groq/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/hithorizons/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/hithorizons/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/hunter/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/hunter/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/kaspr/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/lemlist/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/lemlist/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/mistral/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/mistral/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/naf/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/naf/suggester.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/notion/README.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/notion/append_blocks.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/notion/append_blocks_chunked.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/notion/count_database_entries.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/notion/create_database_from_csv.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/notion/create_page.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/notion/get_database.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/notion/get_page.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/notion/known-pages.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/notion/lib/markdown_converter.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/notion/lib/notion_client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/notion/list_teamspaces.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/notion/notion +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/notion/query_database.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/notion/requirements.txt +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/notion/search.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/notion/tool.yaml +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/notion/update_page.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/pdf/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/pdf/templates/default.css +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/pennylane/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/phantombuster/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/phantombuster/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/resend/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/resend/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/serpapi/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/serpapi/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/serper/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/sirene/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/sirene/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/sirene/data/naf_codes.txt +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/slack/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/unsplash/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/unsplash/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/whatsapp/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/whatsapp/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/whatsapp/node/package-lock.json +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/whatsapp/node/package.json +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/whatsapp/node/whatsapp.mjs +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/wttj/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/wttj/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/zerobounce/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/zerobounce/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/zoho/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/zoho/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/zohodesk/__init__.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/oto/tools/zohodesk/client.py +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/skills/oto-anthropic/SKILL.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/skills/oto-cli/SKILL.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/skills/oto-enrichment/SKILL.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/skills/oto-google/SKILL.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/skills/oto-notion/SKILL.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/skills/oto-pennylane/SKILL.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/skills/oto-search/SKILL.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/skills/oto-sirene/SKILL.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/skills/oto-task/SKILL.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/skills/oto-whatsapp/SKILL.md +0 -0
- {oto_cli-1.2.0 → oto_cli-1.4.0}/skills/oto-zoho/SKILL.md +0 -0
- {oto_cli-1.2.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: `
|
|
5
|
+
Repo: `otomata-tech/oto-cli`. Package: `oto-cli` on PyPI (v1.1.0). Command: `oto`.
|
|
6
6
|
|
|
7
7
|
## Philosophy
|
|
8
8
|
|
|
@@ -29,7 +29,10 @@ 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
|
-
│ │ ├──
|
|
32
|
+
│ │ ├── reddit.py # Reddit JSON API (subreddit, search, post)
|
|
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)
|
|
33
36
|
│ │ ├── search.py # facade: dispatches to serper or browser via config
|
|
34
37
|
│ │ ├── serper.py # direct Serper API (web, news, scrape, suggestions)
|
|
35
38
|
│ │ ├── enrichment.py # kaspr, hunter, lemlist
|
|
@@ -41,23 +44,32 @@ oto/
|
|
|
41
44
|
│ │ ├── zohodesk.py # Zoho Desk (tickets/support)
|
|
42
45
|
│ │ ├── company.py # SIREN lookup multi-source
|
|
43
46
|
│ │ ├── whatsapp.py # WhatsApp messaging
|
|
47
|
+
│ │ ├── slack.py # Slack — send/read/list-channels/dm (bot+user tokens)
|
|
44
48
|
│ │ ├── audio.py # audio recording, transcription
|
|
45
|
-
│ │ ├── gemini.py # Gemini image generation
|
|
49
|
+
│ │ ├── gemini.py # Gemini image generation (gemini-3-pro-image)
|
|
50
|
+
│ │ ├── openai.py # OpenAI image generation (gpt-image-2)
|
|
46
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
|
|
47
54
|
│ │ ├── config.py # config & secrets management
|
|
48
55
|
│ │ └── skills.py # Claude Code skills (enable/disable)
|
|
49
56
|
│ └── tools/ # API clients
|
|
50
57
|
│ ├── google/ # gmail, drive, docs, sheets, slides, calendar, keep
|
|
51
58
|
│ ├── notion/ # pages, databases, search
|
|
52
59
|
│ ├── browser/ # linkedin, crunchbase, pappers, indeed, g2, google
|
|
60
|
+
│ ├── reddit/ # Reddit JSON API (no auth)
|
|
53
61
|
│ ├── whatsapp/ # Node.js bridge (whatsapp-web.js)
|
|
54
62
|
│ ├── sirene/ # INSEE SIRENE API
|
|
63
|
+
│ ├── culture/ # OpendatasoftClient générique + SpectacleClient (LES)
|
|
55
64
|
│ ├── serper/ # Google search (web, news)
|
|
56
65
|
│ ├── anthropic/ # Admin API (usage, costs)
|
|
57
66
|
│ ├── pennylane/ # Accounting
|
|
58
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)
|
|
59
70
|
│ ├── kaspr/, hunter/, lemlist/ # Enrichment & outreach
|
|
60
71
|
│ ├── zohodesk/ # Zoho Desk (tickets/support)
|
|
72
|
+
│ ├── gemini/, openai/ # Image generation (Gemini 3 Pro, gpt-image-2)
|
|
61
73
|
│ ├── pdf/ # pandoc+weasyprint wrapper, bundled CSS template (sober editorial)
|
|
62
74
|
│ └── folk/, zoho/, slack/, resend/ # CRM & messaging
|
|
63
75
|
├── skills/ # Claude Code skills
|
|
@@ -105,14 +117,19 @@ Key rules:
|
|
|
105
117
|
|
|
106
118
|
## Secrets & Config
|
|
107
119
|
|
|
108
|
-
Provider-based resolution
|
|
109
|
-
1.
|
|
110
|
-
2. Configured provider:
|
|
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
|
|
111
128
|
3. Default value
|
|
112
129
|
|
|
113
130
|
```bash
|
|
114
131
|
oto config # show providers + secrets status
|
|
115
|
-
oto config provider secrets
|
|
132
|
+
oto config provider secrets sops # switch to SOPS (default)
|
|
116
133
|
oto config provider search serper # switch search to serper (default) or browser
|
|
117
134
|
oto config secrets-push # upload local secrets.env → Scaleway
|
|
118
135
|
oto config secrets-pull # download Scaleway → local secrets.env
|
|
@@ -147,13 +164,18 @@ oto skills enable oto-google # enable one
|
|
|
147
164
|
oto skills disable oto-pennylane # disable one
|
|
148
165
|
```
|
|
149
166
|
|
|
150
|
-
##
|
|
167
|
+
## Deploy
|
|
151
168
|
|
|
152
|
-
|
|
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`).
|
|
153
174
|
|
|
154
175
|
```bash
|
|
155
176
|
# Bump version in oto/__init__.py, then:
|
|
156
|
-
hatch build && hatch publish -u __token__
|
|
177
|
+
hatch build && hatch publish -u __token__ \
|
|
178
|
+
-a "$(sops --decrypt --extract '["PYPI_TOKEN"]' ~/.otomata/secrets/secrets/secrets.yaml)"
|
|
157
179
|
gh release create vX.Y.Z --generate-notes dist/*
|
|
158
180
|
```
|
|
159
181
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: oto-cli
|
|
3
|
-
Version: 1.
|
|
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/
|
|
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/
|
|
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/
|
|
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`.
|
|
@@ -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"),
|
|
@@ -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
|
+
))
|