@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.504.4",
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, 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.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`. 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
+
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 connector is read-only creating or modifying events isn't possible." |
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 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.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`. 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.
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 | "Your installed connector only grants read access this action needs a write scope we don't have." |
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 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.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`. 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
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 | "This connector grants only read access modifying mail isn't possible." |
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, 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
 
@@ -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 sandbox environment (auto-injected by aichat2 from the user's
21
- AceDataCloud BYOC connection; or set manually in `.env`).
20
+ from the environment.
22
21
  """
23
22
 
24
23
  from __future__ import annotations