Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions .agents/skills/dapier-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,38 +77,38 @@ uv run dapier runs events list --limit 10
# Gmail over a stored Google connection (token stays server-side)
uv run dapier gmail accounts # Google accounts: email + connection id
uv run dapier gmail search "from:boss is:unread" --account me@gmail.com
uv run dapier gmail read MSG_ID --save-attachments ./att
uv run dapier gmail read MSG_ID --save-attachments ./att --account me@gmail.com
uv run dapier gmail send --to a@b.c --subject Hi --file body.txt --attach r.pdf --account me@gmail.com
uv run dapier gmail reply MSG_ID --draft --text "On it" # reply draft, thread preserved
uv run dapier gmail reply MSG_ID --all --text "Thanks all" # direct reply-all send
uv run dapier gmail draft create --to a@b.c --subject S --text ... --attach inv.pdf
uv run dapier gmail draft send DRAFT_ID
uv run dapier gmail mark MSG_ID --read --archive
uv run dapier gmail reply MSG_ID --draft --text "On it" --account me@gmail.com # reply draft, thread preserved
uv run dapier gmail reply MSG_ID --all --text "Thanks all" --account me@gmail.com # direct reply-all send
uv run dapier gmail draft create --to a@b.c --subject S --text ... --attach inv.pdf --account me@gmail.com
uv run dapier gmail draft send DRAFT_ID --account me@gmail.com
uv run dapier gmail mark MSG_ID --read --archive --account me@gmail.com

# Google Drive over a stored Google connection
uv run dapier gdrive accounts
uv run dapier gdrive list --account me@gmail.com --max 20
uv run dapier gdrive download FILE_ID --out ./out.pdf # native docs export (PDF/CSV/PNG)
uv run dapier gdrive upload ./f.txt --name f.txt --folder FOL_ID
uv run dapier gdrive share FILE_ID --role writer --with user --email p@x.y
uv run dapier gdrive trash FILE_ID # --permanent destroys
uv run dapier gdrive restore FILE_ID
uv run dapier gdrive download FILE_ID --out ./out.pdf --account me@gmail.com # native docs export (PDF/CSV/PNG)
uv run dapier gdrive upload ./f.txt --name f.txt --folder FOL_ID --account me@gmail.com
uv run dapier gdrive share FILE_ID --role writer --with user --email p@x.y --account me@gmail.com
uv run dapier gdrive trash FILE_ID --account me@gmail.com # --permanent destroys
uv run dapier gdrive restore FILE_ID --account me@gmail.com

# Google Calendar (guest email only ever via --send-updates; default none)
uv run dapier calendar calendars --account me@gmail.com
uv run dapier calendar events --time-min 2026-11-04T00:00:00Z --time-max 2026-11-11T00:00:00Z
uv run dapier calendar freebusy --calendar primary --time-min ... --time-max ...
uv run dapier calendar events --time-min 2026-11-04T00:00:00Z --time-max 2026-11-11T00:00:00Z --account me@gmail.com
uv run dapier calendar freebusy --calendar primary --time-min ... --time-max ... --account me@gmail.com
uv run dapier calendar create --summary Sync --start 2026-11-05T09:00:00Z \
--end 2026-11-05T10:00:00Z --attendee peer@example.com --preview # review first
uv run dapier calendar create ... --send-updates all # actually invite
uv run dapier calendar update EVENT_ID --summary Renamed --calendar primary
uv run dapier calendar create ... --send-updates all --account me@gmail.com # actually invite
uv run dapier calendar update EVENT_ID --summary Renamed --calendar primary --account me@gmail.com
```

With several Google accounts connected, `--account` (verified email or
connection id) is required on `gmail`/`gdrive`/`calendar` operations;
`--account` (verified email or connection id) is required even with one
connected account on `gmail`/`gdrive`/`calendar` operations;
ambiguous or missing selections are rejected with the candidate list.
Insufficient-scope operations fail with the provider's 403 plus a
`dapier connections edit --scopes` hint. Write verbs accept `--preview`
`dapier connections edit --scopes` hint. Composition, selected Drive writes, and Calendar writes accept `--preview`
to print the exact request body without sending; calendar event windows
are bounded (≤ 366 days) and guests are only emailed via `--send-updates`.

Expand Down
338 changes: 141 additions & 197 deletions .agents/skills/google-data-pull/SKILL.md

Large diffs are not rendered by default.

18 changes: 9 additions & 9 deletions dapier_cli/cli/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@
EPILOG = """examples:
dapier calendar accounts # list Google accounts serving Calendar
dapier calendar calendars --account me@gmail.com
dapier calendar events --time-min 2026-11-04T00:00:00Z --time-max 2026-11-11T00:00:00Z
dapier calendar events --time-min 2026-11-04T00:00:00Z --time-max 2026-11-11T00:00:00Z --account me@gmail.com
dapier calendar freebusy --calendar primary --calendar work@group.calendar.google.com \\
--time-min 2026-11-04T00:00:00Z --time-max 2026-11-05T00:00:00Z
dapier calendar create --summary "Sync" --start 2026-11-05T09:00:00Z \\
--end 2026-11-05T10:00:00Z --attendee peer@example.com
dapier calendar create ... --send-updates all # default none: no guest email
dapier calendar create ... --preview # review the JSON, then run without --preview
dapier calendar delete EVENT_ID
--account takes the verified email or the connection id; when several Google
accounts serve Calendar it is required. Event windows are bounded
dapier calendar create ... --send-updates all --account me@gmail.com # default none: no guest email
dapier calendar create ... --preview --account me@gmail.com # review the JSON, then run without --preview
dapier calendar delete EVENT_ID --account me@gmail.com
--account is required on every operation except accounts; use the verified
email or connection id. Event windows are bounded
(<= 366 days). Writes never invite guests unless --send-updates says so;
--preview prints the exact request body without sending anything."""

Expand All @@ -28,7 +28,7 @@

def _write_args(p):
p.add_argument("--calendar", default=None, help="Calendar id (default primary)")
p.add_argument("--account")
p.add_argument("--account", required=True)
p.add_argument("--summary", default=None)
p.add_argument("--start", default=None, help="ISO 8601 start (or date for all-day)")
p.add_argument("--end", default=None, help="ISO 8601 end (or date for all-day)")
Expand Down Expand Up @@ -66,7 +66,7 @@ def register(sub):
acc.add_argument("--json", action="store_true")

def base_args(x):
x.add_argument("--account")
x.add_argument("--account", required=True)

cals = sp.add_parser("calendars", help="List the account's calendars")
base_args(cals)
Expand Down Expand Up @@ -108,7 +108,7 @@ def base_args(x):
delete = sp.add_parser("delete", help="Delete an event (guests notified per --send-updates)")
delete.add_argument("event_id")
delete.add_argument("--calendar", default=None)
delete.add_argument("--account")
delete.add_argument("--account", required=True)
delete.add_argument("--send-updates", default="none",
choices=["all", "externalOnly", "none"])
delete.add_argument("--json", action="store_true")
Expand Down
18 changes: 9 additions & 9 deletions dapier_cli/cli/gdrive.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
EPILOG = """examples:
dapier gdrive accounts # list Google accounts serving Drive
dapier gdrive list --account me@gmail.com --max 20
dapier gdrive search "name contains 'invoice'"
dapier gdrive download FILE_ID --out ./report.pdf
dapier gdrive upload ./notes.txt --name notes.txt --folder FOLDER_ID
dapier gdrive share FILE_ID --role reader --with user --email peer@example.com
dapier gdrive trash FILE_ID # recoverable; --permanent destroys
--account takes the verified email or the connection id; when several Google
accounts serve Drive it is required, and an ambiguous fragment is rejected.
dapier gdrive search "name contains 'invoice'" --account me@gmail.com
dapier gdrive download FILE_ID --out ./report.pdf --account me@gmail.com
dapier gdrive upload ./notes.txt --name notes.txt --folder FOLDER_ID --account me@gmail.com
dapier gdrive share FILE_ID --role reader --with user --email peer@example.com --account me@gmail.com
dapier gdrive trash FILE_ID --account me@gmail.com # recoverable; --permanent destroys
--account is required on every operation except accounts; use the verified
email or connection id. Ambiguous fragments are rejected.
Google-native docs export on download (doc/slide → PDF, sheet → CSV,
drawing → PNG; override with --export-mime)."""

Expand All @@ -40,7 +40,7 @@ def register(sub):
acc.add_argument("--json", action="store_true")

def account_arg(x):
x.add_argument("--account")
x.add_argument("--account", required=True)

listing = sp.add_parser("list", help="List files, newest first")
listing.add_argument("--query", default=None, help="Extra Drive query, e.g. name contains 'x'")
Expand Down Expand Up @@ -188,5 +188,5 @@ def run(args, api_url, debug, child=None):
as_json=as_json, debug=debug)
if args.command == "mkdir":
return c.gdrive_mkdir(api_url, args.name, account=account, parent=args.parent,
as_json=as_json, debug=debug)
as_json=as_json, preview=args.preview, debug=debug)
return 2
63 changes: 35 additions & 28 deletions dapier_cli/cli/gmail.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@
EPILOG = """examples:
dapier gmail accounts # list Google accounts (email + connection id)
dapier gmail search "from:boss is:unread" --account me@gmail.com
dapier gmail read MSG_ID --save-attachments ./att
dapier gmail send --to a@b.c --subject "Hi" --file body.txt --attach report.pdf
dapier gmail reply MSG_ID --text "On it" # direct send in the original thread
dapier gmail reply MSG_ID --draft # park it as an editable reply draft
dapier gmail draft create --to a@b.c --subject "Invoice" --text "..." --attach inv.pdf
dapier gmail draft send DRAFT_ID
--account takes the verified email or the connection id; when several Google
accounts serve Gmail it is required, and an ambiguous fragment is rejected."""
dapier gmail read MSG_ID --save-attachments ./att --account me@gmail.com
dapier gmail send --to a@b.c --subject "Hi" --file body.txt --attach report.pdf --account me@gmail.com
dapier gmail reply MSG_ID --text "On it" --account me@gmail.com # direct send in the original thread
dapier gmail reply MSG_ID --draft --account me@gmail.com # park it as an editable reply draft
dapier gmail draft create --to a@b.c --subject "Invoice" --text "..." --attach inv.pdf --account me@gmail.com
dapier gmail draft send DRAFT_ID --account me@gmail.com
--account is required on every operation except accounts; use the verified
email or connection id. Ambiguous fragments are rejected."""

COMMANDS = ("accounts", "search", "list", "read", "attachment", "threads",
"thread", "labels", "mark", "send", "reply", "draft")
Expand Down Expand Up @@ -63,7 +63,7 @@ def register(sub):

read = sub_p.add_parser("read", help="Read one message (headers, body, attachments)")
read.add_argument("message_id")
read.add_argument("--account")
read.add_argument("--account", required=True)
read.add_argument("--html", action="store_true", help="Include the HTML part")
read.add_argument("--json", action="store_true")
read.add_argument("--save-attachments", default=None, metavar="DIR",
Expand All @@ -72,7 +72,7 @@ def register(sub):
att = sub_p.add_parser("attachment", help="Save one attachment by id")
att.add_argument("message_id")
att.add_argument("attachment_id")
att.add_argument("--account")
att.add_argument("--account", required=True)
att.add_argument("--out", default=None, help="Output path (default: the filename)")

threads = sub_p.add_parser("threads", help="List threads (optionally searched)")
Expand All @@ -81,20 +81,20 @@ def register(sub):

thread = sub_p.add_parser("thread", help="Read one thread with all messages")
thread.add_argument("thread_id")
thread.add_argument("--account")
thread.add_argument("--account", required=True)
thread.add_argument("--json", action="store_true")

labels = sub_p.add_parser("labels", help="List the account's labels")
labels.add_argument("--account")
labels.add_argument("--account", required=True)
labels.add_argument("--json", action="store_true")

mark = sub_p.add_parser("mark", help="Mark read/unread, archive, trash, label")
mark.add_argument("message_id")
mark.add_argument("--account")
mark.add_argument("--account", required=True)
mode = mark.add_mutually_exclusive_group()
mode.add_argument("--read", action="store_true")
mode.add_argument("--unread", action="store_true")
mode.add_argument("--archive", action="store_true")
mark.add_argument("--archive", action="store_true")
mode.add_argument("--trash", action="store_true")
mode.add_argument("--untrash", action="store_true")
mark.add_argument("--add-label", action="append", default=[])
Expand All @@ -107,7 +107,10 @@ def register(sub):

reply = sub_p.add_parser("reply", help="Reply to a message in its thread (send or --draft)")
reply.add_argument("message_id")
reply.add_argument("--account")
reply.add_argument("--account", required=True)
reply.add_argument("--to", help="Explicit recipients instead of the inferred reply recipients")
reply.add_argument("--cc", help="Explicit Cc recipients")
reply.add_argument("--bcc", help="Explicit Bcc recipients")
reply.add_argument("--all", dest="reply_all", action="store_true",
help="Reply to all original recipients")
reply.add_argument("--draft", action="store_true",
Expand All @@ -123,38 +126,41 @@ def register(sub):

dupdate = draft_sp.add_parser("update", help="Replace a draft's content")
dupdate.add_argument("draft_id")
_recipient_args(dupdate)
_recipient_args(dupdate, required=False)
_common_message(dupdate)

dlist = draft_sp.add_parser("list", help="List drafts")
dlist.add_argument("--account")
dlist.add_argument("--account", required=True)
dlist.add_argument("--json", action="store_true")
dlist.add_argument("--max", type=int, default=25)
dlist.add_argument("--page-token")

dget = draft_sp.add_parser("get", help="Show one draft")
dget.add_argument("draft_id")
dget.add_argument("--account")
dget.add_argument("--account", required=True)
dget.add_argument("--json", action="store_true")

ddelete = draft_sp.add_parser("delete", help="Delete a draft")
ddelete.add_argument("draft_id")
ddelete.add_argument("--account")
ddelete.add_argument("--account", required=True)

dsend = draft_sp.add_parser("send", help="Send an existing draft")
dsend.add_argument("draft_id")
dsend.add_argument("--account")
dsend.add_argument("--account", required=True)
dsend.add_argument("--json", action="store_true")


def _list_args(p):
p.add_argument("--account")
p.add_argument("--account", required=True)
p.add_argument("--max", type=int, default=25)
p.add_argument("--json", action="store_true")
p.add_argument("--label", default=None, help="Filter by label id, e.g. INBOX")
p.add_argument("--page-token", default=None)


def _recipient_args(p):
p.add_argument("--account")
p.add_argument("--to", required=True)
def _recipient_args(p, required=True):
p.add_argument("--account", required=True)
p.add_argument("--to", required=required)
p.add_argument("--cc", default=None)
p.add_argument("--bcc", default=None)
p.add_argument("--subject", default=None)
Expand All @@ -167,10 +173,10 @@ def run(args, api_url, debug, child=None):
return c.gmail_accounts(api_url, as_json=args.json, debug=debug)
if args.command == "search":
return c.gmail_search(api_url, args.query, account=account, max_results=args.max,
label=args.label, page_token=args.page_token, debug=debug)
label=args.label, page_token=args.page_token, as_json=args.json, debug=debug)
if args.command == "list":
return c.gmail_list(api_url, account=account, max_results=args.max,
label=args.label, page_token=args.page_token, debug=debug)
label=args.label, page_token=args.page_token, as_json=args.json, debug=debug)
if args.command == "read":
return c.gmail_read(api_url, args.message_id, account=account, include_html=args.html,
as_json=args.json, save_attachments=args.save_attachments, debug=debug)
Expand Down Expand Up @@ -202,7 +208,7 @@ def run(args, api_url, debug, child=None):
if args.command == "reply":
return c.gmail_reply(api_url, args.message_id, account=account, reply_all=args.reply_all,
text=args.text, file=args.file, attach=args.attach,
draft=args.draft, as_json=args.json,
draft=args.draft, to=args.to, cc=args.cc, bcc=args.bcc, as_json=args.json,
preview=getattr(args, "preview", False), debug=debug)
if args.command == "draft":
return _run_draft(args, api_url, account, debug)
Expand All @@ -213,7 +219,8 @@ def _run_draft(args, api_url, account, debug):
c = commands
name = args.draft_command
if name == "list":
return c.gmail_draft_list(api_url, account=account, as_json=args.json, debug=debug)
return c.gmail_draft_list(api_url, account=account, as_json=args.json, debug=debug,
max_results=args.max, page_token=args.page_token)
if name == "get":
return c.gmail_draft_get(api_url, args.draft_id, account=account, as_json=args.json, debug=debug)
if name == "delete":
Expand Down
5 changes: 3 additions & 2 deletions dapier_cli/commands/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def _account_params(account, extra=None):
params = dict(extra or {})
if account:
params["account"] = account
return "?" + urlencode(params) if params else {}
return "?" + urlencode(params) if params else ""


def calendar_accounts(api_url, as_json=False, debug=False):
Expand Down Expand Up @@ -143,7 +143,8 @@ def calendar_freebusy(api_url, account=None, calendar_ids=None, time_min=None,


def _write_body(account, calendar, send_updates, fields):
body = {key: value for key, value in (fields or {}).items() if value}
body = {({"timezone_name": "timezone", "payload": "event"}.get(key, key)): value
for key, value in (fields or {}).items() if value is not None and value != []}
if account:
body["account"] = account
if calendar:
Expand Down
Loading
Loading