@acedatacloud/skills 2026.504.4 → 2026.504.5

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.504.4",
3
+ "version": "2026.504.5",
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,48 @@
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, or checking free time on a specific calendar.
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 from their Google
6
- Calendar — list / search / inspect events, build today's or this
7
- week's agenda, check free / busy windows, or pull invite details
8
- for a specific meeting. The installed connector grants read-only
9
- scope (`calendar.readonly`); creating / updating / cancelling
10
- events is out of scope.
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.0"
18
+ version: "1.1"
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`. The token already
22
- carries the `calendar.readonly` scope the user agreed to at install
23
- plus the identity scopes (`openid email profile`).
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 is asking for a write this
29
- connector cannot satisfy say so.
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
+
36
46
  ## Recipes
37
47
 
38
48
  ### Verify auth + discover calendars (always run first)
@@ -182,14 +192,150 @@ while : ; do
182
192
  done
183
193
  ```
184
194
 
195
+ ## Write recipes
196
+
197
+ These all need the broader `calendar` scope. If the user only granted
198
+ `calendar.readonly` you'll get `403 insufficientPermissions` —
199
+ surface that and ask them to re-install with the read+write box
200
+ checked. **Always echo the event summary, time and attendee list
201
+ back to the user before creating or cancelling anything.**
202
+
203
+ ### Create a single event (with optional attendees + Google Meet link)
204
+
205
+ ```sh
206
+ TZ=$(curl -sS -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
207
+ "https://www.googleapis.com/calendar/v3/users/me/settings/timezone" | jq -r .value)
208
+
209
+ cat > /tmp/_cal_event.json <<JSON
210
+ {
211
+ "summary": "Sync — Q2 OKR review",
212
+ "location": "Online",
213
+ "description": "Drafted by AceDataCloud.",
214
+ "start": {"dateTime": "2026-05-12T10:00:00", "timeZone": "$TZ"},
215
+ "end": {"dateTime": "2026-05-12T10:30:00", "timeZone": "$TZ"},
216
+ "attendees": [
217
+ {"email": "alice@example.com"},
218
+ {"email": "bob@example.com"}
219
+ ],
220
+ "reminders": {"useDefault": true},
221
+ "conferenceData": {
222
+ "createRequest": {
223
+ "requestId": "meet-$(date +%s)",
224
+ "conferenceSolutionKey": {"type": "hangoutsMeet"}
225
+ }
226
+ }
227
+ }
228
+ JSON
229
+
230
+ # sendUpdates: 'all' = email all attendees; 'externalOnly' = only non-org; 'none' = silent
231
+ curl -sS -X POST -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
232
+ -H 'Content-Type: application/json' \
233
+ --data @/tmp/_cal_event.json \
234
+ "https://www.googleapis.com/calendar/v3/calendars/primary/events?conferenceDataVersion=1&sendUpdates=all" \
235
+ | jq '{id, htmlLink, hangoutLink, summary, start, end, attendees}'
236
+ ```
237
+
238
+ Drop the `conferenceData` block if the user didn't ask for a Meet
239
+ link — it'll fall back to a plain event.
240
+
241
+ ### Create a recurring event
242
+
243
+ ```sh
244
+ TZ=$(curl -sS -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
245
+ "https://www.googleapis.com/calendar/v3/users/me/settings/timezone" | jq -r .value)
246
+ cat > /tmp/_cal_recur.json <<JSON
247
+ {
248
+ "summary": "Weekly 1:1",
249
+ "start": {"dateTime": "2026-05-12T15:00:00", "timeZone": "$TZ"},
250
+ "end": {"dateTime": "2026-05-12T15:30:00", "timeZone": "$TZ"},
251
+ "recurrence": ["RRULE:FREQ=WEEKLY;BYDAY=TU;COUNT=12"]
252
+ }
253
+ JSON
254
+ curl -sS -X POST -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
255
+ -H 'Content-Type: application/json' \
256
+ --data @/tmp/_cal_recur.json \
257
+ "https://www.googleapis.com/calendar/v3/calendars/primary/events" \
258
+ | jq '{id, recurrence, summary}'
259
+ ```
260
+
261
+ RRULE follows RFC 5545. Common patterns: `FREQ=DAILY`, `FREQ=WEEKLY;BYDAY=MO,WE,FR`,
262
+ `FREQ=MONTHLY;BYMONTHDAY=15`. Add `UNTIL=20261231T235959Z` or `COUNT=12`
263
+ for a hard stop.
264
+
265
+ ### Update an existing event (PATCH — partial update)
266
+
267
+ ```sh
268
+ EVENT_ID='abc123def4567890ghijklmnop'
269
+ curl -sS -X PATCH -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
270
+ -H 'Content-Type: application/json' \
271
+ --data '{"location":"Conference Room 4","description":"Now in-person."}' \
272
+ "https://www.googleapis.com/calendar/v3/calendars/primary/events/$EVENT_ID?sendUpdates=all" \
273
+ | jq '{id, summary, location, description}'
274
+ ```
275
+
276
+ `PATCH` only changes the fields you send; `PUT` replaces the entire
277
+ event payload. Prefer `PATCH`.
278
+
279
+ ### Reschedule an event
280
+
281
+ ```sh
282
+ EVENT_ID='abc123def4567890ghijklmnop'
283
+ TZ=$(curl -sS -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
284
+ "https://www.googleapis.com/calendar/v3/users/me/settings/timezone" | jq -r .value)
285
+ cat > /tmp/_cal_resched.json <<JSON
286
+ {
287
+ "start": {"dateTime": "2026-05-12T14:00:00", "timeZone": "$TZ"},
288
+ "end": {"dateTime": "2026-05-12T14:30:00", "timeZone": "$TZ"}
289
+ }
290
+ JSON
291
+ curl -sS -X PATCH -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
292
+ -H 'Content-Type: application/json' \
293
+ --data @/tmp/_cal_resched.json \
294
+ "https://www.googleapis.com/calendar/v3/calendars/primary/events/$EVENT_ID?sendUpdates=all" \
295
+ | jq '{id, summary, start, end}'
296
+ ```
297
+
298
+ ### Add or change attendees
299
+
300
+ Google requires you to send the **complete** attendee list when
301
+ patching attendees — fetch the current list, mutate, send back:
302
+
303
+ ```sh
304
+ EVENT_ID='abc123def4567890ghijklmnop'
305
+ CURRENT=$(curl -sS -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
306
+ "https://www.googleapis.com/calendar/v3/calendars/primary/events/$EVENT_ID?fields=attendees" \
307
+ | jq '.attendees // []')
308
+ NEW=$(echo "$CURRENT" | jq '. + [{"email":"carol@example.com"}]')
309
+ curl -sS -X PATCH -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
310
+ -H 'Content-Type: application/json' \
311
+ --data "{\"attendees\": $NEW}" \
312
+ "https://www.googleapis.com/calendar/v3/calendars/primary/events/$EVENT_ID?sendUpdates=all" \
313
+ | jq '{id, attendees}'
314
+ ```
315
+
316
+ ### Cancel / delete an event
317
+
318
+ ```sh
319
+ EVENT_ID='abc123def4567890ghijklmnop'
320
+ curl -sS -X DELETE -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
321
+ "https://www.googleapis.com/calendar/v3/calendars/primary/events/$EVENT_ID?sendUpdates=all" \
322
+ -o /dev/null -w 'HTTP %{http_code}\n'
323
+ ```
324
+
325
+ `204` = success. To cancel one occurrence of a recurring event, fetch
326
+ the instance with `events.instances` first, then `DELETE` the
327
+ specific instance id (it has a longer `EVENT_ID_YYYYMMDDTHHMMSSZ`
328
+ shape).
329
+
185
330
  ## Common error codes
186
331
 
187
332
  | HTTP | meaning | what to tell the user |
188
333
  |---|---|---|
189
334
  | `401 UNAUTHENTICATED` | token expired / revoked | "Reconnect the Google Calendar connector on the Connections page." |
190
- | `403 insufficientPermissions` | scope missing | "This connector is read-only creating or modifying events isn't possible." |
335
+ | `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
336
  | `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
337
  | `404 notFound` | wrong event / calendar id | double-check the id and try `calendarList` to confirm the calendar exists. |
338
+ | `409 conflict` | recurring event id collision | append a UUID to your `requestId` and retry. |
193
339
  | `429 quotaExceeded` | quota / throttling | back off ~5s, then retry once. |
194
340
 
195
341
  Never log or echo `$GOOGLE_CALENDAR_TOKEN` — treat it as a secret.
@@ -1,32 +1,40 @@
1
1
  ---
2
2
  name: google-drive
3
- description: Read and search 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, or searching by name / owner / folder.
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 or download files
6
- in their Google Drive — including Google-native docs (Docs / Sheets /
7
- Slides) which need a special "export" call to get plain content. The
8
- installed connector grants read-only scope (`drive.readonly`); writes
9
- are out of scope.
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.0"
17
+ version: "1.1"
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`. The token already carries
21
- the `drive.readonly` scope the user agreed to at install plus the
22
- identity scopes (`openid email profile`).
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 connector grants only `drive.readonly` and the user is
29
- asking for a write say so explicitly.
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.
@@ -188,14 +196,153 @@ while : ; do
188
196
  done
189
197
  ```
190
198
 
199
+ ## Write recipes
200
+
201
+ These all need the broader `drive` scope. If the user only granted
202
+ `drive.readonly` you'll get `403 insufficientPermissions` — surface
203
+ that and suggest re-installing with the read+write box checked.
204
+ **Always echo the target name + path back to the user before
205
+ trashing or bulk-moving anything.**
206
+
207
+ ### Rename a file
208
+
209
+ ```sh
210
+ FILE_ID='1A2B3CdEfGhIjKlMn'
211
+ NEW_NAME='2026 Q2 OKR (final).gdoc'
212
+ curl -sS -X PATCH -H "Authorization: Bearer $GOOGLE_DRIVE_TOKEN" \
213
+ -H 'Content-Type: application/json' \
214
+ --data "{\"name\":$(jq -nr --arg n "$NEW_NAME" '$n')}" \
215
+ "https://www.googleapis.com/drive/v3/files/$FILE_ID?fields=id,name"
216
+ ```
217
+
218
+ ### Move a file to a different folder
219
+
220
+ Drive's folder model is parent-id based. Move = remove old parent,
221
+ add new parent:
222
+
223
+ ```sh
224
+ FILE_ID='1A2B3CdEfGhIjKlMn'
225
+ NEW_PARENT='1XYZnewFolderId'
226
+
227
+ # Read existing parents (so we can pass them in removeParents)
228
+ OLD_PARENTS=$(curl -sS -H "Authorization: Bearer $GOOGLE_DRIVE_TOKEN" \
229
+ "https://www.googleapis.com/drive/v3/files/$FILE_ID?fields=parents" \
230
+ | jq -r '.parents | join(",")')
231
+
232
+ curl -sS -X PATCH -H "Authorization: Bearer $GOOGLE_DRIVE_TOKEN" \
233
+ --data '' \
234
+ "https://www.googleapis.com/drive/v3/files/$FILE_ID?addParents=$NEW_PARENT&removeParents=$OLD_PARENTS&fields=id,name,parents"
235
+ ```
236
+
237
+ ### Create a new folder
238
+
239
+ ```sh
240
+ PARENT_ID='1XYZparentFolderId' # or 'root' for My Drive root
241
+ curl -sS -X POST -H "Authorization: Bearer $GOOGLE_DRIVE_TOKEN" \
242
+ -H 'Content-Type: application/json' \
243
+ --data "{\"name\":\"Reports / 2026Q2\",\"mimeType\":\"application/vnd.google-apps.folder\",\"parents\":[\"$PARENT_ID\"]}" \
244
+ "https://www.googleapis.com/drive/v3/files?fields=id,name,webViewLink" \
245
+ | jq
246
+ ```
247
+
248
+ ### Upload a file (multipart so metadata + bytes go in one request)
249
+
250
+ ```sh
251
+ LOCAL=/tmp/report.pdf
252
+ NAME='Q2 report.pdf'
253
+ PARENT_ID='1XYZparentFolderId'
254
+ MIME='application/pdf'
255
+
256
+ BOUNDARY='aceDataBoundary'
257
+ META=$(jq -nc --arg n "$NAME" --arg p "$PARENT_ID" '{name:$n, parents:[$p]}')
258
+ {
259
+ printf -- '--%s\r\n' "$BOUNDARY"
260
+ printf 'Content-Type: application/json; charset=UTF-8\r\n\r\n'
261
+ printf '%s\r\n' "$META"
262
+ printf -- '--%s\r\n' "$BOUNDARY"
263
+ printf 'Content-Type: %s\r\n\r\n' "$MIME"
264
+ cat "$LOCAL"
265
+ printf '\r\n--%s--\r\n' "$BOUNDARY"
266
+ } > /tmp/_drive_upload.bin
267
+
268
+ curl -sS -X POST -H "Authorization: Bearer $GOOGLE_DRIVE_TOKEN" \
269
+ -H "Content-Type: multipart/related; boundary=$BOUNDARY" \
270
+ --data-binary @/tmp/_drive_upload.bin \
271
+ "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id,name,webViewLink" \
272
+ | jq
273
+ ```
274
+
275
+ For a **media-only** upload (no metadata) use `uploadType=media`; for
276
+ files >5 MB use `uploadType=resumable` (covered in [Drive's docs]
277
+ (https://developers.google.com/drive/api/guides/manage-uploads#resumable)).
278
+
279
+ ### Replace the contents of an existing file
280
+
281
+ ```sh
282
+ FILE_ID='1A2B3CdEfGhIjKlMn'
283
+ LOCAL=/tmp/report-v2.pdf
284
+ curl -sS -X PATCH -H "Authorization: Bearer $GOOGLE_DRIVE_TOKEN" \
285
+ -H 'Content-Type: application/pdf' \
286
+ --data-binary @"$LOCAL" \
287
+ "https://www.googleapis.com/upload/drive/v3/files/$FILE_ID?uploadType=media&fields=id,name,modifiedTime"
288
+ ```
289
+
290
+ Metadata stays the same (id / parents / name) — only the bytes are
291
+ replaced and Drive bumps `modifiedTime`.
292
+
293
+ ### Trash a file (or restore one)
294
+
295
+ ```sh
296
+ FILE_ID='1A2B3CdEfGhIjKlMn'
297
+ curl -sS -X PATCH -H "Authorization: Bearer $GOOGLE_DRIVE_TOKEN" \
298
+ -H 'Content-Type: application/json' \
299
+ --data '{"trashed":true}' \
300
+ "https://www.googleapis.com/drive/v3/files/$FILE_ID?fields=id,name,trashed"
301
+
302
+ # Restore:
303
+ curl -sS -X PATCH -H "Authorization: Bearer $GOOGLE_DRIVE_TOKEN" \
304
+ -H 'Content-Type: application/json' \
305
+ --data '{"trashed":false}' \
306
+ "https://www.googleapis.com/drive/v3/files/$FILE_ID?fields=id,name,trashed"
307
+ ```
308
+
309
+ Prefer `trashed:true` over `DELETE` — `DELETE` is permanent and the
310
+ user can't undo it. Only use `DELETE` when they explicitly say
311
+ "permanently delete".
312
+
313
+ ### Bulk "move every PDF in the root to /Documents/PDF" (confirmation pattern)
314
+
315
+ ```sh
316
+ # 1. List candidates and show the user before doing anything
317
+ DST_FOLDER_ID='1XYZdocsPdfFolder'
318
+ ROOT_ID='root'
319
+
320
+ CANDS=$(curl -sS -H "Authorization: Bearer $GOOGLE_DRIVE_TOKEN" \
321
+ --get "https://www.googleapis.com/drive/v3/files" \
322
+ --data-urlencode "q='$ROOT_ID' in parents and mimeType='application/pdf' and trashed=false" \
323
+ --data-urlencode 'fields=files(id,name,webViewLink)' \
324
+ | jq '.files')
325
+ echo "$CANDS" | jq -r '.[] | "- \(.name)"'
326
+
327
+ # 2. (after user confirms) actually move
328
+ echo "$CANDS" | jq -r '.[] | .id' | while read FID; do
329
+ curl -sS -X PATCH -H "Authorization: Bearer $GOOGLE_DRIVE_TOKEN" \
330
+ --data '' \
331
+ "https://www.googleapis.com/drive/v3/files/$FID?addParents=$DST_FOLDER_ID&removeParents=$ROOT_ID&fields=id,name,parents" \
332
+ | jq -c '{id, name, parents}'
333
+ done
334
+ ```
335
+
191
336
  ## Common error codes
192
337
 
193
338
  | HTTP | meaning | what to tell the user |
194
339
  |---|---|---|
195
340
  | `401 UNAUTHENTICATED` | token expired / revoked | "Reconnect the Google Drive connector on the Connections page." |
196
- | `403 insufficientPermissions` | scope missing | "Your installed connector only grants read access this action needs a write scope we don't have." |
341
+ | `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
342
  | `403 userRateLimitExceeded` | quota | retry once after 5–10s; if it persists, tell the user. |
198
343
  | `404 notFound` | wrong file id OR file isn't visible to this account | double-check the id; if shared, use `sharedWithMe` query above. |
199
344
  | `400 invalidQuery` | malformed `q` | print the `q` you sent + the error message back to the user. |
200
345
 
201
346
  Never log or echo `$GOOGLE_DRIVE_TOKEN` — treat it as a secret.
347
+
348
+ Never log or echo `$GOOGLE_DRIVE_TOKEN` — treat it as a secret.
@@ -1,31 +1,44 @@
1
1
  ---
2
2
  name: google-gmail
3
- description: Read, search and triage 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, or finding mail by label / query.
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 or
6
- inspect Gmail mail — including triaging the inbox, surfacing unread,
7
- pulling a single thread for review, or downloading an attachment.
8
- The installed connector grants read-only scope (`gmail.readonly`);
9
- sending / replying / archiving / labelling are out of scope.
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.0"
19
+ version: "1.1"
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`. The token already
21
- carries the `gmail.readonly` scope the user agreed to at install plus
22
- the identity scopes (`openid email profile`).
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
- or `400 insufficientPermissions` means the user is asking for a write
28
- this connector cannot satisfy say so.
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 needsexplain 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
@@ -201,12 +214,169 @@ while : ; do
201
214
  done
202
215
  ```
203
216
 
217
+ ## Write recipes
218
+
219
+ These all need `gmail.modify` (label / archive / trash) or
220
+ `gmail.send` (compose + send). If the user only granted
221
+ `gmail.readonly` at install you'll get `403 insufficientPermissions`
222
+ — surface that and ask them to re-install with the write boxes
223
+ checked.
224
+
225
+ ### Mark a message read / unread, star it, archive it (gmail.modify)
226
+
227
+ ```sh
228
+ MSG_ID='18f1a2b3c4d5e6f0'
229
+
230
+ # Mark as read = remove the UNREAD label
231
+ curl -sS -X POST -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
232
+ -H 'Content-Type: application/json' \
233
+ --data '{"removeLabelIds":["UNREAD"]}' \
234
+ "https://gmail.googleapis.com/gmail/v1/users/me/messages/$MSG_ID/modify"
235
+
236
+ # Star it = add the STARRED label
237
+ curl -sS -X POST -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
238
+ -H 'Content-Type: application/json' \
239
+ --data '{"addLabelIds":["STARRED"]}' \
240
+ "https://gmail.googleapis.com/gmail/v1/users/me/messages/$MSG_ID/modify"
241
+
242
+ # Archive = remove from INBOX (keeps in All Mail)
243
+ curl -sS -X POST -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
244
+ -H 'Content-Type: application/json' \
245
+ --data '{"removeLabelIds":["INBOX"]}' \
246
+ "https://gmail.googleapis.com/gmail/v1/users/me/messages/$MSG_ID/modify"
247
+ ```
248
+
249
+ The `modify` endpoint takes `addLabelIds` and `removeLabelIds`
250
+ together — useful for atomic "archive + label" moves. Use the same
251
+ shape on `/threads/$THREAD_ID/modify` to apply across a whole thread.
252
+
253
+ ### Apply a custom label
254
+
255
+ ```sh
256
+ # 1. find or remember the label id from labels.list
257
+ LABEL_ID='Label_4'
258
+ MSG_ID='18f1a2b3c4d5e6f0'
259
+
260
+ curl -sS -X POST -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
261
+ -H 'Content-Type: application/json' \
262
+ --data "{\"addLabelIds\":[\"$LABEL_ID\"]}" \
263
+ "https://gmail.googleapis.com/gmail/v1/users/me/messages/$MSG_ID/modify"
264
+ ```
265
+
266
+ Creating a brand-new label needs the same scope:
267
+
268
+ ```sh
269
+ curl -sS -X POST -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
270
+ -H 'Content-Type: application/json' \
271
+ --data '{"name":"Follow up","messageListVisibility":"show","labelListVisibility":"labelShow"}' \
272
+ "https://gmail.googleapis.com/gmail/v1/users/me/labels" \
273
+ | jq '{id, name}'
274
+ ```
275
+
276
+ ### Trash a message or thread
277
+
278
+ ```sh
279
+ MSG_ID='18f1a2b3c4d5e6f0'
280
+ curl -sS -X POST -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
281
+ "https://gmail.googleapis.com/gmail/v1/users/me/messages/$MSG_ID/trash"
282
+
283
+ # Whole thread:
284
+ THREAD_ID='18f1a2b3c4d5e6f0'
285
+ curl -sS -X POST -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
286
+ "https://gmail.googleapis.com/gmail/v1/users/me/threads/$THREAD_ID/trash"
287
+ ```
288
+
289
+ Use `/untrash` (same shape) to restore. **Never** use
290
+ `messages.delete` — it permanently deletes and needs a higher scope
291
+ that we don't request.
292
+
293
+ ### Send a brand-new email (gmail.send)
294
+
295
+ Gmail wants the message as a base64url-encoded RFC 2822 string.
296
+
297
+ ```sh
298
+ # Compose the message
299
+ TO='alice@example.com'
300
+ SUBJECT='Quick hello'
301
+ BODY='Hi Alice,
302
+
303
+ Just a quick test note from the AceDataCloud Gmail connector.
304
+
305
+ Best,
306
+ Qingcai'
307
+
308
+ # Multi-line subject lines need MIME encoded-word for non-ASCII; ASCII is fine raw.
309
+ 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' \
310
+ "$TO" "$SUBJECT" "$BODY" \
311
+ | base64 | tr -d '\n' | tr '+/' '-_' | tr -d '=')
312
+
313
+ curl -sS -X POST -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
314
+ -H 'Content-Type: application/json' \
315
+ --data "{\"raw\":\"$RAW\"}" \
316
+ "https://gmail.googleapis.com/gmail/v1/users/me/messages/send" \
317
+ | jq '{id, threadId, labelIds}'
318
+ ```
319
+
320
+ For non-ASCII subjects (Chinese / emoji), use MIME encoded-word:
321
+
322
+ ```sh
323
+ SUBJECT_RAW='你好,季度复盘草稿'
324
+ SUBJECT_ENCODED="=?UTF-8?B?$(printf %s "$SUBJECT_RAW" | base64)?="
325
+ ```
326
+
327
+ ### Reply in-thread (keeps the thread together)
328
+
329
+ Reply by setting the `In-Reply-To` and `References` headers to the
330
+ Message-Id of the message you're replying to, **and** pass the
331
+ Gmail thread id in the API body:
332
+
333
+ ```sh
334
+ ORIG_MSG_ID='18f1a2b3c4d5e6f0'
335
+ ORIG=$(curl -sS -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
336
+ --get "https://gmail.googleapis.com/gmail/v1/users/me/messages/$ORIG_MSG_ID" \
337
+ --data-urlencode 'format=metadata' \
338
+ --data-urlencode 'metadataHeaders=Message-ID' \
339
+ --data-urlencode 'metadataHeaders=Subject' \
340
+ --data-urlencode 'metadataHeaders=From')
341
+ MID=$(echo "$ORIG" | jq -r '.payload.headers | from_entries | .["Message-ID"] // .["Message-Id"]')
342
+ FROM=$(echo "$ORIG" | jq -r '.payload.headers | from_entries | .From')
343
+ SUBJ=$(echo "$ORIG" | jq -r '.payload.headers | from_entries | .Subject')
344
+ TID=$(echo "$ORIG" | jq -r .threadId)
345
+
346
+ 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' \
347
+ "$FROM" "$SUBJ" "$MID" "$MID" \
348
+ 'Replying inline — will follow up later today.' \
349
+ | base64 | tr -d '\n' | tr '+/' '-_' | tr -d '=')
350
+
351
+ curl -sS -X POST -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
352
+ -H 'Content-Type: application/json' \
353
+ --data "{\"raw\":\"$RAW\",\"threadId\":\"$TID\"}" \
354
+ "https://gmail.googleapis.com/gmail/v1/users/me/messages/send" \
355
+ | jq '{id, threadId}'
356
+ ```
357
+
358
+ Without the `threadId` in the body Gmail starts a brand-new thread
359
+ even with the right `In-Reply-To` headers.
360
+
361
+ ### Save a draft instead of sending
362
+
363
+ Same `raw` payload, different endpoint — still costs `gmail.send`
364
+ (`drafts` shares the send scope under the hood for write):
365
+
366
+ ```sh
367
+ curl -sS -X POST -H "Authorization: Bearer $GOOGLE_GMAIL_TOKEN" \
368
+ -H 'Content-Type: application/json' \
369
+ --data "{\"message\":{\"raw\":\"$RAW\"}}" \
370
+ "https://gmail.googleapis.com/gmail/v1/users/me/drafts" \
371
+ | jq '{id, message: {id: .message.id, threadId: .message.threadId}}'
372
+ ```
373
+
204
374
  ## Common error codes
205
375
 
206
376
  | HTTP | meaning | what to tell the user |
207
377
  |---|---|---|
208
378
  | `401 UNAUTHENTICATED` | token expired / revoked | "Reconnect the Gmail connector on the Connections page." |
209
- | `403 insufficientPermissions` | scope missing | "This connector grants only read access modifying mail isn't possible." |
379
+ | `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
380
  | `403 userRateLimitExceeded` / `429` | quota / throttling | back off ~5s, then retry once. |
211
381
  | `404 notFound` | wrong message / thread / attachment id | double-check the id, or fall back to `messages.list` with the right query. |
212
382
  | `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, or grouping todos by list.
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 Tasks — list
6
- task lists, surface pending items, group by due date, or pull
7
- details for one task. The installed connector grants read-only
8
- scope (`tasks.readonly`); creating / updating / deleting tasks is
9
- out of scope.
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.0"
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`. The token already
21
- carries the `tasks.readonly` scope the user agreed to at install plus
22
- the identity scopes (`openid email profile`).
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 is asking for a write this
28
- connector cannot satisfy say so.
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 connector is read-only adding or completing tasks isn't possible." |
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