@acedatacloud/skills 2026.504.4 → 2026.505.0
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.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@acedatacloud/skills",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.505.0",
|
|
4
4
|
"description": "Agent Skills for AceDataCloud AI services — music, image, video generation, LLM chat, web search. Compatible with Claude Code, GitHub Copilot, Gemini CLI, OpenAI Codex, and 30+ AI coding agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-skills",
|
|
@@ -1,38 +1,114 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: google-calendar
|
|
3
|
-
description: Read Google Calendar events / agenda / free-busy / invitations via the Calendar v3 REST API. Use when the user mentions Google Calendar events, today's agenda, this week's meetings, finding conflicts, listing invitations,
|
|
3
|
+
description: Read and manage Google Calendar events / agenda / free-busy / invitations via the Calendar v3 REST API. Use when the user mentions Google Calendar events, today's agenda, this week's meetings, finding conflicts, listing invitations, checking free time, or scheduling / rescheduling / cancelling a meeting.
|
|
4
4
|
when_to_use: |
|
|
5
|
-
Trigger when the user wants to read events
|
|
6
|
-
Calendar — list / search / inspect events, build today's
|
|
7
|
-
week's agenda, check free / busy windows,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
Trigger when the user wants to read or manage events on their
|
|
6
|
+
Google Calendar — list / search / inspect events, build today's
|
|
7
|
+
or this week's agenda, check free / busy windows, pull invite
|
|
8
|
+
details, or have the AI create / update / cancel events on their
|
|
9
|
+
behalf and email invites to attendees. The installed connector
|
|
10
|
+
always grants `calendar.readonly`; the user opts in to the
|
|
11
|
+
broader `calendar` scope (full read + write) at install — confirm
|
|
12
|
+
before destructive writes.
|
|
11
13
|
connections: [google/calendar]
|
|
12
14
|
allowed_tools: [Bash]
|
|
13
15
|
license: Apache-2.0
|
|
14
16
|
metadata:
|
|
15
17
|
author: acedatacloud
|
|
16
|
-
version: "1.
|
|
18
|
+
version: "1.2"
|
|
17
19
|
---
|
|
18
20
|
|
|
19
21
|
Drive Google Calendar via `curl + jq`. The user's OAuth bearer token
|
|
20
22
|
is in `$GOOGLE_CALENDAR_TOKEN`; every call needs it as
|
|
21
|
-
`Authorization: Bearer $GOOGLE_CALENDAR_TOKEN`.
|
|
22
|
-
carries
|
|
23
|
-
|
|
23
|
+
`Authorization: Bearer $GOOGLE_CALENDAR_TOKEN`. At minimum the token
|
|
24
|
+
carries `calendar.readonly` plus the identity scopes
|
|
25
|
+
(`openid email profile`); if the user opted in to write at install
|
|
26
|
+
time it also carries the broader `calendar` scope (read + write).
|
|
24
27
|
|
|
25
28
|
The Calendar API returns standard JSON; failures surface as
|
|
26
29
|
`{"error": {"code": 401|403|..., "message": "..."}}` — show that
|
|
27
30
|
error verbatim. `401` means the token expired (re-install). `403
|
|
28
|
-
insufficientPermissions` means the user
|
|
29
|
-
|
|
31
|
+
insufficientPermissions` on a write means the user only granted
|
|
32
|
+
`calendar.readonly` — ask them to re-install the connector with the
|
|
33
|
+
read+write box checked.
|
|
30
34
|
|
|
31
35
|
**Always start with `users/me/calendarList`** to learn which calendars
|
|
32
36
|
the account can see (the user's primary plus any subscribed / shared
|
|
33
37
|
ones), AND with `users/me/settings/timezone` so you render times in
|
|
34
38
|
the user's local zone instead of UTC.
|
|
35
39
|
|
|
40
|
+
**Before any destructive write** (creating, moving, or cancelling an
|
|
41
|
+
event that has attendees) show the exact event details and ask the
|
|
42
|
+
user to confirm. When attendees are involved, also confirm whether
|
|
43
|
+
they want Google to email the attendees — that's controlled by the
|
|
44
|
+
`sendUpdates` query parameter.
|
|
45
|
+
|
|
46
|
+
## Optional: Google Workspace CLI (`gws`) for agenda + create
|
|
47
|
+
|
|
48
|
+
[`gws`](https://github.com/googleworkspace/cli) is Google's official CLI
|
|
49
|
+
(not officially supported — community-maintained on the `googleworkspace`
|
|
50
|
+
org). It dynamically builds its command surface from Google's Discovery
|
|
51
|
+
Document, exits non-zero on API errors, and ships hand-crafted helper
|
|
52
|
+
commands (prefixed `+`) for time-aware workflows.
|
|
53
|
+
|
|
54
|
+
**Use `gws` for two specific cases:**
|
|
55
|
+
|
|
56
|
+
- `+agenda` reads the user's account timezone from `Settings.timezone`
|
|
57
|
+
(cached for 24 h) and renders today's events in that zone, so you don't
|
|
58
|
+
have to fetch the timezone yourself before formatting times.
|
|
59
|
+
- `+insert` shapes the create-event JSON for you (attendees, sendUpdates,
|
|
60
|
+
reminders) so a one-line invocation produces a well-formed request.
|
|
61
|
+
|
|
62
|
+
For everything else (events.list / patch / move / delete, freebusy,
|
|
63
|
+
calendarList) the curl recipes below are equivalent and shorter — stay
|
|
64
|
+
on those.
|
|
65
|
+
|
|
66
|
+
### Install
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
npm install -g @googleworkspace/cli # or: brew install googleworkspace-cli
|
|
70
|
+
# Pre-built binaries also at https://github.com/googleworkspace/cli/releases
|
|
71
|
+
gws --version
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Auth
|
|
75
|
+
|
|
76
|
+
`gws` reads its OAuth bearer token from the `GOOGLE_WORKSPACE_CLI_TOKEN`
|
|
77
|
+
environment variable. The Calendar token used in this skill is in
|
|
78
|
+
`$GOOGLE_CALENDAR_TOKEN`, so re-export it once at the top of every shell
|
|
79
|
+
block that calls `gws`:
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
export GOOGLE_WORKSPACE_CLI_TOKEN="$GOOGLE_CALENDAR_TOKEN"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Agenda + create
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
# Today on the primary calendar, in the account's own timezone
|
|
89
|
+
gws calendar +agenda
|
|
90
|
+
|
|
91
|
+
# Today / week, with explicit overrides
|
|
92
|
+
gws calendar +agenda --today --tz America/New_York
|
|
93
|
+
gws calendar +agenda --range week
|
|
94
|
+
|
|
95
|
+
# Create an event (auto-shapes attendees + sendUpdates JSON)
|
|
96
|
+
gws calendar +insert --calendar primary \
|
|
97
|
+
--json '{
|
|
98
|
+
"summary":"Standup",
|
|
99
|
+
"start":{"dateTime":"2026-05-06T10:00:00-04:00"},
|
|
100
|
+
"end": {"dateTime":"2026-05-06T10:30:00-04:00"},
|
|
101
|
+
"attendees":[{"email":"alice@example.com"}]
|
|
102
|
+
}' \
|
|
103
|
+
--params '{"sendUpdates":"all"}'
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Both helpers exit non-zero with a structured JSON error on stderr if
|
|
107
|
+
Google rejects the request — surface that verbatim. `+insert` against
|
|
108
|
+
attendees requires the broader `calendar` scope; on `403
|
|
109
|
+
insufficientPermissions` ask the user to re-install with read+write
|
|
110
|
+
checked.
|
|
111
|
+
|
|
36
112
|
## Recipes
|
|
37
113
|
|
|
38
114
|
### Verify auth + discover calendars (always run first)
|
|
@@ -182,14 +258,150 @@ while : ; do
|
|
|
182
258
|
done
|
|
183
259
|
```
|
|
184
260
|
|
|
261
|
+
## Write recipes
|
|
262
|
+
|
|
263
|
+
These all need the broader `calendar` scope. If the user only granted
|
|
264
|
+
`calendar.readonly` you'll get `403 insufficientPermissions` —
|
|
265
|
+
surface that and ask them to re-install with the read+write box
|
|
266
|
+
checked. **Always echo the event summary, time and attendee list
|
|
267
|
+
back to the user before creating or cancelling anything.**
|
|
268
|
+
|
|
269
|
+
### Create a single event (with optional attendees + Google Meet link)
|
|
270
|
+
|
|
271
|
+
```sh
|
|
272
|
+
TZ=$(curl -sS -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
|
|
273
|
+
"https://www.googleapis.com/calendar/v3/users/me/settings/timezone" | jq -r .value)
|
|
274
|
+
|
|
275
|
+
cat > /tmp/_cal_event.json <<JSON
|
|
276
|
+
{
|
|
277
|
+
"summary": "Sync — Q2 OKR review",
|
|
278
|
+
"location": "Online",
|
|
279
|
+
"description": "Drafted by AceDataCloud.",
|
|
280
|
+
"start": {"dateTime": "2026-05-12T10:00:00", "timeZone": "$TZ"},
|
|
281
|
+
"end": {"dateTime": "2026-05-12T10:30:00", "timeZone": "$TZ"},
|
|
282
|
+
"attendees": [
|
|
283
|
+
{"email": "alice@example.com"},
|
|
284
|
+
{"email": "bob@example.com"}
|
|
285
|
+
],
|
|
286
|
+
"reminders": {"useDefault": true},
|
|
287
|
+
"conferenceData": {
|
|
288
|
+
"createRequest": {
|
|
289
|
+
"requestId": "meet-$(date +%s)",
|
|
290
|
+
"conferenceSolutionKey": {"type": "hangoutsMeet"}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
JSON
|
|
295
|
+
|
|
296
|
+
# sendUpdates: 'all' = email all attendees; 'externalOnly' = only non-org; 'none' = silent
|
|
297
|
+
curl -sS -X POST -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
|
|
298
|
+
-H 'Content-Type: application/json' \
|
|
299
|
+
--data @/tmp/_cal_event.json \
|
|
300
|
+
"https://www.googleapis.com/calendar/v3/calendars/primary/events?conferenceDataVersion=1&sendUpdates=all" \
|
|
301
|
+
| jq '{id, htmlLink, hangoutLink, summary, start, end, attendees}'
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Drop the `conferenceData` block if the user didn't ask for a Meet
|
|
305
|
+
link — it'll fall back to a plain event.
|
|
306
|
+
|
|
307
|
+
### Create a recurring event
|
|
308
|
+
|
|
309
|
+
```sh
|
|
310
|
+
TZ=$(curl -sS -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
|
|
311
|
+
"https://www.googleapis.com/calendar/v3/users/me/settings/timezone" | jq -r .value)
|
|
312
|
+
cat > /tmp/_cal_recur.json <<JSON
|
|
313
|
+
{
|
|
314
|
+
"summary": "Weekly 1:1",
|
|
315
|
+
"start": {"dateTime": "2026-05-12T15:00:00", "timeZone": "$TZ"},
|
|
316
|
+
"end": {"dateTime": "2026-05-12T15:30:00", "timeZone": "$TZ"},
|
|
317
|
+
"recurrence": ["RRULE:FREQ=WEEKLY;BYDAY=TU;COUNT=12"]
|
|
318
|
+
}
|
|
319
|
+
JSON
|
|
320
|
+
curl -sS -X POST -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
|
|
321
|
+
-H 'Content-Type: application/json' \
|
|
322
|
+
--data @/tmp/_cal_recur.json \
|
|
323
|
+
"https://www.googleapis.com/calendar/v3/calendars/primary/events" \
|
|
324
|
+
| jq '{id, recurrence, summary}'
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
RRULE follows RFC 5545. Common patterns: `FREQ=DAILY`, `FREQ=WEEKLY;BYDAY=MO,WE,FR`,
|
|
328
|
+
`FREQ=MONTHLY;BYMONTHDAY=15`. Add `UNTIL=20261231T235959Z` or `COUNT=12`
|
|
329
|
+
for a hard stop.
|
|
330
|
+
|
|
331
|
+
### Update an existing event (PATCH — partial update)
|
|
332
|
+
|
|
333
|
+
```sh
|
|
334
|
+
EVENT_ID='abc123def4567890ghijklmnop'
|
|
335
|
+
curl -sS -X PATCH -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
|
|
336
|
+
-H 'Content-Type: application/json' \
|
|
337
|
+
--data '{"location":"Conference Room 4","description":"Now in-person."}' \
|
|
338
|
+
"https://www.googleapis.com/calendar/v3/calendars/primary/events/$EVENT_ID?sendUpdates=all" \
|
|
339
|
+
| jq '{id, summary, location, description}'
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
`PATCH` only changes the fields you send; `PUT` replaces the entire
|
|
343
|
+
event payload. Prefer `PATCH`.
|
|
344
|
+
|
|
345
|
+
### Reschedule an event
|
|
346
|
+
|
|
347
|
+
```sh
|
|
348
|
+
EVENT_ID='abc123def4567890ghijklmnop'
|
|
349
|
+
TZ=$(curl -sS -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
|
|
350
|
+
"https://www.googleapis.com/calendar/v3/users/me/settings/timezone" | jq -r .value)
|
|
351
|
+
cat > /tmp/_cal_resched.json <<JSON
|
|
352
|
+
{
|
|
353
|
+
"start": {"dateTime": "2026-05-12T14:00:00", "timeZone": "$TZ"},
|
|
354
|
+
"end": {"dateTime": "2026-05-12T14:30:00", "timeZone": "$TZ"}
|
|
355
|
+
}
|
|
356
|
+
JSON
|
|
357
|
+
curl -sS -X PATCH -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
|
|
358
|
+
-H 'Content-Type: application/json' \
|
|
359
|
+
--data @/tmp/_cal_resched.json \
|
|
360
|
+
"https://www.googleapis.com/calendar/v3/calendars/primary/events/$EVENT_ID?sendUpdates=all" \
|
|
361
|
+
| jq '{id, summary, start, end}'
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Add or change attendees
|
|
365
|
+
|
|
366
|
+
Google requires you to send the **complete** attendee list when
|
|
367
|
+
patching attendees — fetch the current list, mutate, send back:
|
|
368
|
+
|
|
369
|
+
```sh
|
|
370
|
+
EVENT_ID='abc123def4567890ghijklmnop'
|
|
371
|
+
CURRENT=$(curl -sS -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
|
|
372
|
+
"https://www.googleapis.com/calendar/v3/calendars/primary/events/$EVENT_ID?fields=attendees" \
|
|
373
|
+
| jq '.attendees // []')
|
|
374
|
+
NEW=$(echo "$CURRENT" | jq '. + [{"email":"carol@example.com"}]')
|
|
375
|
+
curl -sS -X PATCH -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
|
|
376
|
+
-H 'Content-Type: application/json' \
|
|
377
|
+
--data "{\"attendees\": $NEW}" \
|
|
378
|
+
"https://www.googleapis.com/calendar/v3/calendars/primary/events/$EVENT_ID?sendUpdates=all" \
|
|
379
|
+
| jq '{id, attendees}'
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### Cancel / delete an event
|
|
383
|
+
|
|
384
|
+
```sh
|
|
385
|
+
EVENT_ID='abc123def4567890ghijklmnop'
|
|
386
|
+
curl -sS -X DELETE -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
|
|
387
|
+
"https://www.googleapis.com/calendar/v3/calendars/primary/events/$EVENT_ID?sendUpdates=all" \
|
|
388
|
+
-o /dev/null -w 'HTTP %{http_code}\n'
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
`204` = success. To cancel one occurrence of a recurring event, fetch
|
|
392
|
+
the instance with `events.instances` first, then `DELETE` the
|
|
393
|
+
specific instance id (it has a longer `EVENT_ID_YYYYMMDDTHHMMSSZ`
|
|
394
|
+
shape).
|
|
395
|
+
|
|
185
396
|
## Common error codes
|
|
186
397
|
|
|
187
398
|
| HTTP | meaning | what to tell the user |
|
|
188
399
|
|---|---|---|
|
|
189
400
|
| `401 UNAUTHENTICATED` | token expired / revoked | "Reconnect the Google Calendar connector on the Connections page." |
|
|
190
|
-
| `403 insufficientPermissions` | scope missing | "This
|
|
401
|
+
| `403 insufficientPermissions` | write scope missing | "This action needs the Calendar read+write scope, but only `calendar.readonly` was granted. Re-install the connector with the read+write box checked." |
|
|
191
402
|
| `403 forbidden` | calendar id not visible to this account | check `calendarList` first; if it's a shared calendar, the owner needs to share it. |
|
|
192
403
|
| `404 notFound` | wrong event / calendar id | double-check the id and try `calendarList` to confirm the calendar exists. |
|
|
404
|
+
| `409 conflict` | recurring event id collision | append a UUID to your `requestId` and retry. |
|
|
193
405
|
| `429 quotaExceeded` | quota / throttling | back off ~5s, then retry once. |
|
|
194
406
|
|
|
195
407
|
Never log or echo `$GOOGLE_CALENDAR_TOKEN` — treat it as a secret.
|
|
@@ -1,36 +1,99 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: google-drive
|
|
3
|
-
description: Read and
|
|
3
|
+
description: Read, search, upload, rename, move and delete Google Drive files / folders / shared content via the Drive v3 REST API. Use when the user mentions Drive files, "my drive", shared documents, Google Docs / Sheets / Slides, exporting / downloading a Drive file, searching by name / owner / folder, uploading a new file, renaming or moving files, or organising folders.
|
|
4
4
|
when_to_use: |
|
|
5
|
-
Trigger when the user wants to list, search, read
|
|
6
|
-
in their Google Drive — including Google-native docs
|
|
7
|
-
Slides) which need a special "export" call to get
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
Trigger when the user wants to list, search, read, download or
|
|
6
|
+
modify files in their Google Drive — including Google-native docs
|
|
7
|
+
(Docs / Sheets / Slides) which need a special "export" call to get
|
|
8
|
+
plain content, as well as uploads, renames, folder moves, and
|
|
9
|
+
trashing files. The installed connector always grants `drive.readonly`;
|
|
10
|
+
the user opts in to the broader `drive` scope (full read + write)
|
|
11
|
+
at install time — confirm before performing destructive writes.
|
|
10
12
|
connections: [google/drive]
|
|
11
13
|
allowed_tools: [Bash]
|
|
12
14
|
license: Apache-2.0
|
|
13
15
|
metadata:
|
|
14
16
|
author: acedatacloud
|
|
15
|
-
version: "1.
|
|
17
|
+
version: "1.2"
|
|
16
18
|
---
|
|
17
19
|
|
|
18
20
|
Drive Google Drive via `curl + jq`. The user's OAuth bearer token is
|
|
19
21
|
in `$GOOGLE_DRIVE_TOKEN`; every call needs it as
|
|
20
|
-
`Authorization: Bearer $GOOGLE_DRIVE_TOKEN`.
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
`Authorization: Bearer $GOOGLE_DRIVE_TOKEN`. At minimum the token
|
|
23
|
+
carries `drive.readonly` plus the identity scopes
|
|
24
|
+
(`openid email profile`); if the user opted in to write at install
|
|
25
|
+
time it also carries the broader `drive` scope (full read + write).
|
|
23
26
|
|
|
24
27
|
The Drive API returns standard JSON; failures surface as
|
|
25
28
|
`{"error": {"code": 401|403|..., "message": "..."}}` — show that
|
|
26
29
|
error verbatim to the user. `401` means the token expired and the
|
|
27
30
|
user must re-install the connector. `403 insufficientPermissions`
|
|
28
|
-
means the
|
|
29
|
-
|
|
31
|
+
on a write means the user did not grant the `drive` scope at install
|
|
32
|
+
— ask them to re-install with the read+write box checked.
|
|
33
|
+
|
|
34
|
+
**Before any destructive write** (renaming, moving, trashing, or
|
|
35
|
+
bulk-mutating files) show the exact target list and ask the user to
|
|
36
|
+
confirm. Never trash by guessing an id — always echo back the file
|
|
37
|
+
name + path you're about to touch.
|
|
30
38
|
|
|
31
39
|
**Always start with `/about?fields=user`** to confirm the connection
|
|
32
40
|
works AND learn which Google account you're operating against.
|
|
33
41
|
|
|
42
|
+
## Optional: Google Workspace CLI (`gws`) for uploads
|
|
43
|
+
|
|
44
|
+
[`gws`](https://github.com/googleworkspace/cli) is Google's official CLI
|
|
45
|
+
(not officially supported — community-maintained on the `googleworkspace`
|
|
46
|
+
org). It dynamically builds its command surface from Google's Discovery
|
|
47
|
+
Document, exits non-zero on API errors, supports `--page-all`
|
|
48
|
+
auto-pagination, and ships a `+upload` helper that wraps the multipart
|
|
49
|
+
upload protocol.
|
|
50
|
+
|
|
51
|
+
**Use `gws` for uploads.** A Drive multipart upload requires a
|
|
52
|
+
hand-formatted `multipart/related` body with a JSON metadata part and a
|
|
53
|
+
binary file part separated by a boundary string — easy to get wrong from
|
|
54
|
+
curl. `gws drive +upload` does it correctly. **For everything else**
|
|
55
|
+
(list, search, get, export, rename, move, trash, delete) the curl recipes
|
|
56
|
+
below are equivalent and shorter — stay on those.
|
|
57
|
+
|
|
58
|
+
### Install
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
npm install -g @googleworkspace/cli # or: brew install googleworkspace-cli
|
|
62
|
+
# Pre-built binaries also at https://github.com/googleworkspace/cli/releases
|
|
63
|
+
gws --version
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Auth
|
|
67
|
+
|
|
68
|
+
`gws` reads its OAuth bearer token from the `GOOGLE_WORKSPACE_CLI_TOKEN`
|
|
69
|
+
environment variable. The Drive token used in this skill is in
|
|
70
|
+
`$GOOGLE_DRIVE_TOKEN`, so re-export it once at the top of every shell
|
|
71
|
+
block that calls `gws`:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
export GOOGLE_WORKSPACE_CLI_TOKEN="$GOOGLE_DRIVE_TOKEN"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Upload
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
# Simple upload to My Drive (auto-detects MIME type, sets the file name
|
|
81
|
+
# from --name; falls back to the local filename if --name is omitted)
|
|
82
|
+
gws drive +upload ./report.pdf --name "Q1 Report"
|
|
83
|
+
|
|
84
|
+
# Upload into a specific folder, or with explicit metadata, via the
|
|
85
|
+
# generic Discovery method + --upload (multipart wire format handled
|
|
86
|
+
# for you)
|
|
87
|
+
gws drive files create \
|
|
88
|
+
--json '{"name":"report.pdf","parents":["FOLDER_ID"],"description":"Q1"}' \
|
|
89
|
+
--upload ./report.pdf
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Both exit non-zero with a structured JSON error on stderr if Google
|
|
93
|
+
rejects the request — surface that verbatim. Uploads need the broader
|
|
94
|
+
`drive` scope; on `403 insufficientPermissions` ask the user to
|
|
95
|
+
re-install the connector with read+write checked.
|
|
96
|
+
|
|
34
97
|
## Recipes
|
|
35
98
|
|
|
36
99
|
### Verify auth (always run first)
|
|
@@ -188,14 +251,153 @@ while : ; do
|
|
|
188
251
|
done
|
|
189
252
|
```
|
|
190
253
|
|
|
254
|
+
## Write recipes
|
|
255
|
+
|
|
256
|
+
These all need the broader `drive` scope. If the user only granted
|
|
257
|
+
`drive.readonly` you'll get `403 insufficientPermissions` — surface
|
|
258
|
+
that and suggest re-installing with the read+write box checked.
|
|
259
|
+
**Always echo the target name + path back to the user before
|
|
260
|
+
trashing or bulk-moving anything.**
|
|
261
|
+
|
|
262
|
+
### Rename a file
|
|
263
|
+
|
|
264
|
+
```sh
|
|
265
|
+
FILE_ID='1A2B3CdEfGhIjKlMn'
|
|
266
|
+
NEW_NAME='2026 Q2 OKR (final).gdoc'
|
|
267
|
+
curl -sS -X PATCH -H "Authorization: Bearer $GOOGLE_DRIVE_TOKEN" \
|
|
268
|
+
-H 'Content-Type: application/json' \
|
|
269
|
+
--data "{\"name\":$(jq -nr --arg n "$NEW_NAME" '$n')}" \
|
|
270
|
+
"https://www.googleapis.com/drive/v3/files/$FILE_ID?fields=id,name"
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Move a file to a different folder
|
|
274
|
+
|
|
275
|
+
Drive's folder model is parent-id based. Move = remove old parent,
|
|
276
|
+
add new parent:
|
|
277
|
+
|
|
278
|
+
```sh
|
|
279
|
+
FILE_ID='1A2B3CdEfGhIjKlMn'
|
|
280
|
+
NEW_PARENT='1XYZnewFolderId'
|
|
281
|
+
|
|
282
|
+
# Read existing parents (so we can pass them in removeParents)
|
|
283
|
+
OLD_PARENTS=$(curl -sS -H "Authorization: Bearer $GOOGLE_DRIVE_TOKEN" \
|
|
284
|
+
"https://www.googleapis.com/drive/v3/files/$FILE_ID?fields=parents" \
|
|
285
|
+
| jq -r '.parents | join(",")')
|
|
286
|
+
|
|
287
|
+
curl -sS -X PATCH -H "Authorization: Bearer $GOOGLE_DRIVE_TOKEN" \
|
|
288
|
+
--data '' \
|
|
289
|
+
"https://www.googleapis.com/drive/v3/files/$FILE_ID?addParents=$NEW_PARENT&removeParents=$OLD_PARENTS&fields=id,name,parents"
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Create a new folder
|
|
293
|
+
|
|
294
|
+
```sh
|
|
295
|
+
PARENT_ID='1XYZparentFolderId' # or 'root' for My Drive root
|
|
296
|
+
curl -sS -X POST -H "Authorization: Bearer $GOOGLE_DRIVE_TOKEN" \
|
|
297
|
+
-H 'Content-Type: application/json' \
|
|
298
|
+
--data "{\"name\":\"Reports / 2026Q2\",\"mimeType\":\"application/vnd.google-apps.folder\",\"parents\":[\"$PARENT_ID\"]}" \
|
|
299
|
+
"https://www.googleapis.com/drive/v3/files?fields=id,name,webViewLink" \
|
|
300
|
+
| jq
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Upload a file (multipart so metadata + bytes go in one request)
|
|
304
|
+
|
|
305
|
+
```sh
|
|
306
|
+
LOCAL=/tmp/report.pdf
|
|
307
|
+
NAME='Q2 report.pdf'
|
|
308
|
+
PARENT_ID='1XYZparentFolderId'
|
|
309
|
+
MIME='application/pdf'
|
|
310
|
+
|
|
311
|
+
BOUNDARY='aceDataBoundary'
|
|
312
|
+
META=$(jq -nc --arg n "$NAME" --arg p "$PARENT_ID" '{name:$n, parents:[$p]}')
|
|
313
|
+
{
|
|
314
|
+
printf -- '--%s\r\n' "$BOUNDARY"
|
|
315
|
+
printf 'Content-Type: application/json; charset=UTF-8\r\n\r\n'
|
|
316
|
+
printf '%s\r\n' "$META"
|
|
317
|
+
printf -- '--%s\r\n' "$BOUNDARY"
|
|
318
|
+
printf 'Content-Type: %s\r\n\r\n' "$MIME"
|
|
319
|
+
cat "$LOCAL"
|
|
320
|
+
printf '\r\n--%s--\r\n' "$BOUNDARY"
|
|
321
|
+
} > /tmp/_drive_upload.bin
|
|
322
|
+
|
|
323
|
+
curl -sS -X POST -H "Authorization: Bearer $GOOGLE_DRIVE_TOKEN" \
|
|
324
|
+
-H "Content-Type: multipart/related; boundary=$BOUNDARY" \
|
|
325
|
+
--data-binary @/tmp/_drive_upload.bin \
|
|
326
|
+
"https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id,name,webViewLink" \
|
|
327
|
+
| jq
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
For a **media-only** upload (no metadata) use `uploadType=media`; for
|
|
331
|
+
files >5 MB use `uploadType=resumable` (covered in [Drive's docs]
|
|
332
|
+
(https://developers.google.com/drive/api/guides/manage-uploads#resumable)).
|
|
333
|
+
|
|
334
|
+
### Replace the contents of an existing file
|
|
335
|
+
|
|
336
|
+
```sh
|
|
337
|
+
FILE_ID='1A2B3CdEfGhIjKlMn'
|
|
338
|
+
LOCAL=/tmp/report-v2.pdf
|
|
339
|
+
curl -sS -X PATCH -H "Authorization: Bearer $GOOGLE_DRIVE_TOKEN" \
|
|
340
|
+
-H 'Content-Type: application/pdf' \
|
|
341
|
+
--data-binary @"$LOCAL" \
|
|
342
|
+
"https://www.googleapis.com/upload/drive/v3/files/$FILE_ID?uploadType=media&fields=id,name,modifiedTime"
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Metadata stays the same (id / parents / name) — only the bytes are
|
|
346
|
+
replaced and Drive bumps `modifiedTime`.
|
|
347
|
+
|
|
348
|
+
### Trash a file (or restore one)
|
|
349
|
+
|
|
350
|
+
```sh
|
|
351
|
+
FILE_ID='1A2B3CdEfGhIjKlMn'
|
|
352
|
+
curl -sS -X PATCH -H "Authorization: Bearer $GOOGLE_DRIVE_TOKEN" \
|
|
353
|
+
-H 'Content-Type: application/json' \
|
|
354
|
+
--data '{"trashed":true}' \
|
|
355
|
+
"https://www.googleapis.com/drive/v3/files/$FILE_ID?fields=id,name,trashed"
|
|
356
|
+
|
|
357
|
+
# Restore:
|
|
358
|
+
curl -sS -X PATCH -H "Authorization: Bearer $GOOGLE_DRIVE_TOKEN" \
|
|
359
|
+
-H 'Content-Type: application/json' \
|
|
360
|
+
--data '{"trashed":false}' \
|
|
361
|
+
"https://www.googleapis.com/drive/v3/files/$FILE_ID?fields=id,name,trashed"
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Prefer `trashed:true` over `DELETE` — `DELETE` is permanent and the
|
|
365
|
+
user can't undo it. Only use `DELETE` when they explicitly say
|
|
366
|
+
"permanently delete".
|
|
367
|
+
|
|
368
|
+
### Bulk "move every PDF in the root to /Documents/PDF" (confirmation pattern)
|
|
369
|
+
|
|
370
|
+
```sh
|
|
371
|
+
# 1. List candidates and show the user before doing anything
|
|
372
|
+
DST_FOLDER_ID='1XYZdocsPdfFolder'
|
|
373
|
+
ROOT_ID='root'
|
|
374
|
+
|
|
375
|
+
CANDS=$(curl -sS -H "Authorization: Bearer $GOOGLE_DRIVE_TOKEN" \
|
|
376
|
+
--get "https://www.googleapis.com/drive/v3/files" \
|
|
377
|
+
--data-urlencode "q='$ROOT_ID' in parents and mimeType='application/pdf' and trashed=false" \
|
|
378
|
+
--data-urlencode 'fields=files(id,name,webViewLink)' \
|
|
379
|
+
| jq '.files')
|
|
380
|
+
echo "$CANDS" | jq -r '.[] | "- \(.name)"'
|
|
381
|
+
|
|
382
|
+
# 2. (after user confirms) actually move
|
|
383
|
+
echo "$CANDS" | jq -r '.[] | .id' | while read FID; do
|
|
384
|
+
curl -sS -X PATCH -H "Authorization: Bearer $GOOGLE_DRIVE_TOKEN" \
|
|
385
|
+
--data '' \
|
|
386
|
+
"https://www.googleapis.com/drive/v3/files/$FID?addParents=$DST_FOLDER_ID&removeParents=$ROOT_ID&fields=id,name,parents" \
|
|
387
|
+
| jq -c '{id, name, parents}'
|
|
388
|
+
done
|
|
389
|
+
```
|
|
390
|
+
|
|
191
391
|
## Common error codes
|
|
192
392
|
|
|
193
393
|
| HTTP | meaning | what to tell the user |
|
|
194
394
|
|---|---|---|
|
|
195
395
|
| `401 UNAUTHENTICATED` | token expired / revoked | "Reconnect the Google Drive connector on the Connections page." |
|
|
196
|
-
| `403 insufficientPermissions` | scope missing | "
|
|
396
|
+
| `403 insufficientPermissions` | write scope missing | "This action needs the Drive read+write scope, but only `drive.readonly` was granted at install. Re-install the connector and check the read+write box." |
|
|
197
397
|
| `403 userRateLimitExceeded` | quota | retry once after 5–10s; if it persists, tell the user. |
|
|
198
398
|
| `404 notFound` | wrong file id OR file isn't visible to this account | double-check the id; if shared, use `sharedWithMe` query above. |
|
|
199
399
|
| `400 invalidQuery` | malformed `q` | print the `q` you sent + the error message back to the user. |
|
|
200
400
|
|
|
201
401
|
Never log or echo `$GOOGLE_DRIVE_TOKEN` — treat it as a secret.
|
|
402
|
+
|
|
403
|
+
Never log or echo `$GOOGLE_DRIVE_TOKEN` — treat it as a secret.
|
|
@@ -1,37 +1,120 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: google-gmail
|
|
3
|
-
description: Read, search and
|
|
3
|
+
description: Read, search, triage, label, archive and send Gmail mail / threads / labels / attachments via the Gmail v1 REST API. Use when the user mentions Gmail, "my inbox", unread mail, recent emails from someone, summarising a thread, downloading an attachment, finding mail by label / query, archiving or labelling a thread, or drafting and sending a reply / new message.
|
|
4
4
|
when_to_use: |
|
|
5
|
-
Trigger when the user wants to read, list, search, summarise
|
|
6
|
-
inspect Gmail mail — including triaging the inbox,
|
|
7
|
-
pulling a single thread
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
Trigger when the user wants to read, list, search, summarise,
|
|
6
|
+
inspect, modify or send Gmail mail — including triaging the inbox,
|
|
7
|
+
surfacing unread, pulling a single thread, downloading an
|
|
8
|
+
attachment, archiving / labelling / trashing messages, or having
|
|
9
|
+
the AI draft and send a reply or new message on their behalf.
|
|
10
|
+
The installed connector always grants `gmail.readonly`; the user
|
|
11
|
+
also opts in to `gmail.modify` (label / archive / trash) and
|
|
12
|
+
`gmail.send` (compose + send) at install time — confirm the action
|
|
13
|
+
is in scope before issuing it.
|
|
10
14
|
connections: [google/gmail]
|
|
11
15
|
allowed_tools: [Bash]
|
|
12
16
|
license: Apache-2.0
|
|
13
17
|
metadata:
|
|
14
18
|
author: acedatacloud
|
|
15
|
-
version: "1.
|
|
19
|
+
version: "1.2"
|
|
16
20
|
---
|
|
17
21
|
|
|
18
22
|
Drive Gmail via `curl + jq`. The user's OAuth bearer token is in
|
|
19
23
|
`$GOOGLE_GMAIL_TOKEN`; every call needs it as
|
|
20
|
-
`Authorization: Bearer $GOOGLE_GMAIL_TOKEN`.
|
|
21
|
-
carries
|
|
22
|
-
|
|
24
|
+
`Authorization: Bearer $GOOGLE_GMAIL_TOKEN`. At minimum the token
|
|
25
|
+
carries `gmail.readonly` plus the identity scopes
|
|
26
|
+
(`openid email profile`); if the user opted in to write at install
|
|
27
|
+
time it also carries `gmail.modify` (label / archive / trash) and/or
|
|
28
|
+
`gmail.send` (compose + send). Always assume the narrowest scope
|
|
29
|
+
until a write actually fails — don't ask Google for new scopes from
|
|
30
|
+
here.
|
|
23
31
|
|
|
24
32
|
The Gmail API returns standard JSON; failures surface as
|
|
25
33
|
`{"error": {"code": 401|403|..., "message": "..."}}` — show that
|
|
26
|
-
error verbatim. `401` means the token expired (re-install). `403
|
|
27
|
-
|
|
28
|
-
this
|
|
34
|
+
error verbatim. `401` means the token expired (re-install). `403
|
|
35
|
+
insufficientPermissions` means the user didn't grant the write scope
|
|
36
|
+
this call needs — explain which scope is missing and suggest
|
|
37
|
+
re-installing the connector with the matching write box checked.
|
|
38
|
+
|
|
39
|
+
**Before any destructive write** (trashing a thread, sending an email)
|
|
40
|
+
show the user the exact target / draft and ask them to confirm. Don't
|
|
41
|
+
fan out across many messages without an explicit go-ahead.
|
|
29
42
|
|
|
30
43
|
**Always start with `users/me/profile`** to confirm the connection works
|
|
31
44
|
AND learn which Gmail account you're operating against. Mailbox payloads
|
|
32
45
|
can be huge — fetch metadata first, only `format=full` when the user
|
|
33
46
|
actually wants the body of a specific message.
|
|
34
47
|
|
|
48
|
+
## Optional: Google Workspace CLI (`gws`) for outbound mail
|
|
49
|
+
|
|
50
|
+
[`gws`](https://github.com/googleworkspace/cli) is Google's official CLI
|
|
51
|
+
(not officially supported — community-maintained on the `googleworkspace`
|
|
52
|
+
org). It dynamically builds its command surface from Google's Discovery
|
|
53
|
+
Document, exits non-zero on API errors, and ships hand-crafted helper
|
|
54
|
+
commands (prefixed `+`) that handle the message-encoding boilerplate.
|
|
55
|
+
|
|
56
|
+
**Use `gws` for sending mail.** The Gmail REST API requires every
|
|
57
|
+
outbound message to be a fully-formed RFC 822 message, base64url-encoded
|
|
58
|
+
into a `raw` field, with reply / forward threading carried in
|
|
59
|
+
`In-Reply-To` / `References` / `threadId`. The `+send / +reply /
|
|
60
|
+
+reply-all / +forward` helpers do all of that for you. **For everything
|
|
61
|
+
else** (read, search, labels, attachments) `gws` and curl are equivalent,
|
|
62
|
+
so the curl recipes below are usually shorter — stay on those.
|
|
63
|
+
|
|
64
|
+
### Install
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
npm install -g @googleworkspace/cli # or: brew install googleworkspace-cli
|
|
68
|
+
# Pre-built binaries also at https://github.com/googleworkspace/cli/releases
|
|
69
|
+
gws --version
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Auth
|
|
73
|
+
|
|
74
|
+
`gws` reads its OAuth bearer token from the `GOOGLE_WORKSPACE_CLI_TOKEN`
|
|
75
|
+
environment variable. The Gmail token used in this skill is in
|
|
76
|
+
`$GOOGLE_GMAIL_TOKEN`, so re-export it once at the top of every shell
|
|
77
|
+
block that calls `gws`:
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
export GOOGLE_WORKSPACE_CLI_TOKEN="$GOOGLE_GMAIL_TOKEN"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
You can confirm the active account with `gws gmail users getProfile
|
|
84
|
+
--params '{"userId":"me"}'`.
|
|
85
|
+
|
|
86
|
+
### Send / reply / forward
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
# New message
|
|
90
|
+
gws gmail +send \
|
|
91
|
+
--to alice@example.com \
|
|
92
|
+
--cc team@example.com \
|
|
93
|
+
--subject "Q1 status" \
|
|
94
|
+
--body "Numbers attached."
|
|
95
|
+
|
|
96
|
+
# Reply (handles threadId, In-Reply-To, References automatically;
|
|
97
|
+
# To is the original sender, Subject gets the "Re: " prefix)
|
|
98
|
+
gws gmail +reply --message-id MSG_ID --body "Thanks — looks good."
|
|
99
|
+
|
|
100
|
+
# Reply-all
|
|
101
|
+
gws gmail +reply-all --message-id MSG_ID --body "+1"
|
|
102
|
+
|
|
103
|
+
# Forward to new recipients (preserves the original message body
|
|
104
|
+
# inline; original headers are summarised in the forward block)
|
|
105
|
+
gws gmail +forward --message-id MSG_ID --to bob@example.com
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Each helper exits with a non-zero status and a JSON error on stderr if
|
|
109
|
+
Google rejects the request — surface that error verbatim. `+send` /
|
|
110
|
+
`+reply` need the `gmail.send` scope; if the user only granted
|
|
111
|
+
`gmail.readonly` you'll see `403 insufficientPermissions` and should ask
|
|
112
|
+
them to re-install the connector with the send box checked.
|
|
113
|
+
|
|
114
|
+
All the read / list / search / label / attachment recipes below are
|
|
115
|
+
intentionally **not** rewritten to `gws` — a one-line `curl ... | jq` is
|
|
116
|
+
shorter and easier to compose with shell pipelines.
|
|
117
|
+
|
|
35
118
|
## Recipes
|
|
36
119
|
|
|
37
120
|
### Verify auth (always run first)
|
|
@@ -201,12 +284,169 @@ while : ; do
|
|
|
201
284
|
done
|
|
202
285
|
```
|
|
203
286
|
|
|
287
|
+
## Write recipes
|
|
288
|
+
|
|
289
|
+
These all need `gmail.modify` (label / archive / trash) or
|
|
290
|
+
`gmail.send` (compose + send). If the user only granted
|
|
291
|
+
`gmail.readonly` at install you'll get `403 insufficientPermissions`
|
|
292
|
+
— surface that and ask them to re-install with the write boxes
|
|
293
|
+
checked.
|
|
294
|
+
|
|
295
|
+
### Mark a message read / unread, star it, archive it (gmail.modify)
|
|
296
|
+
|
|
297
|
+
```sh
|
|
298
|
+
MSG_ID='18f1a2b3c4d5e6f0'
|
|
299
|
+
|
|
300
|
+
# Mark as read = remove the UNREAD label
|
|
301
|
+
curl -sS -X POST -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
|
|
302
|
+
-H 'Content-Type: application/json' \
|
|
303
|
+
--data '{"removeLabelIds":["UNREAD"]}' \
|
|
304
|
+
"https://gmail.googleapis.com/gmail/v1/users/me/messages/$MSG_ID/modify"
|
|
305
|
+
|
|
306
|
+
# Star it = add the STARRED label
|
|
307
|
+
curl -sS -X POST -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
|
|
308
|
+
-H 'Content-Type: application/json' \
|
|
309
|
+
--data '{"addLabelIds":["STARRED"]}' \
|
|
310
|
+
"https://gmail.googleapis.com/gmail/v1/users/me/messages/$MSG_ID/modify"
|
|
311
|
+
|
|
312
|
+
# Archive = remove from INBOX (keeps in All Mail)
|
|
313
|
+
curl -sS -X POST -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
|
|
314
|
+
-H 'Content-Type: application/json' \
|
|
315
|
+
--data '{"removeLabelIds":["INBOX"]}' \
|
|
316
|
+
"https://gmail.googleapis.com/gmail/v1/users/me/messages/$MSG_ID/modify"
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
The `modify` endpoint takes `addLabelIds` and `removeLabelIds`
|
|
320
|
+
together — useful for atomic "archive + label" moves. Use the same
|
|
321
|
+
shape on `/threads/$THREAD_ID/modify` to apply across a whole thread.
|
|
322
|
+
|
|
323
|
+
### Apply a custom label
|
|
324
|
+
|
|
325
|
+
```sh
|
|
326
|
+
# 1. find or remember the label id from labels.list
|
|
327
|
+
LABEL_ID='Label_4'
|
|
328
|
+
MSG_ID='18f1a2b3c4d5e6f0'
|
|
329
|
+
|
|
330
|
+
curl -sS -X POST -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
|
|
331
|
+
-H 'Content-Type: application/json' \
|
|
332
|
+
--data "{\"addLabelIds\":[\"$LABEL_ID\"]}" \
|
|
333
|
+
"https://gmail.googleapis.com/gmail/v1/users/me/messages/$MSG_ID/modify"
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Creating a brand-new label needs the same scope:
|
|
337
|
+
|
|
338
|
+
```sh
|
|
339
|
+
curl -sS -X POST -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
|
|
340
|
+
-H 'Content-Type: application/json' \
|
|
341
|
+
--data '{"name":"Follow up","messageListVisibility":"show","labelListVisibility":"labelShow"}' \
|
|
342
|
+
"https://gmail.googleapis.com/gmail/v1/users/me/labels" \
|
|
343
|
+
| jq '{id, name}'
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Trash a message or thread
|
|
347
|
+
|
|
348
|
+
```sh
|
|
349
|
+
MSG_ID='18f1a2b3c4d5e6f0'
|
|
350
|
+
curl -sS -X POST -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
|
|
351
|
+
"https://gmail.googleapis.com/gmail/v1/users/me/messages/$MSG_ID/trash"
|
|
352
|
+
|
|
353
|
+
# Whole thread:
|
|
354
|
+
THREAD_ID='18f1a2b3c4d5e6f0'
|
|
355
|
+
curl -sS -X POST -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
|
|
356
|
+
"https://gmail.googleapis.com/gmail/v1/users/me/threads/$THREAD_ID/trash"
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Use `/untrash` (same shape) to restore. **Never** use
|
|
360
|
+
`messages.delete` — it permanently deletes and needs a higher scope
|
|
361
|
+
that we don't request.
|
|
362
|
+
|
|
363
|
+
### Send a brand-new email (gmail.send)
|
|
364
|
+
|
|
365
|
+
Gmail wants the message as a base64url-encoded RFC 2822 string.
|
|
366
|
+
|
|
367
|
+
```sh
|
|
368
|
+
# Compose the message
|
|
369
|
+
TO='alice@example.com'
|
|
370
|
+
SUBJECT='Quick hello'
|
|
371
|
+
BODY='Hi Alice,
|
|
372
|
+
|
|
373
|
+
Just a quick test note from the AceDataCloud Gmail connector.
|
|
374
|
+
|
|
375
|
+
Best,
|
|
376
|
+
Qingcai'
|
|
377
|
+
|
|
378
|
+
# Multi-line subject lines need MIME encoded-word for non-ASCII; ASCII is fine raw.
|
|
379
|
+
RAW=$(printf 'To: %s\r\nSubject: %s\r\nContent-Type: text/plain; charset=UTF-8\r\nMIME-Version: 1.0\r\n\r\n%s' \
|
|
380
|
+
"$TO" "$SUBJECT" "$BODY" \
|
|
381
|
+
| base64 | tr -d '\n' | tr '+/' '-_' | tr -d '=')
|
|
382
|
+
|
|
383
|
+
curl -sS -X POST -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
|
|
384
|
+
-H 'Content-Type: application/json' \
|
|
385
|
+
--data "{\"raw\":\"$RAW\"}" \
|
|
386
|
+
"https://gmail.googleapis.com/gmail/v1/users/me/messages/send" \
|
|
387
|
+
| jq '{id, threadId, labelIds}'
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
For non-ASCII subjects (Chinese / emoji), use MIME encoded-word:
|
|
391
|
+
|
|
392
|
+
```sh
|
|
393
|
+
SUBJECT_RAW='你好,季度复盘草稿'
|
|
394
|
+
SUBJECT_ENCODED="=?UTF-8?B?$(printf %s "$SUBJECT_RAW" | base64)?="
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### Reply in-thread (keeps the thread together)
|
|
398
|
+
|
|
399
|
+
Reply by setting the `In-Reply-To` and `References` headers to the
|
|
400
|
+
Message-Id of the message you're replying to, **and** pass the
|
|
401
|
+
Gmail thread id in the API body:
|
|
402
|
+
|
|
403
|
+
```sh
|
|
404
|
+
ORIG_MSG_ID='18f1a2b3c4d5e6f0'
|
|
405
|
+
ORIG=$(curl -sS -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
|
|
406
|
+
--get "https://gmail.googleapis.com/gmail/v1/users/me/messages/$ORIG_MSG_ID" \
|
|
407
|
+
--data-urlencode 'format=metadata' \
|
|
408
|
+
--data-urlencode 'metadataHeaders=Message-ID' \
|
|
409
|
+
--data-urlencode 'metadataHeaders=Subject' \
|
|
410
|
+
--data-urlencode 'metadataHeaders=From')
|
|
411
|
+
MID=$(echo "$ORIG" | jq -r '.payload.headers | from_entries | .["Message-ID"] // .["Message-Id"]')
|
|
412
|
+
FROM=$(echo "$ORIG" | jq -r '.payload.headers | from_entries | .From')
|
|
413
|
+
SUBJ=$(echo "$ORIG" | jq -r '.payload.headers | from_entries | .Subject')
|
|
414
|
+
TID=$(echo "$ORIG" | jq -r .threadId)
|
|
415
|
+
|
|
416
|
+
RAW=$(printf 'To: %s\r\nSubject: Re: %s\r\nIn-Reply-To: %s\r\nReferences: %s\r\nContent-Type: text/plain; charset=UTF-8\r\nMIME-Version: 1.0\r\n\r\n%s' \
|
|
417
|
+
"$FROM" "$SUBJ" "$MID" "$MID" \
|
|
418
|
+
'Replying inline — will follow up later today.' \
|
|
419
|
+
| base64 | tr -d '\n' | tr '+/' '-_' | tr -d '=')
|
|
420
|
+
|
|
421
|
+
curl -sS -X POST -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
|
|
422
|
+
-H 'Content-Type: application/json' \
|
|
423
|
+
--data "{\"raw\":\"$RAW\",\"threadId\":\"$TID\"}" \
|
|
424
|
+
"https://gmail.googleapis.com/gmail/v1/users/me/messages/send" \
|
|
425
|
+
| jq '{id, threadId}'
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
Without the `threadId` in the body Gmail starts a brand-new thread
|
|
429
|
+
even with the right `In-Reply-To` headers.
|
|
430
|
+
|
|
431
|
+
### Save a draft instead of sending
|
|
432
|
+
|
|
433
|
+
Same `raw` payload, different endpoint — still costs `gmail.send`
|
|
434
|
+
(`drafts` shares the send scope under the hood for write):
|
|
435
|
+
|
|
436
|
+
```sh
|
|
437
|
+
curl -sS -X POST -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
|
|
438
|
+
-H 'Content-Type: application/json' \
|
|
439
|
+
--data "{\"message\":{\"raw\":\"$RAW\"}}" \
|
|
440
|
+
"https://gmail.googleapis.com/gmail/v1/users/me/drafts" \
|
|
441
|
+
| jq '{id, message: {id: .message.id, threadId: .message.threadId}}'
|
|
442
|
+
```
|
|
443
|
+
|
|
204
444
|
## Common error codes
|
|
205
445
|
|
|
206
446
|
| HTTP | meaning | what to tell the user |
|
|
207
447
|
|---|---|---|
|
|
208
448
|
| `401 UNAUTHENTICATED` | token expired / revoked | "Reconnect the Gmail connector on the Connections page." |
|
|
209
|
-
| `403 insufficientPermissions` | scope missing |
|
|
449
|
+
| `403 insufficientPermissions` | scope missing | identify which scope (`gmail.modify` for label/archive/trash, `gmail.send` for sending) and suggest re-installing the connector with that box checked. |
|
|
210
450
|
| `403 userRateLimitExceeded` / `429` | quota / throttling | back off ~5s, then retry once. |
|
|
211
451
|
| `404 notFound` | wrong message / thread / attachment id | double-check the id, or fall back to `messages.list` with the right query. |
|
|
212
452
|
| `400 invalidQuery` | malformed `q` | print the `q` you sent + the error back to the user. |
|
|
@@ -1,36 +1,43 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: google-tasks
|
|
3
|
-
description: Read Google Tasks task lists and individual tasks via the Tasks v1 REST API. Use when the user mentions Google Tasks, todo / pending / overdue tasks, weekly task recap,
|
|
3
|
+
description: Read and manage Google Tasks task lists and individual tasks via the Tasks v1 REST API. Use when the user mentions Google Tasks, todo / pending / overdue tasks, weekly task recap, grouping todos by list, adding or completing a task, or moving / deleting tasks.
|
|
4
4
|
when_to_use: |
|
|
5
|
-
Trigger when the user wants to inspect their Google
|
|
6
|
-
task lists, surface pending items, group by due date,
|
|
7
|
-
details for one task
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
Trigger when the user wants to inspect or manage their Google
|
|
6
|
+
Tasks — list task lists, surface pending items, group by due date,
|
|
7
|
+
pull details for one task, add new todos, mark items complete,
|
|
8
|
+
re-order or delete tasks. The installed connector always grants
|
|
9
|
+
`tasks.readonly`; the user opts in to the broader `tasks` scope
|
|
10
|
+
(full read + write) at install — confirm before destructive writes.
|
|
10
11
|
connections: [google/tasks]
|
|
11
12
|
allowed_tools: [Bash]
|
|
12
13
|
license: Apache-2.0
|
|
13
14
|
metadata:
|
|
14
15
|
author: acedatacloud
|
|
15
|
-
version: "1.
|
|
16
|
+
version: "1.1"
|
|
16
17
|
---
|
|
17
18
|
|
|
18
19
|
Drive Google Tasks via `curl + jq`. The user's OAuth bearer token is
|
|
19
20
|
in `$GOOGLE_TASKS_TOKEN`; every call needs it as
|
|
20
|
-
`Authorization: Bearer $GOOGLE_TASKS_TOKEN`.
|
|
21
|
-
carries
|
|
22
|
-
|
|
21
|
+
`Authorization: Bearer $GOOGLE_TASKS_TOKEN`. At minimum the token
|
|
22
|
+
carries `tasks.readonly` plus the identity scopes
|
|
23
|
+
(`openid email profile`); if the user opted in to write at install
|
|
24
|
+
time it also carries the broader `tasks` scope (read + write).
|
|
23
25
|
|
|
24
26
|
The Tasks API returns standard JSON; failures surface as
|
|
25
27
|
`{"error": {"code": 401|403|..., "message": "..."}}` — show that
|
|
26
28
|
error verbatim. `401` means the token expired (re-install). `403
|
|
27
|
-
insufficientPermissions` means the user
|
|
28
|
-
|
|
29
|
+
insufficientPermissions` on a write means the user only granted
|
|
30
|
+
`tasks.readonly` — ask them to re-install with the read+write box
|
|
31
|
+
checked.
|
|
29
32
|
|
|
30
33
|
**Always start with `users/@me/lists`** to discover which task lists
|
|
31
34
|
the account has — the user's default plus any extras they created on
|
|
32
35
|
calendar.google.com or in the Tasks app.
|
|
33
36
|
|
|
37
|
+
**Before bulk creates / completions / deletes** echo the exact
|
|
38
|
+
titles back to the user and ask them to confirm. Don't trash a
|
|
39
|
+
task by guessing an id.
|
|
40
|
+
|
|
34
41
|
## Recipes
|
|
35
42
|
|
|
36
43
|
### Verify auth + list all task lists (always run first)
|
|
@@ -173,12 +180,115 @@ while : ; do
|
|
|
173
180
|
done
|
|
174
181
|
```
|
|
175
182
|
|
|
183
|
+
## Write recipes
|
|
184
|
+
|
|
185
|
+
These all need the broader `tasks` scope. If the user only granted
|
|
186
|
+
`tasks.readonly` you'll get `403 insufficientPermissions` — surface
|
|
187
|
+
that and ask them to re-install with the read+write box checked.
|
|
188
|
+
|
|
189
|
+
### Add a new task
|
|
190
|
+
|
|
191
|
+
```sh
|
|
192
|
+
LIST_ID='MTAxMjM0NTY3OA'
|
|
193
|
+
curl -sS -X POST -H "Authorization: Bearer $GOOGLE_TASKS_TOKEN" \
|
|
194
|
+
-H 'Content-Type: application/json' \
|
|
195
|
+
--data '{"title":"Draft Q2 plan","notes":"Outline + risks + asks.","due":"2026-05-15T00:00:00.000Z"}' \
|
|
196
|
+
"https://tasks.googleapis.com/tasks/v1/lists/$LIST_ID/tasks" \
|
|
197
|
+
| jq '{id, title, due, status}'
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Google stores `due` as midnight UTC of the chosen day — the time of
|
|
201
|
+
day is ignored in the UI. To insert at the very top of the list,
|
|
202
|
+
add `?previous=` (no value) to the URL.
|
|
203
|
+
|
|
204
|
+
### Bulk add three tasks under user confirmation
|
|
205
|
+
|
|
206
|
+
```sh
|
|
207
|
+
LIST_ID='MTAxMjM0NTY3OA'
|
|
208
|
+
DUE='2026-05-12T00:00:00.000Z'
|
|
209
|
+
for T in 'Reply to Alice' 'Review PR #404' 'Send meeting recap'; do
|
|
210
|
+
curl -sS -X POST -H "Authorization: Bearer $GOOGLE_TASKS_TOKEN" \
|
|
211
|
+
-H 'Content-Type: application/json' \
|
|
212
|
+
--data "{\"title\":$(jq -nr --arg t "$T" '$t'),\"due\":\"$DUE\"}" \
|
|
213
|
+
"https://tasks.googleapis.com/tasks/v1/lists/$LIST_ID/tasks" \
|
|
214
|
+
| jq -c '{id, title, due}'
|
|
215
|
+
done
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Always list the titles you're about to create and ask for the user's
|
|
219
|
+
go-ahead before running this loop — there is no atomic batch endpoint.
|
|
220
|
+
|
|
221
|
+
### Mark a task complete
|
|
222
|
+
|
|
223
|
+
```sh
|
|
224
|
+
LIST_ID='MTAxMjM0NTY3OA'
|
|
225
|
+
TASK_ID='dGFza0lkRXhhbXBsZQ'
|
|
226
|
+
NOW=$(date -u +%Y-%m-%dT%H:%M:%S.000Z)
|
|
227
|
+
curl -sS -X PATCH -H "Authorization: Bearer $GOOGLE_TASKS_TOKEN" \
|
|
228
|
+
-H 'Content-Type: application/json' \
|
|
229
|
+
--data "{\"status\":\"completed\",\"completed\":\"$NOW\"}" \
|
|
230
|
+
"https://tasks.googleapis.com/tasks/v1/lists/$LIST_ID/tasks/$TASK_ID" \
|
|
231
|
+
| jq '{id, title, status, completed}'
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Reverse with `{"status":"needsAction","completed":null}`.
|
|
235
|
+
|
|
236
|
+
### Edit a task's title / notes / due date
|
|
237
|
+
|
|
238
|
+
```sh
|
|
239
|
+
LIST_ID='MTAxMjM0NTY3OA'
|
|
240
|
+
TASK_ID='dGFza0lkRXhhbXBsZQ'
|
|
241
|
+
curl -sS -X PATCH -H "Authorization: Bearer $GOOGLE_TASKS_TOKEN" \
|
|
242
|
+
-H 'Content-Type: application/json' \
|
|
243
|
+
--data '{"title":"Draft Q2 plan (rev2)","notes":"Cover risks + asks + budget.","due":"2026-05-20T00:00:00.000Z"}' \
|
|
244
|
+
"https://tasks.googleapis.com/tasks/v1/lists/$LIST_ID/tasks/$TASK_ID" \
|
|
245
|
+
| jq '{id, title, due, notes}'
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Delete a task
|
|
249
|
+
|
|
250
|
+
```sh
|
|
251
|
+
LIST_ID='MTAxMjM0NTY3OA'
|
|
252
|
+
TASK_ID='dGFza0lkRXhhbXBsZQ'
|
|
253
|
+
curl -sS -X DELETE -H "Authorization: Bearer $GOOGLE_TASKS_TOKEN" \
|
|
254
|
+
"https://tasks.googleapis.com/tasks/v1/lists/$LIST_ID/tasks/$TASK_ID" \
|
|
255
|
+
-o /dev/null -w 'HTTP %{http_code}\n'
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
`204` = success. There is no soft-delete — once gone the task is
|
|
259
|
+
gone. Echo the title back before deleting.
|
|
260
|
+
|
|
261
|
+
### Re-order: move a task to a position
|
|
262
|
+
|
|
263
|
+
```sh
|
|
264
|
+
LIST_ID='MTAxMjM0NTY3OA'
|
|
265
|
+
TASK_ID='dGFza0lkRXhhbXBsZQ'
|
|
266
|
+
PREV='dGFza0lkUHJldg' # task id this one should appear AFTER; omit to move to top
|
|
267
|
+
curl -sS -X POST -H "Authorization: Bearer $GOOGLE_TASKS_TOKEN" \
|
|
268
|
+
--data '' \
|
|
269
|
+
"https://tasks.googleapis.com/tasks/v1/lists/$LIST_ID/tasks/$TASK_ID/move?previous=$PREV" \
|
|
270
|
+
| jq '{id, title, parent, position}'
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Use `?parent=...` instead of `?previous=...` to nest a task under
|
|
274
|
+
another task as a sub-task.
|
|
275
|
+
|
|
276
|
+
### Create a brand-new task list
|
|
277
|
+
|
|
278
|
+
```sh
|
|
279
|
+
curl -sS -X POST -H "Authorization: Bearer $GOOGLE_TASKS_TOKEN" \
|
|
280
|
+
-H 'Content-Type: application/json' \
|
|
281
|
+
--data '{"title":"Q2 follow-ups"}' \
|
|
282
|
+
"https://tasks.googleapis.com/tasks/v1/users/@me/lists" \
|
|
283
|
+
| jq '{id, title}'
|
|
284
|
+
```
|
|
285
|
+
|
|
176
286
|
## Common error codes
|
|
177
287
|
|
|
178
288
|
| HTTP | meaning | what to tell the user |
|
|
179
289
|
|---|---|---|
|
|
180
290
|
| `401 UNAUTHENTICATED` | token expired / revoked | "Reconnect the Google Tasks connector on the Connections page." |
|
|
181
|
-
| `403 insufficientPermissions` | scope missing | "This
|
|
291
|
+
| `403 insufficientPermissions` | write scope missing | "This action needs the Tasks read+write scope, but only `tasks.readonly` was granted. Re-install the connector with the read+write box checked." |
|
|
182
292
|
| `404 notFound` | wrong list / task id | re-list with `users/@me/lists` to find the right id. |
|
|
183
293
|
| `429 quotaExceeded` | quota / throttling | back off ~5s, then retry once. |
|
|
184
294
|
|
|
@@ -17,8 +17,7 @@ Quick examples:
|
|
|
17
17
|
|
|
18
18
|
Environment:
|
|
19
19
|
Reads TENCENTCLOUD_SECRET_ID, TENCENTCLOUD_SECRET_KEY, TENCENTCLOUD_REGION
|
|
20
|
-
from the
|
|
21
|
-
AceDataCloud BYOC connection; or set manually in `.env`).
|
|
20
|
+
from the environment.
|
|
22
21
|
"""
|
|
23
22
|
|
|
24
23
|
from __future__ import annotations
|