oto-cli 1.0.0__py3-none-any.whl
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/__init__.py +3 -0
- oto/cli.py +74 -0
- oto/commands/__init__.py +0 -0
- oto/commands/anthropic.py +64 -0
- oto/commands/audio.py +91 -0
- oto/commands/browser.py +257 -0
- oto/commands/company.py +23 -0
- oto/commands/enrichment.py +152 -0
- oto/commands/folk.py +238 -0
- oto/commands/google.py +355 -0
- oto/commands/notion.py +58 -0
- oto/commands/pennylane.py +272 -0
- oto/commands/search.py +36 -0
- oto/commands/sirene.py +201 -0
- oto/commands/skills.py +91 -0
- oto/commands/whatsapp.py +47 -0
- oto/config.py +167 -0
- oto/tools/.cache/notion/07a366cf2d7a0f77e7a1736805e94786e6053bb581b6b7179591b71d34843ce8.json +98 -0
- oto/tools/.cache/notion/0b21878dbd4bf5a67ff52104361a07ee575b03a075b7b81cbc4a0d5d4045f759.json +142 -0
- oto/tools/.cache/notion/0bf47e753ec29da03776bca010122393b4fc188af7f28dd11b456a74e5cae8f4.json +98 -0
- oto/tools/.cache/notion/1b2c924688fdcef8b7e3748f92ed4fdb232cadb387a7e9e157c438d32374ff97.json +4356 -0
- oto/tools/.cache/notion/2a66512043d3531dc900b8244fc13972c0abfe7760c10fbb7fc21f309238ad0b.json +132 -0
- oto/tools/.cache/notion/2b59aed15d0c747ec2cae70a1eb3f554b3e170875110b82f8e4614b761c84dcf.json +142 -0
- oto/tools/.cache/notion/2f7d1284021d30e842cc9a2b506e87b4a32c61e8e9abb616aec37828c441231d.json +220 -0
- oto/tools/.cache/notion/33eb548bce70fb7b0e8eb09211ba6c03ad22b6e7a289606b05c5d690f724d8e4.json +1659 -0
- oto/tools/.cache/notion/364d3a5e790f3d8b5137aae0a5a9ee151cd8c9f7dacb4fd669220f968405ea36.json +88 -0
- oto/tools/.cache/notion/3e7e836a6bf18f951d4887fefeacb146473491ad2cbaa1793c79af08b7ef45b6.json +132 -0
- oto/tools/.cache/notion/3f95b2d8bdf3d47e8f6c167d6d9a0889f27b64e8c78f0afdad538fecae070858.json +98 -0
- oto/tools/.cache/notion/46c38d10065fb68ca912692df53b6e54eb1f6f9cdff342de563020917e27ccf5.json +98 -0
- oto/tools/.cache/notion/51fa4d44044d751ca2319d87d607bd58a01004a24a8065808d65c273b35d3a10.json +210 -0
- oto/tools/.cache/notion/59888e9c297cda6836afb560fb4048b100f98eb3abead8b175fe92fbf858210b.json +186 -0
- oto/tools/.cache/notion/60df8d17a4e08fdbafb0ea398ff68d45cb79da7c01bd129e98264be1bff7ed1d.json +176 -0
- oto/tools/.cache/notion/62a44af3f86b5841d905789565a0208f80c1a304bcfc8889ccd55f61f29508b2.json +132 -0
- oto/tools/.cache/notion/69ec094fc12a40d1591abbd4f21295af672389963f900ea421a6d75b6f21f03b.json +176 -0
- oto/tools/.cache/notion/703fc49f07e73447e176fa69f31c7ff7bbb7c8c95325af4ca1a8664385bfeb0b.json +98 -0
- oto/tools/.cache/notion/7e247cf818c2a930de8a5a8599716900cb5af90570a9f701699dd8897fe43c8d.json +88 -0
- oto/tools/.cache/notion/86741d020ca89789bda8156315ead5e980068c19754baf6e611ffc618557dc06.json +115 -0
- oto/tools/.cache/notion/8cc6e5e1d955310412df09c9179ec67d42767d1b557262e28bc2afea05ab7c24.json +210 -0
- oto/tools/.cache/notion/989d88c22dd009e5683987e70671d1e20fd6a5a3e9ee7670db9ea602fe8fbded.json +4605 -0
- oto/tools/.cache/notion/999d01d80c8ef0c856572028e31e701cad8c5704b8886388b756d32ef50cbfaf.json +132 -0
- oto/tools/.cache/notion/9f10e5fa6404211b156b2f5c22a0644e5eb0c49774d2b4f8d768211f0c6aea22.json +3380 -0
- oto/tools/.cache/notion/9f950bdfc2aeb9ae5e1b185e827636f83fa44a9d7de0d1462d87926508b26907.json +142 -0
- oto/tools/.cache/notion/a4719c76c9ff4c4b056e87689e2f487770c35d82d70acb098ead7712e0d3be8d.json +142 -0
- oto/tools/.cache/notion/a808160a7352fa1a3029ef1e2929dfa99b5021b0d22ce1cfb9efe3c68f7e7af1.json +98 -0
- oto/tools/.cache/notion/a9c29806556409fbfcd7622b876244caed085c63c70380dc6caacd162e6d05a9.json +132 -0
- oto/tools/.cache/notion/b3142c5509452a15382cade5cb81324a26d1946d128a1b4342a143872f9354fe.json +149 -0
- oto/tools/.cache/notion/b516a0820a66ae04229bfcb7f0463be0867f31a8dc1b875e57aba02b970a7a31.json +98 -0
- oto/tools/.cache/notion/c175f8c35c81ebd2fb9e4bf4d2f1cc29f98b45006f59354c9f33ee780609d896.json +247 -0
- oto/tools/.cache/notion/c950254390b901d475937c868e27e74960720fe65e2d8506bd39e30c634e619d.json +98 -0
- oto/tools/.cache/notion/cac99fb54fa178e1a7f72e5212e1e99eff5fe72a1fa52f65fcee432bcf528b53.json +166 -0
- oto/tools/.cache/notion/e38237abecf19ce5b9a92ed90723d2642a11b0f1ef189b1a1fc10b7ebcd3d4a4.json +88 -0
- oto/tools/.cache/notion/e58e933def9b7000d791c83f6660adbecac59ab61e54f8d855a52d80200eb929.json +292 -0
- oto/tools/.cache/notion/f8088377a1049850b0ec1acabf09da8504fb1f194479cc43ce4c31db3895afcd.json +176 -0
- oto/tools/.cache/notion/fc2ccb6edbe2020010e036a69e7ece3ab3f007c9a4fed39066d6792d332e65af.json +142 -0
- oto/tools/.cache/notion/ff117c74176f8fc26aba7f06649c7794dc4a0c51194033f453b16c8af8ac4201.json +36 -0
- oto/tools/__init__.py +1 -0
- oto/tools/anthropic/__init__.py +5 -0
- oto/tools/anthropic/client.py +354 -0
- oto/tools/anthropic_batch/__init__.py +5 -0
- oto/tools/anthropic_batch/client.py +352 -0
- oto/tools/apollo/__init__.py +5 -0
- oto/tools/apollo/client.py +191 -0
- oto/tools/attio/__init__.py +5 -0
- oto/tools/attio/client.py +242 -0
- oto/tools/audio/__init__.py +5 -0
- oto/tools/audio/client.py +116 -0
- oto/tools/browser/__init__.py +26 -0
- oto/tools/browser/crunchbase.py +423 -0
- oto/tools/browser/g2.py +236 -0
- oto/tools/browser/indeed.py +282 -0
- oto/tools/browser/linkedin.py +821 -0
- oto/tools/browser/pappers.py +344 -0
- oto/tools/clearbit/__init__.py +5 -0
- oto/tools/clearbit/client.py +126 -0
- oto/tools/collective/__init__.py +5 -0
- oto/tools/collective/client.py +333 -0
- oto/tools/common/__init__.py +5 -0
- oto/tools/common/rate_limiter.py +457 -0
- oto/tools/figma/__init__.py +5 -0
- oto/tools/figma/client.py +254 -0
- oto/tools/folk/__init__.py +5 -0
- oto/tools/folk/client.py +203 -0
- oto/tools/gemini/__init__.py +5 -0
- oto/tools/gemini/client.py +264 -0
- oto/tools/google/__init__.py +1 -0
- oto/tools/google/calendar/__init__.py +0 -0
- oto/tools/google/calendar/lib/__init__.py +0 -0
- oto/tools/google/calendar/lib/calendar_client.py +155 -0
- oto/tools/google/credentials.py +177 -0
- oto/tools/google/docs/get_section.py +53 -0
- oto/tools/google/docs/insert_section.py +56 -0
- oto/tools/google/docs/insert_text.py +99 -0
- oto/tools/google/docs/lib/__init__.py +0 -0
- oto/tools/google/docs/lib/docs_client.py +341 -0
- oto/tools/google/docs/list_headings.py +44 -0
- oto/tools/google/docs/move_section.py +37 -0
- oto/tools/google/docs/replace_section.py +47 -0
- oto/tools/google/docs/sync/__init__.py +1 -0
- oto/tools/google/docs/sync/gdoc_sync.py +427 -0
- oto/tools/google/drive/README.md +308 -0
- oto/tools/google/drive/check_quota.py +31 -0
- oto/tools/google/drive/copy_file.py +76 -0
- oto/tools/google/drive/create_folder.py +58 -0
- oto/tools/google/drive/download_file.py +60 -0
- oto/tools/google/drive/export_doc.py +64 -0
- oto/tools/google/drive/export_slides.py +95 -0
- oto/tools/google/drive/extract_slides_content.py +113 -0
- oto/tools/google/drive/lib/drive_client.py +454 -0
- oto/tools/google/drive/list_files.py +97 -0
- oto/tools/google/drive/list_shared_drives.py +32 -0
- oto/tools/google/drive/move_file.py +57 -0
- oto/tools/google/drive/requirements.txt +4 -0
- oto/tools/google/drive/tool.yaml +210 -0
- oto/tools/google/drive/upload_file.py +90 -0
- oto/tools/google/gmail/__init__.py +1 -0
- oto/tools/google/gmail/get_message.py +33 -0
- oto/tools/google/gmail/lib/__init__.py +1 -0
- oto/tools/google/gmail/lib/gmail_client.py +385 -0
- oto/tools/google/gmail/list_messages.py +37 -0
- oto/tools/google/gmail/requirements.txt +3 -0
- oto/tools/google/gmail/search.py +34 -0
- oto/tools/google/gmail/send.py +39 -0
- oto/tools/google/gmail/tool.yaml +107 -0
- oto/tools/google/keep/__init__.py +0 -0
- oto/tools/google/keep/create_note.py +42 -0
- oto/tools/google/keep/get_master_token.py +101 -0
- oto/tools/google/keep/get_note.py +33 -0
- oto/tools/google/keep/lib/__init__.py +0 -0
- oto/tools/google/keep/lib/keep_client.py +273 -0
- oto/tools/google/keep/list_notes.py +42 -0
- oto/tools/google/keep/search.py +34 -0
- oto/tools/google/sheets/create_sheet_direct.py +157 -0
- oto/tools/google/sheets/create_sheet_from_csv.py +167 -0
- oto/tools/google/sheets/create_sheet_in_folder.py +165 -0
- oto/tools/google/sheets/create_sheet_via_drive.py +157 -0
- oto/tools/google/sheets/tool.yaml +74 -0
- oto/tools/google/slides/.folders +19 -0
- oto/tools/google/slides/321-LAYOUTS.md +203 -0
- oto/tools/google/slides/README.md +210 -0
- oto/tools/google/slides/USAGE-EDIT.md +626 -0
- oto/tools/google/slides/USAGE.md +158 -0
- oto/tools/google/slides/create-demo-presentation.py +348 -0
- oto/tools/google/slides/generate_slides.py +437 -0
- oto/tools/google/slides/lib/__init__.py +1 -0
- oto/tools/google/slides/lib/content_filler.py +579 -0
- oto/tools/google/slides/lib/layout_mappings.py +182 -0
- oto/tools/google/slides/lib/slides_client.py +1239 -0
- oto/tools/google/slides/requirements.txt +5 -0
- oto/tools/google/slides/test-copy-and-edit.py +187 -0
- oto/tools/google/slides/test-copy-slide.py +148 -0
- oto/tools/google/slides/test-edit.py +183 -0
- oto/tools/google/slides/tool.yaml +37 -0
- oto/tools/groq/__init__.py +5 -0
- oto/tools/groq/client.py +165 -0
- oto/tools/hithorizons/__init__.py +5 -0
- oto/tools/hithorizons/client.py +168 -0
- oto/tools/hunter/__init__.py +5 -0
- oto/tools/hunter/client.py +104 -0
- oto/tools/kaspr/__init__.py +5 -0
- oto/tools/kaspr/client.py +80 -0
- oto/tools/lemlist/__init__.py +5 -0
- oto/tools/lemlist/client.py +486 -0
- oto/tools/mistral/__init__.py +5 -0
- oto/tools/mistral/client.py +149 -0
- oto/tools/naf/__init__.py +5 -0
- oto/tools/naf/suggester.py +140 -0
- oto/tools/notion/README.md +310 -0
- oto/tools/notion/append_blocks.py +204 -0
- oto/tools/notion/append_blocks_chunked.py +296 -0
- oto/tools/notion/count_database_entries.py +47 -0
- oto/tools/notion/create_database_from_csv.py +269 -0
- oto/tools/notion/create_page.py +65 -0
- oto/tools/notion/get_database.py +67 -0
- oto/tools/notion/get_page.py +78 -0
- oto/tools/notion/known-pages.md +75 -0
- oto/tools/notion/lib/markdown_converter.py +287 -0
- oto/tools/notion/lib/notion_client.py +313 -0
- oto/tools/notion/list_teamspaces.py +244 -0
- oto/tools/notion/notion +122 -0
- oto/tools/notion/query_database.py +77 -0
- oto/tools/notion/requirements.txt +1 -0
- oto/tools/notion/search.py +93 -0
- oto/tools/notion/tool.yaml +235 -0
- oto/tools/notion/update_page.py +65 -0
- oto/tools/pennylane/__init__.py +5 -0
- oto/tools/pennylane/client.py +386 -0
- oto/tools/phantombuster/__init__.py +5 -0
- oto/tools/phantombuster/client.py +177 -0
- oto/tools/resend/__init__.py +5 -0
- oto/tools/resend/client.py +149 -0
- oto/tools/serpapi/__init__.py +5 -0
- oto/tools/serpapi/client.py +128 -0
- oto/tools/serper/__init__.py +5 -0
- oto/tools/serper/client.py +210 -0
- oto/tools/sirene/__init__.py +34 -0
- oto/tools/sirene/client.py +474 -0
- oto/tools/sirene/data/naf_codes.txt +732 -0
- oto/tools/sirene/entreprises.py +175 -0
- oto/tools/sirene/stock.py +377 -0
- oto/tools/slack/__init__.py +5 -0
- oto/tools/slack/client.py +202 -0
- oto/tools/unsplash/__init__.py +5 -0
- oto/tools/unsplash/client.py +197 -0
- oto/tools/whatsapp/__init__.py +3 -0
- oto/tools/whatsapp/client.py +68 -0
- oto/tools/whatsapp/node/package-lock.json +1564 -0
- oto/tools/whatsapp/node/package.json +11 -0
- oto/tools/whatsapp/node/whatsapp.mjs +357 -0
- oto/tools/wttj/__init__.py +5 -0
- oto/tools/wttj/client.py +251 -0
- oto/tools/zerobounce/__init__.py +5 -0
- oto/tools/zerobounce/client.py +93 -0
- oto_cli-1.0.0.dist-info/METADATA +171 -0
- oto_cli-1.0.0.dist-info/RECORD +217 -0
- oto_cli-1.0.0.dist-info/WHEEL +4 -0
- oto_cli-1.0.0.dist-info/entry_points.txt +2 -0
- oto_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
oto/commands/folk.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Folk CRM commands."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import typer
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
app = typer.Typer(help="Folk CRM — contacts, companies, deals, notes")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _out(data):
|
|
11
|
+
print(json.dumps(data, indent=2, ensure_ascii=False))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _client():
|
|
15
|
+
from oto.tools.folk import FolkClient
|
|
16
|
+
return FolkClient()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# --- Groups ---
|
|
20
|
+
|
|
21
|
+
@app.command("groups")
|
|
22
|
+
def groups():
|
|
23
|
+
"""List all groups (pipelines)."""
|
|
24
|
+
items = _client().list_groups()
|
|
25
|
+
_out({"count": len(items), "groups": items})
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# --- People ---
|
|
29
|
+
|
|
30
|
+
@app.command("people")
|
|
31
|
+
def people(
|
|
32
|
+
search: Optional[str] = typer.Argument(None, help="Search by name"),
|
|
33
|
+
group: Optional[str] = typer.Option(None, "--group", "-g", help="Filter by group ID"),
|
|
34
|
+
):
|
|
35
|
+
"""List contacts."""
|
|
36
|
+
c = _client()
|
|
37
|
+
filters = {}
|
|
38
|
+
if search:
|
|
39
|
+
filters["fullName"] = search
|
|
40
|
+
items = c.list_people(**filters)
|
|
41
|
+
result = []
|
|
42
|
+
for p in items:
|
|
43
|
+
result.append({
|
|
44
|
+
"id": p["id"],
|
|
45
|
+
"name": p.get("fullName") or f"{p.get('firstName','')} {p.get('lastName','')}".strip(),
|
|
46
|
+
"jobTitle": p.get("jobTitle", ""),
|
|
47
|
+
"emails": p.get("emails", []),
|
|
48
|
+
"companies": [co.get("name", "") for co in p.get("companies", [])],
|
|
49
|
+
})
|
|
50
|
+
_out({"count": len(result), "people": result})
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@app.command("person")
|
|
54
|
+
def person(person_id: str = typer.Argument(..., help="Person ID (per_...)")):
|
|
55
|
+
"""Get a person's details."""
|
|
56
|
+
_out(_client().get_person(person_id))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@app.command("add-person")
|
|
60
|
+
def add_person(
|
|
61
|
+
first_name: str = typer.Argument(...),
|
|
62
|
+
last_name: Optional[str] = typer.Option(None, "--last", "-l"),
|
|
63
|
+
email: Optional[str] = typer.Option(None, "--email", "-e"),
|
|
64
|
+
phone: Optional[str] = typer.Option(None, "--phone"),
|
|
65
|
+
job_title: Optional[str] = typer.Option(None, "--title", "-t"),
|
|
66
|
+
company: Optional[str] = typer.Option(None, "--company", "-c", help="Company name (creates if needed)"),
|
|
67
|
+
group: Optional[str] = typer.Option(None, "--group", "-g", help="Group ID"),
|
|
68
|
+
):
|
|
69
|
+
"""Create a contact."""
|
|
70
|
+
emails = [email] if email else None
|
|
71
|
+
phones = [phone] if phone else None
|
|
72
|
+
group_ids = [group] if group else None
|
|
73
|
+
result = _client().create_person(
|
|
74
|
+
first_name=first_name, last_name=last_name,
|
|
75
|
+
emails=emails, phones=phones, job_title=job_title,
|
|
76
|
+
company_name=company, group_ids=group_ids,
|
|
77
|
+
)
|
|
78
|
+
_out(result)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@app.command("update-person")
|
|
82
|
+
def update_person(
|
|
83
|
+
person_id: str = typer.Argument(..., help="Person ID (per_...)"),
|
|
84
|
+
first_name: Optional[str] = typer.Option(None, "--first"),
|
|
85
|
+
last_name: Optional[str] = typer.Option(None, "--last"),
|
|
86
|
+
email: Optional[str] = typer.Option(None, "--email", "-e"),
|
|
87
|
+
job_title: Optional[str] = typer.Option(None, "--title", "-t"),
|
|
88
|
+
company: Optional[str] = typer.Option(None, "--company", "-c"),
|
|
89
|
+
group: Optional[str] = typer.Option(None, "--group", "-g", help="Set group (replaces all)"),
|
|
90
|
+
):
|
|
91
|
+
"""Update a contact."""
|
|
92
|
+
fields = {}
|
|
93
|
+
if first_name:
|
|
94
|
+
fields["firstName"] = first_name
|
|
95
|
+
if last_name:
|
|
96
|
+
fields["lastName"] = last_name
|
|
97
|
+
if email:
|
|
98
|
+
fields["emails"] = [email]
|
|
99
|
+
if job_title:
|
|
100
|
+
fields["jobTitle"] = job_title
|
|
101
|
+
if company:
|
|
102
|
+
fields["companies"] = [{"name": company}]
|
|
103
|
+
if group:
|
|
104
|
+
fields["groups"] = [{"id": group}]
|
|
105
|
+
_out(_client().update_person(person_id, **fields))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@app.command("delete-person")
|
|
109
|
+
def delete_person(person_id: str = typer.Argument(..., help="Person ID (per_...)")):
|
|
110
|
+
"""Delete a contact."""
|
|
111
|
+
_client().delete_person(person_id)
|
|
112
|
+
print(f"Deleted {person_id}")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# --- Companies ---
|
|
116
|
+
|
|
117
|
+
@app.command("companies")
|
|
118
|
+
def companies(search: Optional[str] = typer.Argument(None, help="Search by name")):
|
|
119
|
+
"""List companies."""
|
|
120
|
+
filters = {}
|
|
121
|
+
if search:
|
|
122
|
+
filters["name"] = search
|
|
123
|
+
items = _client().list_companies(**filters)
|
|
124
|
+
_out({"count": len(items), "companies": items})
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _parse_fields(fields: Optional[list[str]]) -> dict:
|
|
128
|
+
"""Parse --field key=value pairs into a dict."""
|
|
129
|
+
if not fields:
|
|
130
|
+
return {}
|
|
131
|
+
result = {}
|
|
132
|
+
for f in fields:
|
|
133
|
+
if "=" not in f:
|
|
134
|
+
raise typer.BadParameter(f"Invalid field format: {f!r} (expected key=value)")
|
|
135
|
+
key, value = f.split("=", 1)
|
|
136
|
+
result[key] = value
|
|
137
|
+
return result
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@app.command("add-company")
|
|
141
|
+
def add_company(
|
|
142
|
+
name: str = typer.Argument(...),
|
|
143
|
+
industry: Optional[str] = typer.Option(None, "--industry", "-i"),
|
|
144
|
+
group: Optional[str] = typer.Option(None, "--group", "-g", help="Group ID"),
|
|
145
|
+
fields: Optional[list[str]] = typer.Option(None, "--field", "-f", help="Custom field key=value (requires --group)"),
|
|
146
|
+
):
|
|
147
|
+
"""Create a company."""
|
|
148
|
+
extra: dict = {}
|
|
149
|
+
if group:
|
|
150
|
+
extra["groups"] = [{"id": group}]
|
|
151
|
+
custom = _parse_fields(fields)
|
|
152
|
+
if custom:
|
|
153
|
+
if not group:
|
|
154
|
+
raise typer.BadParameter("--field requires --group")
|
|
155
|
+
extra["customFieldValues"] = {group: custom}
|
|
156
|
+
_out(_client().create_company(name=name, industry=industry, **extra))
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@app.command("update-company")
|
|
160
|
+
def update_company(
|
|
161
|
+
company_id: str = typer.Argument(..., help="Company ID (com_...)"),
|
|
162
|
+
name: Optional[str] = typer.Option(None, "--name", "-n"),
|
|
163
|
+
industry: Optional[str] = typer.Option(None, "--industry", "-i"),
|
|
164
|
+
group: Optional[str] = typer.Option(None, "--group", "-g", help="Group ID"),
|
|
165
|
+
fields: Optional[list[str]] = typer.Option(None, "--field", "-f", help="Custom field key=value (requires --group)"),
|
|
166
|
+
):
|
|
167
|
+
"""Update a company."""
|
|
168
|
+
data: dict = {}
|
|
169
|
+
if name:
|
|
170
|
+
data["name"] = name
|
|
171
|
+
if industry:
|
|
172
|
+
data["industry"] = industry
|
|
173
|
+
if group:
|
|
174
|
+
data["groups"] = [{"id": group}]
|
|
175
|
+
custom = _parse_fields(fields)
|
|
176
|
+
if custom:
|
|
177
|
+
if not group:
|
|
178
|
+
raise typer.BadParameter("--field requires --group")
|
|
179
|
+
data["customFieldValues"] = {group: custom}
|
|
180
|
+
_out(_client().update_company(company_id, **data))
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@app.command("delete-company")
|
|
184
|
+
def delete_company(company_id: str = typer.Argument(..., help="Company ID (com_...)")):
|
|
185
|
+
"""Delete a company."""
|
|
186
|
+
_client().delete_company(company_id)
|
|
187
|
+
print(f"Deleted {company_id}")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# --- Deals ---
|
|
191
|
+
|
|
192
|
+
@app.command("deals")
|
|
193
|
+
def deals(
|
|
194
|
+
group: str = typer.Option(None, "--group", "-g", help="Group ID (default: first group)"),
|
|
195
|
+
object_type: str = typer.Option("deals", "--type", "-t", help="Object type name"),
|
|
196
|
+
):
|
|
197
|
+
"""List deals in a pipeline group."""
|
|
198
|
+
c = _client()
|
|
199
|
+
if not group:
|
|
200
|
+
groups = c.list_groups()
|
|
201
|
+
if not groups:
|
|
202
|
+
print("No groups found.")
|
|
203
|
+
raise typer.Exit(1)
|
|
204
|
+
group = groups[0]["id"]
|
|
205
|
+
items = c.list_deals(group, object_type)
|
|
206
|
+
_out({"count": len(items), "group": group, "deals": items})
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# --- Notes ---
|
|
210
|
+
|
|
211
|
+
@app.command("notes")
|
|
212
|
+
def notes(entity_id: str = typer.Argument(..., help="Entity ID (per_/com_/obj_)")):
|
|
213
|
+
"""List notes for an entity."""
|
|
214
|
+
items = _client().list_notes(entity_id)
|
|
215
|
+
_out({"count": len(items), "notes": items})
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@app.command("add-note")
|
|
219
|
+
def add_note(
|
|
220
|
+
entity_id: str = typer.Argument(..., help="Entity ID"),
|
|
221
|
+
content: str = typer.Argument(..., help="Note content (markdown)"),
|
|
222
|
+
):
|
|
223
|
+
"""Add a note to a person/company/deal."""
|
|
224
|
+
_out(_client().create_note(entity_id, content))
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# --- Interactions ---
|
|
228
|
+
|
|
229
|
+
@app.command("add-interaction")
|
|
230
|
+
def add_interaction(
|
|
231
|
+
entity_id: str = typer.Argument(..., help="Person or company ID"),
|
|
232
|
+
type: str = typer.Argument(..., help="Type: call, meeting, email, linkedin, etc."),
|
|
233
|
+
title: str = typer.Argument(..., help="Short title"),
|
|
234
|
+
content: Optional[str] = typer.Option(None, "--content", "-c"),
|
|
235
|
+
date: Optional[str] = typer.Option(None, "--date", "-d", help="ISO datetime"),
|
|
236
|
+
):
|
|
237
|
+
"""Log an interaction (call, meeting, email...)."""
|
|
238
|
+
_out(_client().create_interaction(entity_id, type, title, content, date))
|
oto/commands/google.py
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
"""Google Workspace commands (Drive, Docs, Sheets, Slides, Gmail, Calendar, Auth)."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
app = typer.Typer(help="Google Workspace tools (Drive, Docs, Sheets, Slides, Gmail, Calendar)")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _apply_signature(client, body: str, html: Optional[str]) -> Optional[str]:
|
|
10
|
+
"""Convert plain text body to HTML with Gmail signature appended."""
|
|
11
|
+
import html as html_mod
|
|
12
|
+
signature = client.get_signature()
|
|
13
|
+
if not signature:
|
|
14
|
+
return html
|
|
15
|
+
body_html = html or '<div dir="ltr">' + html_mod.escape(body).replace('\n', '<br>') + '</div>'
|
|
16
|
+
return body_html + '<br>--<br>' + signature
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.command("drive-list")
|
|
20
|
+
def drive_list(
|
|
21
|
+
folder_id: Optional[str] = typer.Option(None, help="Filter by parent folder ID"),
|
|
22
|
+
query: Optional[str] = typer.Option(None, help="Custom query filter"),
|
|
23
|
+
limit: int = typer.Option(100, help="Max results"),
|
|
24
|
+
account: Optional[str] = typer.Option(None, "--account", "-a", help="Google account name"),
|
|
25
|
+
):
|
|
26
|
+
"""List files in Google Drive."""
|
|
27
|
+
from oto.tools.google.drive.lib.drive_client import DriveClient
|
|
28
|
+
import json
|
|
29
|
+
|
|
30
|
+
client = DriveClient(account=account)
|
|
31
|
+
files = client.list_files(folder_id=folder_id, query=query, page_size=limit)
|
|
32
|
+
print(json.dumps({"count": len(files), "files": files}, indent=2))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@app.command("drive-download")
|
|
36
|
+
def drive_download(
|
|
37
|
+
file_id: str = typer.Argument(..., help="Google Drive file ID"),
|
|
38
|
+
output: str = typer.Argument(..., help="Output path"),
|
|
39
|
+
account: Optional[str] = typer.Option(None, "--account", "-a", help="Google account name"),
|
|
40
|
+
):
|
|
41
|
+
"""Download a file from Google Drive."""
|
|
42
|
+
from oto.tools.google.drive.lib.drive_client import DriveClient
|
|
43
|
+
|
|
44
|
+
client = DriveClient(account=account)
|
|
45
|
+
result = client.download_file(file_id, output)
|
|
46
|
+
print(f"Downloaded: {result['filename']} -> {result['output_path']}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@app.command("drive-upload")
|
|
50
|
+
def drive_upload(
|
|
51
|
+
file_path: str = typer.Argument(..., help="Local file path to upload"),
|
|
52
|
+
folder_id: Optional[str] = typer.Option(None, help="Target folder ID in Drive"),
|
|
53
|
+
name: Optional[str] = typer.Option(None, help="Custom filename (defaults to local name)"),
|
|
54
|
+
account: Optional[str] = typer.Option(None, "--account", "-a", help="Google account name"),
|
|
55
|
+
):
|
|
56
|
+
"""Upload a file to Google Drive."""
|
|
57
|
+
from oto.tools.google.drive.lib.drive_client import DriveClient
|
|
58
|
+
import json
|
|
59
|
+
|
|
60
|
+
client = DriveClient(account=account)
|
|
61
|
+
result = client.upload_file(local_path=file_path, folder_id=folder_id, file_name=name)
|
|
62
|
+
print(json.dumps(result, indent=2))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@app.command("docs-headings")
|
|
66
|
+
def docs_headings(
|
|
67
|
+
doc_id: str = typer.Argument(..., help="Google Docs document ID"),
|
|
68
|
+
):
|
|
69
|
+
"""List headings in a Google Doc."""
|
|
70
|
+
from oto.tools.google.docs.lib.docs_client import DocsClient
|
|
71
|
+
import json
|
|
72
|
+
|
|
73
|
+
client = DocsClient()
|
|
74
|
+
headings = client.list_headings(doc_id)
|
|
75
|
+
print(json.dumps(headings, indent=2))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@app.command("docs-section")
|
|
79
|
+
def docs_section(
|
|
80
|
+
doc_id: str = typer.Argument(..., help="Google Docs document ID"),
|
|
81
|
+
heading: str = typer.Argument(..., help="Heading text to find"),
|
|
82
|
+
):
|
|
83
|
+
"""Get content of a section in a Google Doc."""
|
|
84
|
+
from oto.tools.google.docs.lib.docs_client import DocsClient
|
|
85
|
+
|
|
86
|
+
client = DocsClient()
|
|
87
|
+
section = client.get_section_content(doc_id, heading)
|
|
88
|
+
if section:
|
|
89
|
+
print(f"# {section.title}\n")
|
|
90
|
+
print(section.content)
|
|
91
|
+
else:
|
|
92
|
+
print(f"Section not found: {heading}")
|
|
93
|
+
raise typer.Exit(1)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@app.command("auth")
|
|
97
|
+
def auth(
|
|
98
|
+
name: str = typer.Argument("default", help="Account name (e.g. 'gmail', 'work')"),
|
|
99
|
+
list_accounts: bool = typer.Option(False, "--list", "-l", help="List configured accounts"),
|
|
100
|
+
):
|
|
101
|
+
"""Set up or list Google OAuth accounts."""
|
|
102
|
+
from oto.tools.google.credentials import list_accounts as _list_accounts, setup_account, DEFAULT_SCOPES as DRIVE_SCOPES
|
|
103
|
+
from oto.tools.google.gmail.lib.gmail_client import SCOPES as GMAIL_SCOPES
|
|
104
|
+
from oto.tools.google.calendar.lib.calendar_client import SCOPES as CALENDAR_SCOPES
|
|
105
|
+
|
|
106
|
+
ALL_SCOPES = list(set(GMAIL_SCOPES + CALENDAR_SCOPES + DRIVE_SCOPES))
|
|
107
|
+
|
|
108
|
+
if list_accounts:
|
|
109
|
+
accounts = _list_accounts()
|
|
110
|
+
if not accounts:
|
|
111
|
+
print("No accounts configured. Run: oto google auth <name>")
|
|
112
|
+
else:
|
|
113
|
+
for a in accounts:
|
|
114
|
+
print(f" {a}")
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
print(f"Setting up account '{name}'... Opening browser for Google consent.")
|
|
118
|
+
setup_account(name, ALL_SCOPES)
|
|
119
|
+
print(f"Account '{name}' configured.")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@app.command("calendar-list")
|
|
123
|
+
def calendar_list(
|
|
124
|
+
account: Optional[str] = typer.Option(None, "--account", "-a", help="Google account name"),
|
|
125
|
+
):
|
|
126
|
+
"""List available calendars."""
|
|
127
|
+
from oto.tools.google.calendar.lib.calendar_client import CalendarClient
|
|
128
|
+
import json
|
|
129
|
+
|
|
130
|
+
client = CalendarClient(account=account)
|
|
131
|
+
calendars = client.list_calendars()
|
|
132
|
+
print(json.dumps({"count": len(calendars), "calendars": calendars}, indent=2, ensure_ascii=False))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@app.command("calendar-today")
|
|
136
|
+
def calendar_today(
|
|
137
|
+
calendar_id: str = typer.Option("primary", "--calendar", "-c", help="Calendar ID"),
|
|
138
|
+
account: Optional[str] = typer.Option(None, "--account", "-a", help="Google account name"),
|
|
139
|
+
):
|
|
140
|
+
"""List today's events."""
|
|
141
|
+
from oto.tools.google.calendar.lib.calendar_client import CalendarClient
|
|
142
|
+
import json
|
|
143
|
+
|
|
144
|
+
client = CalendarClient(account=account)
|
|
145
|
+
events = client.today(calendar_id=calendar_id)
|
|
146
|
+
print(json.dumps({"count": len(events), "events": events}, indent=2, ensure_ascii=False))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@app.command("calendar-upcoming")
|
|
150
|
+
def calendar_upcoming(
|
|
151
|
+
days: int = typer.Option(7, "--days", "-d", help="Number of days ahead"),
|
|
152
|
+
calendar_id: str = typer.Option("primary", "--calendar", "-c", help="Calendar ID"),
|
|
153
|
+
limit: int = typer.Option(50, "--limit", "-n", help="Max events"),
|
|
154
|
+
account: Optional[str] = typer.Option(None, "--account", "-a", help="Google account name"),
|
|
155
|
+
):
|
|
156
|
+
"""List upcoming events (default: next 7 days)."""
|
|
157
|
+
from oto.tools.google.calendar.lib.calendar_client import CalendarClient
|
|
158
|
+
import json
|
|
159
|
+
|
|
160
|
+
client = CalendarClient(account=account)
|
|
161
|
+
events = client.upcoming(days=days, calendar_id=calendar_id, max_results=limit)
|
|
162
|
+
print(json.dumps({"count": len(events), "events": events}, indent=2, ensure_ascii=False))
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@app.command("calendar-search")
|
|
166
|
+
def calendar_search(
|
|
167
|
+
query: str = typer.Argument(..., help="Search query"),
|
|
168
|
+
days: int = typer.Option(30, "--days", "-d", help="Search window in days"),
|
|
169
|
+
calendar_id: str = typer.Option("primary", "--calendar", "-c", help="Calendar ID"),
|
|
170
|
+
limit: int = typer.Option(20, "--limit", "-n", help="Max events"),
|
|
171
|
+
account: Optional[str] = typer.Option(None, "--account", "-a", help="Google account name"),
|
|
172
|
+
):
|
|
173
|
+
"""Search calendar events."""
|
|
174
|
+
from oto.tools.google.calendar.lib.calendar_client import CalendarClient
|
|
175
|
+
from datetime import datetime, timedelta, timezone
|
|
176
|
+
import json
|
|
177
|
+
|
|
178
|
+
client = CalendarClient(account=account)
|
|
179
|
+
now = datetime.now(timezone.utc)
|
|
180
|
+
events = client.list_events(
|
|
181
|
+
calendar_id=calendar_id,
|
|
182
|
+
time_min=now.isoformat(),
|
|
183
|
+
time_max=(now + timedelta(days=days)).isoformat(),
|
|
184
|
+
max_results=limit,
|
|
185
|
+
query=query,
|
|
186
|
+
)
|
|
187
|
+
print(json.dumps({"count": len(events), "events": events}, indent=2, ensure_ascii=False))
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@app.command("calendar-get")
|
|
191
|
+
def calendar_get(
|
|
192
|
+
event_id: str = typer.Argument(..., help="Event ID"),
|
|
193
|
+
calendar_id: str = typer.Option("primary", "--calendar", "-c", help="Calendar ID"),
|
|
194
|
+
account: Optional[str] = typer.Option(None, "--account", "-a", help="Google account name"),
|
|
195
|
+
):
|
|
196
|
+
"""Get details of a calendar event."""
|
|
197
|
+
from oto.tools.google.calendar.lib.calendar_client import CalendarClient
|
|
198
|
+
import json
|
|
199
|
+
|
|
200
|
+
client = CalendarClient(account=account)
|
|
201
|
+
event = client.get_event(event_id, calendar_id=calendar_id)
|
|
202
|
+
print(json.dumps(event, indent=2, ensure_ascii=False))
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@app.command("gmail-list")
|
|
206
|
+
def gmail_list(
|
|
207
|
+
query: Optional[str] = typer.Option(None, help="Gmail search query"),
|
|
208
|
+
label: Optional[str] = typer.Option(None, help="Filter by label ID"),
|
|
209
|
+
limit: int = typer.Option(20, "--limit", "-n", help="Max messages"),
|
|
210
|
+
account: Optional[str] = typer.Option(None, "--account", "-a", help="Google account name"),
|
|
211
|
+
):
|
|
212
|
+
"""List recent Gmail messages."""
|
|
213
|
+
from oto.tools.google.gmail.lib.gmail_client import GmailClient
|
|
214
|
+
import json
|
|
215
|
+
|
|
216
|
+
client = GmailClient(account=account)
|
|
217
|
+
label_ids = [label] if label else None
|
|
218
|
+
messages = client.list_messages(query=query, label_ids=label_ids, max_results=limit)
|
|
219
|
+
print(json.dumps({"count": len(messages), "messages": messages}, indent=2, ensure_ascii=False))
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@app.command("gmail-search")
|
|
223
|
+
def gmail_search(
|
|
224
|
+
query: str = typer.Argument(..., help="Gmail search query (e.g. 'is:unread', 'from:user@example.com')"),
|
|
225
|
+
limit: int = typer.Option(20, "--limit", "-n", help="Max messages"),
|
|
226
|
+
account: Optional[str] = typer.Option(None, "--account", "-a", help="Google account name"),
|
|
227
|
+
):
|
|
228
|
+
"""Search Gmail messages."""
|
|
229
|
+
from oto.tools.google.gmail.lib.gmail_client import GmailClient
|
|
230
|
+
import json
|
|
231
|
+
|
|
232
|
+
client = GmailClient(account=account)
|
|
233
|
+
messages = client.search(query=query, max_results=limit)
|
|
234
|
+
print(json.dumps({"count": len(messages), "messages": messages}, indent=2, ensure_ascii=False))
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@app.command("gmail-get")
|
|
238
|
+
def gmail_get(
|
|
239
|
+
message_id: str = typer.Argument(..., help="Gmail message ID"),
|
|
240
|
+
account: Optional[str] = typer.Option(None, "--account", "-a", help="Google account name"),
|
|
241
|
+
):
|
|
242
|
+
"""Read a Gmail message."""
|
|
243
|
+
from oto.tools.google.gmail.lib.gmail_client import GmailClient
|
|
244
|
+
import json
|
|
245
|
+
|
|
246
|
+
client = GmailClient(account=account)
|
|
247
|
+
message = client.get_message(message_id)
|
|
248
|
+
print(json.dumps(message, indent=2, ensure_ascii=False))
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@app.command("gmail-attachments")
|
|
252
|
+
def gmail_attachments(
|
|
253
|
+
message_id: str = typer.Argument(..., help="Gmail message ID"),
|
|
254
|
+
output: str = typer.Option(".", "--output", "-o", help="Output directory"),
|
|
255
|
+
account: Optional[str] = typer.Option(None, "--account", "-a", help="Google account name"),
|
|
256
|
+
):
|
|
257
|
+
"""Download attachments from a Gmail message."""
|
|
258
|
+
from oto.tools.google.gmail.lib.gmail_client import GmailClient
|
|
259
|
+
import json
|
|
260
|
+
|
|
261
|
+
client = GmailClient(account=account)
|
|
262
|
+
files = client.download_attachments(message_id, output)
|
|
263
|
+
print(json.dumps({"count": len(files), "files": files}, indent=2, ensure_ascii=False))
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@app.command("gmail-draft")
|
|
267
|
+
def gmail_draft(
|
|
268
|
+
to: Optional[str] = typer.Option(None, help="Recipient email (auto-detected with --reply-to)"),
|
|
269
|
+
subject: Optional[str] = typer.Option(None, help="Email subject (auto-detected with --reply-to)"),
|
|
270
|
+
body: str = typer.Option(..., help="Email body (plain text)"),
|
|
271
|
+
html: Optional[str] = typer.Option(None, help="Email body (HTML). If provided, sent as multipart alternative with plain text."),
|
|
272
|
+
cc: Optional[str] = typer.Option(None, help="CC recipients"),
|
|
273
|
+
bcc: Optional[str] = typer.Option(None, help="BCC recipients"),
|
|
274
|
+
reply_to: Optional[str] = typer.Option(None, "--reply-to", "-r", help="Message ID to reply to (threads the draft)"),
|
|
275
|
+
attach: Optional[list[str]] = typer.Option(None, "--attach", "-f", help="File paths to attach"),
|
|
276
|
+
sign: bool = typer.Option(True, "--sign/--no-sign", help="Append Gmail signature"),
|
|
277
|
+
account: Optional[str] = typer.Option(None, "--account", "-a", help="Google account name"),
|
|
278
|
+
):
|
|
279
|
+
"""Create a draft email in Gmail. Use --reply-to for threaded replies."""
|
|
280
|
+
from oto.tools.google.gmail.lib.gmail_client import GmailClient
|
|
281
|
+
import json
|
|
282
|
+
|
|
283
|
+
client = GmailClient(account=account)
|
|
284
|
+
final_html = _apply_signature(client, body, html) if sign else html
|
|
285
|
+
if reply_to:
|
|
286
|
+
result = client.create_draft_reply(message_id=reply_to, body=body, html=final_html, cc=cc, attachments=attach)
|
|
287
|
+
else:
|
|
288
|
+
if not to or not subject:
|
|
289
|
+
raise typer.BadParameter("--to and --subject are required (unless using --reply-to)")
|
|
290
|
+
result = client.create_draft(to=to, subject=subject, body=body, html=final_html, cc=cc, bcc=bcc, attachments=attach)
|
|
291
|
+
print(json.dumps(result, indent=2))
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@app.command("gmail-reply")
|
|
295
|
+
def gmail_reply(
|
|
296
|
+
message_id: str = typer.Argument(..., help="Gmail message ID to reply to"),
|
|
297
|
+
body: str = typer.Option(..., help="Reply body (plain text)"),
|
|
298
|
+
html: Optional[str] = typer.Option(None, help="Reply body (HTML)"),
|
|
299
|
+
cc: Optional[str] = typer.Option(None, help="CC recipients"),
|
|
300
|
+
attach: Optional[list[str]] = typer.Option(None, "--attach", "-f", help="File paths to attach"),
|
|
301
|
+
sign: bool = typer.Option(True, "--sign/--no-sign", help="Append Gmail signature"),
|
|
302
|
+
account: Optional[str] = typer.Option(None, "--account", "-a", help="Google account name"),
|
|
303
|
+
):
|
|
304
|
+
"""Reply to a Gmail message (preserves thread)."""
|
|
305
|
+
from oto.tools.google.gmail.lib.gmail_client import GmailClient
|
|
306
|
+
import json
|
|
307
|
+
|
|
308
|
+
client = GmailClient(account=account)
|
|
309
|
+
final_html = _apply_signature(client, body, html) if sign else html
|
|
310
|
+
result = client.reply(message_id=message_id, body=body, html=final_html, cc=cc, attachments=attach)
|
|
311
|
+
print(json.dumps(result, indent=2))
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@app.command("gmail-send")
|
|
315
|
+
def gmail_send(
|
|
316
|
+
to: str = typer.Option(..., help="Recipient email"),
|
|
317
|
+
subject: str = typer.Option(..., help="Email subject"),
|
|
318
|
+
body: str = typer.Option(..., help="Email body (plain text)"),
|
|
319
|
+
html: Optional[str] = typer.Option(None, help="Email body (HTML)"),
|
|
320
|
+
cc: Optional[str] = typer.Option(None, help="CC recipients"),
|
|
321
|
+
bcc: Optional[str] = typer.Option(None, help="BCC recipients"),
|
|
322
|
+
attach: Optional[list[str]] = typer.Option(None, "--attach", "-f", help="File paths to attach"),
|
|
323
|
+
sign: bool = typer.Option(True, "--sign/--no-sign", help="Append Gmail signature"),
|
|
324
|
+
account: Optional[str] = typer.Option(None, "--account", "-a", help="Google account name"),
|
|
325
|
+
):
|
|
326
|
+
"""Send an email via Gmail."""
|
|
327
|
+
from oto.tools.google.gmail.lib.gmail_client import GmailClient
|
|
328
|
+
import json
|
|
329
|
+
|
|
330
|
+
client = GmailClient(account=account)
|
|
331
|
+
final_html = _apply_signature(client, body, html) if sign else html
|
|
332
|
+
result = client.send(to=to, subject=subject, body=body, html=final_html, cc=cc, bcc=bcc, attachments=attach)
|
|
333
|
+
print(json.dumps(result, indent=2))
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@app.command("gmail-archive")
|
|
337
|
+
def gmail_archive(
|
|
338
|
+
message_ids: Optional[list[str]] = typer.Argument(None, help="Gmail message IDs to archive"),
|
|
339
|
+
query: Optional[str] = typer.Option(None, "--query", "-q", help="Archive all messages matching this Gmail query"),
|
|
340
|
+
account: Optional[str] = typer.Option(None, "--account", "-a", help="Google account name"),
|
|
341
|
+
):
|
|
342
|
+
"""Archive Gmail messages (remove from inbox)."""
|
|
343
|
+
from oto.tools.google.gmail.lib.gmail_client import GmailClient
|
|
344
|
+
import json
|
|
345
|
+
|
|
346
|
+
client = GmailClient(account=account)
|
|
347
|
+
ids = list(message_ids or [])
|
|
348
|
+
if query:
|
|
349
|
+
msgs = client.search(query=query, max_results=100)
|
|
350
|
+
ids.extend(m['id'] for m in msgs if 'INBOX' in m.get('labelIds', []))
|
|
351
|
+
if not ids:
|
|
352
|
+
print(json.dumps({"archived": 0, "message": "No messages to archive"}))
|
|
353
|
+
return
|
|
354
|
+
results = client.archive_messages(ids)
|
|
355
|
+
print(json.dumps({"archived": len(results), "results": results}, indent=2))
|
oto/commands/notion.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Notion commands (search, page, database)."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
app = typer.Typer(help="Notion tools")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@app.command("search")
|
|
10
|
+
def search(
|
|
11
|
+
query: str = typer.Argument(..., help="Search query"),
|
|
12
|
+
filter_type: Optional[str] = typer.Option(None, help="Filter by type (page, database)"),
|
|
13
|
+
):
|
|
14
|
+
"""Search Notion workspace."""
|
|
15
|
+
from oto.tools.notion.lib.notion_client import NotionClient
|
|
16
|
+
import json
|
|
17
|
+
|
|
18
|
+
client = NotionClient()
|
|
19
|
+
results = client.search(query, filter_type=filter_type)
|
|
20
|
+
print(json.dumps(results, indent=2, ensure_ascii=False))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@app.command("page")
|
|
24
|
+
def page(
|
|
25
|
+
page_id: str = typer.Argument(..., help="Notion page ID"),
|
|
26
|
+
blocks: bool = typer.Option(False, "--blocks", "-b", help="Include page blocks/content"),
|
|
27
|
+
):
|
|
28
|
+
"""Get a Notion page."""
|
|
29
|
+
from oto.tools.notion.lib.notion_client import NotionClient
|
|
30
|
+
import json
|
|
31
|
+
|
|
32
|
+
client = NotionClient()
|
|
33
|
+
page = client.get_page(page_id)
|
|
34
|
+
|
|
35
|
+
if blocks:
|
|
36
|
+
page["blocks"] = client.get_page_blocks(page_id, recursive=True)
|
|
37
|
+
|
|
38
|
+
print(json.dumps(page, indent=2, ensure_ascii=False))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.command("database")
|
|
42
|
+
def database(
|
|
43
|
+
database_id: str = typer.Argument(..., help="Notion database ID"),
|
|
44
|
+
query: bool = typer.Option(False, "--query", "-q", help="Query database entries"),
|
|
45
|
+
limit: int = typer.Option(100, help="Max results when querying"),
|
|
46
|
+
):
|
|
47
|
+
"""Get a Notion database schema or query its entries."""
|
|
48
|
+
from oto.tools.notion.lib.notion_client import NotionClient
|
|
49
|
+
import json
|
|
50
|
+
|
|
51
|
+
client = NotionClient()
|
|
52
|
+
|
|
53
|
+
if query:
|
|
54
|
+
results = client.query_database(database_id, page_size=limit)
|
|
55
|
+
print(json.dumps(results, indent=2, ensure_ascii=False))
|
|
56
|
+
else:
|
|
57
|
+
db = client.get_database(database_id)
|
|
58
|
+
print(json.dumps(db, indent=2, ensure_ascii=False))
|