Skip to content

Commit ca91c36

Browse files
dylburgerclaudemichelle0927
authored
feat(gmail): eval-driven fixes to the AI-optimized Gmail actions (PipedreamHQ#21491)
* feat(gmail): eval-driven fixes to the AI-optimized Gmail actions Iterated against the MCP eval suite (pd-connect-eval-monster/evals/gmail); this ships the fixes those evals surfaced. Confirming run: 29/31 pass^2 on Sonnet 5 (multi-step 12/12), and a Haiku 4.5 + Sonnet 5 sweep flags no failure as a tool problem — every remaining failure passes on at least one model. - send-email [patch]: eval #16 ("reply so both messages stay in one conversation") returned HTTP 400 "Recipient address required" on EVERY reply that passed inReplyToMessageId without an explicit `to`. getOptionsToSendEmail only populated opts.to on the replyAll branch, so the documented "only the original sender is addressed" behavior was never implemented. Now defaults to the replied-to message's Reply-To/From. Also covers a replyAll self-thread case where filtering the user's own address left zero recipients, and stops a missing To/Cc header throwing. #16 now passes. - create-draft [patch]: same defect via the shared app file. #17 ("draft a reply to Hammond") now passes. - find-email [minor]: eval #23 ("catch me up on this week's mail and email me the summary") produced a confidently wrong digest — it described an offline fence sensor as healthy. The agent had correctly asked for format:"full" plus payload; the 42k-char response exceeded the MCP client's 25k-token result ceiling and was spilled to a file, and the in-tool budget then stripped the requested field, leaving 197-char snippets. The needed sentence sits at character 670 of a 934-character body, so no snippet could contain it and nothing signalled that it had been cut. Adds `bodyText` (decoded plain text, ~half the size of raw payload), a `bodyChars` cap with bodyTruncated / bodyTotalChars, snippetTruncated on metadata results, lowers the response cap to 30k chars, and inverts the budget rule: a field the caller explicitly named is never stripped — whole messages are dropped instead and the note says how many of how many are shown. #23 now passes; largest tool result is 45% of the ceiling. All new props are optional; omitting them reproduces prior output. - list-thread-messages [minor]: same result-ceiling risk; response cap lowered to 30k chars. It already degraded full -> metadata -> tail, so it needed the cap rather than a fields projection. - modify-labels [patch]: three description defects the evals surfaced — the agent skipped the tool in 20% of tasks that required it, trashed an entire label's worth of mail with no confirmation (PipedreamHQ#31), and believed it could create filter rules for future mail (#27). Adds "use this whenever the user says star / archive / file / label / trash / mark read", a trash-is-destructive confirmation rule, and a "not for filters or future mail" exclusion. Tool recall across the suite went 0.88 -> 1.00. App package.json bumped 2.1.0 -> 2.2.0 (largest segment applied: minor). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(gmail): bump every component that bundles the changed shared code The "Ensure component commits modify component versions" check flagged this and it is correct: gmail.app.mjs and common/utils.mjs are bundled into every gmail component at publish time, so each one has to be revved for the registry to pick up the new code — even where its own behavior is unchanged. Patch bump for the 12 actions and 5 sources that were not already versioned in the previous commit. No functional change in any of them; the reply-recipient fix in gmail.app.mjs only reaches send-email and create-draft (already bumped), and the utils additions are new functions with no existing call sites touched. package.json stays at 2.2.0 — the largest applied segment is still the minor bump from find-email / list-thread-messages. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(gmail): address review findings on the eval-driven changes - find-email: requesting `fields: ["payload"]` without also setting `format: "full"` fetched metadata, whose payload carries only headers — no `parts`, no `body.attachmentId`. The caller got a payload-shaped object with none of the attachment data it asked for. `fetchFull` now keys off `wantsPayload`, so either route to a payload forces the full fetch. Verified: that call now fetches full and returns the attachment id; default, `bodyText`, and `format: "full"` paths are unchanged. - gmail.app.mjs: a present-but-blank `Reply-To` defeated the reply recipient fallback. `??` only guards null/undefined, so an empty header left originalSender = "" and the fallback dead — reintroducing the HTTP 400 "Recipient address required" that fallback exists to prevent. The header helper now treats blank/whitespace-only values as absent. Verified across blank, whitespace, valid, and missing Reply-To. - gmail.app.mjs: reply-all Cc'd the authenticated user back on their own reply. `opts.to` had a self-filter, `opts.cc` did not. Extracted the existing filter and applied it to both. - utils.mjs: MAX_RESPONSE_CHARS was declared identically in find-email and list-thread-messages. It is a property of the MCP client, not of either action, so both copies had to move in lockstep. Now exported once from common/utils.mjs and imported by both. Not changed: reply-all still propagates the original message's Bcc. That predates this PR, only fires on replies to a sent copy (Gmail strips Bcc from received mail), and removing it is a behavior change to published semantics with no eval covering either side — it belongs in its own change, not a review fix-up. Flagged for the reviewer. No version bumps: every touched component is already bumped relative to master in this PR and none of these versions has been released. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Dylan Sather <Dylan Sather> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: michelle0927 <michelle0927@users.noreply.github.com>
1 parent d872c63 commit ca91c36

25 files changed

Lines changed: 312 additions & 58 deletions

File tree

components/gmail/actions/approve-workflow/approve-workflow.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export default {
44
key: "gmail-approve-workflow",
55
name: "Approve Workflow",
66
description: "Suspend the workflow until approved by email. [See the documentation](https://pipedream.com/docs/code/nodejs/rerun#flowsuspend)",
7-
version: "0.0.16",
7+
version: "0.0.17",
88
annotations: {
99
destructiveHint: false,
1010
openWorldHint: true,

components/gmail/actions/create-draft/create-draft.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export default {
1111
+ " To draft to yourself, pass `\"me\"` in `to` — the action resolves it to the authenticated user's email address. No pre-call to **Get Current User** required."
1212
+ " Attachments use `file-ref` inputs and require matching `attachmentFilenames[]` entries."
1313
+ " [See the documentation](https://developers.google.com/gmail/api/reference/rest/v1/users.drafts/create).",
14-
version: "0.2.1",
14+
version: "0.2.2",
1515
annotations: {
1616
destructiveHint: false,
1717
openWorldHint: true,

components/gmail/actions/create-label/create-label.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export default {
1313
+ " Nested labels are expressed with `/` — e.g. `Clients/Acme` creates or targets a sub-label under `Clients`."
1414
+ " `color` is optional; when provided, both `textColor` and `backgroundColor` must be supplied together and must come from Gmail's fixed palette."
1515
+ " [See the documentation](https://developers.google.com/workspace/gmail/api/reference/rest/v1/users.labels/create).",
16-
version: "0.1.1",
16+
version: "0.1.2",
1717
annotations: {
1818
destructiveHint: false,
1919
openWorldHint: true,

components/gmail/actions/delete-label/delete-label.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export default {
88
+ " Only user-created labels can be deleted — Gmail's built-in system labels (`INBOX`, `SENT`, `SPAM`, `TRASH`, etc.) cannot."
99
+ " This deletes the label *definition* itself; to merely detach a label from specific messages without removing it, use **Modify Labels** with `removeLabels` instead."
1010
+ " [See the documentation](https://developers.google.com/workspace/gmail/api/reference/rest/v1/users.labels/delete).",
11-
version: "0.0.1",
11+
version: "0.0.2",
1212
annotations: {
1313
destructiveHint: true,
1414
openWorldHint: true,

components/gmail/actions/download-attachment/download-attachment.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export default {
1717
+ " If `filename` is omitted, the action looks up the attachment's filename from the message payload."
1818
+ " Set `convertToPdf: true` to convert image / HTML / plain-text / DOCX attachments to PDF during download; other MIME types are rejected."
1919
+ " [See the documentation](https://developers.google.com/gmail/api/reference/rest/v1/users.messages.attachments/get).",
20-
version: "0.1.1",
20+
version: "0.1.2",
2121
annotations: {
2222
destructiveHint: false,
2323
openWorldHint: true,

components/gmail/actions/find-email/find-email.mjs

Lines changed: 148 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,26 @@
1-
import utils from "../../common/utils.mjs";
1+
import utils, { MAX_RESPONSE_CHARS } from "../../common/utils.mjs";
22
import gmail from "../../gmail.app.mjs";
33

44
const DEFAULT_MAX_RESULTS = 25;
5-
const MAX_RESPONSE_CHARS = 100_000;
5+
// Applied only when a response busts the budget AND the caller named no `fields` of its
6+
// own: keeps every message but strips it to what callers reason about, so counts stay
7+
// correct. Never applied over an explicit `fields` choice — see utils.fitToBudget.
8+
const COMPACT_FIELDS = [
9+
"labelIds",
10+
"subject",
11+
"sender",
12+
"recipient",
13+
"date",
14+
"snippet",
15+
];
16+
const DEFAULT_BODY_CHARS = 2000;
17+
// Bodies shrink toward this floor before whole messages are dropped. Truncation is only
18+
// acceptable because it announces itself per message (`bodyTruncated`), so the caller can
19+
// re-fetch the one message it cares about; below this a body is too clipped to reason from.
20+
const MIN_BODY_CHARS = 400;
21+
// Gmail snippets are a fixed ~200-char prefix. At this length the body is longer than
22+
// what was returned, and the caller cannot tell that from the snippet alone.
23+
const SNIPPET_TRUNCATED_AT = 190;
624
const METADATA_HEADERS = [
725
"From",
826
"To",
@@ -21,10 +39,14 @@ export default {
2139
+ " The `q` parameter accepts the full Gmail search operator set — combine operators freely: `from:alan@ingen.com is:unread newer_than:7d has:attachment subject:\"DNA sequences\"`. Common operators: `from:`, `to:`, `subject:`, `has:attachment`, `filename:pdf`, `is:unread`, `is:starred`, `label:INBOX`, `newer_than:7d`, `older_than:1m`, `after:2025/01/01`, `before:2025/12/31`, `category:primary`."
2240
+ " `labelIds` accepts either raw label IDs (`INBOX`, `STARRED`) or user-visible names (`Clients/Acme`) — names are resolved server-side via **List Labels**."
2341
+ " Each returned message carries `id`, `threadId`, `labelIds`, the decoded `subject`/`sender`/`recipient`/`date`, and a `snippet`. With `format: \"full\"` the decoded body text and `payload.parts[].body.attachmentId` + `filename` + `mimeType` are also included — feed those into **Download Attachment**, or feed `threadId` into **Get Thread** for the whole conversation."
24-
+ " `format` defaults to `metadata` (headers + snippet only) to keep the response small; set it to `full` only when you actually need body text or attachment IDs."
25-
+ " Responses are hard-capped at 100k characters — anything beyond is truncated with a `[truncated]` marker so the caller knows to narrow the query."
42+
+ " **Set `fields` on every call** to name just what you need — message records are large, and a wide search returns tens of thousands of characters that crowd out the rest of the task. `id` and `threadId` are always returned."
43+
+ " To BROWSE or COUNT, use `[\"subject\", \"sender\", \"date\"]`. **To READ or SUMMARISE — \"catch me up\", \"what did X say about Y\", \"is there anything I need to reply to\" — use `[\"subject\", \"sender\", \"date\", \"bodyText\"]`.**"
44+
+ " `bodyText` is the decoded plain-text body (HTML converted, MIME scaffolding and attachments stripped), about half the size of the raw `payload`, and requesting it fetches full messages for you."
45+
+ " **Never answer a question about what an email SAYS from `snippet`** — it is a fixed ~200-character prefix, so the sentence you need is usually past its end, and nothing in a snippet indicates that it was cut. Where the snippet is all you asked for and content was likely cut, the message carries `snippetTruncated: true`."
46+
+ " `format` stays `metadata` unless you need the raw MIME tree for **Download Attachment**, in which case pass `format: \"full\"` and request `payload`."
47+
+ " Responses are capped. Over the cap: if you named `fields`, whole messages are dropped rather than your chosen fields being removed, and the note says how many of how many are shown — narrow `q` and retry. If you named none, messages are compacted instead so counts stay accurate. `bodyText` shrinks toward a floor before anything is dropped, flagging each cut message with `bodyTruncated: true`."
2648
+ " [See the documentation](https://developers.google.com/gmail/api/reference/rest/v1/users.messages/list) and [Gmail search operators](https://support.google.com/mail/answer/7190).",
27-
version: "0.2.1",
49+
version: "0.3.0",
2850
annotations: {
2951
destructiveHint: false,
3052
openWorldHint: true,
@@ -77,6 +99,30 @@ export default {
7799
default: "metadata",
78100
optional: true,
79101
},
102+
fields: {
103+
type: "string[]",
104+
label: "Fields",
105+
description:
106+
"Return only these fields on each message, instead of the full record. **Always set this** unless you genuinely need every field — message records are large, and a wide search can return tens of thousands of characters that crowd out the rest of the task."
107+
+ " `id` and `threadId` are always included so results can be fed into **Get Thread**, **Modify Labels**, or **Download Attachment**."
108+
+ "\n\nChoose by what you are doing:"
109+
+ "\n- **Browsing, counting, \"which emails…\"** → `[\"subject\", \"sender\", \"date\"]`. Add `labelIds` to check read/starred state."
110+
+ "\n- **Reading, summarising, \"catch me up\", \"what did X say\"** → `[\"subject\", \"sender\", \"date\", \"bodyText\"]`. **`bodyText` is a derived field**: the decoded plain-text body, HTML converted, attachments and MIME scaffolding stripped. It is roughly half the size of `payload` and is what you want for any question about what an email SAYS. Requesting it fetches full messages automatically — you do not also need `format: \"full\"`."
111+
+ "\n- **Downloading an attachment** → `[\"payload\"]` with `format: \"full\"`, then read `payload.parts[].body.attachmentId`."
112+
+ "\n\nDo NOT try to answer a content question from `snippet`: it is a fixed ~200-character prefix, so the sentence you need is usually past its end, and a snippet gives no sign that anything was cut. Ask for `bodyText` instead."
113+
+ " Omit `fields` entirely to return the complete message record, exactly as this tool has always done.",
114+
optional: true,
115+
},
116+
bodyChars: {
117+
type: "integer",
118+
label: "Body Characters",
119+
description:
120+
`Maximum characters of \`bodyText\` to return per message. Default ${DEFAULT_BODY_CHARS}, which covers a normal one-page email in full.`
121+
+ " Only applies when `bodyText` is requested in `fields`. Any message whose body is cut comes back with `bodyTruncated: true` and `bodyTotalChars` (its real length) — re-fetch that single message with a higher limit, or use **Get Thread**, to read the rest."
122+
+ " Raise it for long newsletters or threads; lower it to fit more messages in one response.",
123+
optional: true,
124+
default: DEFAULT_BODY_CHARS,
125+
},
80126
},
81127
async run({ $ }) {
82128
let labelIds = this.labelIds;
@@ -95,23 +141,35 @@ export default {
95141
}
96142

97143
const format = this.format || "metadata";
144+
const fields = this.fields;
145+
// `bodyText` is derived from the decoded body, which only a full fetch carries, so
146+
// asking for it upgrades the fetch. The caller shouldn't have to know that.
147+
const wantsBodyText = Boolean(fields?.includes("bodyText"));
148+
const wantsPayload = format === "full" || Boolean(fields?.includes("payload"));
149+
// Asking for `payload` has to force a full fetch too. A metadata fetch returns a
150+
// payload carrying only headers — no `parts`, no `body.attachmentId` — so a caller
151+
// that requested `fields: ["payload"]` without also setting `format: "full"` would
152+
// get a payload-shaped object with none of the attachment data it asked for.
153+
const fetchFull = wantsBodyText || wantsPayload;
154+
98155
const { messages = [] } = await this.gmail.listMessages({
99156
q: this.q,
100157
labelIds,
101158
includeSpamTrash: this.includeSpamTrash,
102159
maxResults: this.maxResults ?? DEFAULT_MAX_RESULTS,
103160
});
104161

105-
const getOpts = format === "metadata"
162+
const getOpts = fetchFull
106163
? {
107-
format: "metadata",
108-
metadataHeaders: METADATA_HEADERS,
164+
format: "full",
109165
}
110166
: {
111-
format: "full",
167+
format: "metadata",
168+
metadataHeaders: METADATA_HEADERS,
112169
};
113170

114171
const results = [];
172+
const fullBodies = new Map();
115173
for await (const message of this.gmail.getAllMessages(messages.map(({ id }) => id), getOpts)) {
116174
const headers = message.payload?.headers ?? [];
117175
const messageIdHeader = utils.getHeader(headers, "message-id");
@@ -125,41 +183,108 @@ export default {
125183
message.sender = utils.getHeader(headers, "from");
126184
message.recipient = utils.getHeader(headers, "to");
127185

128-
if (format === "metadata") {
129-
delete message.payload;
130-
} else {
186+
if (fetchFull) {
131187
if (message.payload?.body?.data && !Array.isArray(message.payload.parts)) {
132188
message.payload.body.text = utils.decodeBase64Url(message.payload.body.data);
133189
}
134190
if (Array.isArray(message.payload?.parts)) {
135191
utils.attachTextToParts(message.payload.parts);
136192
}
193+
if (wantsBodyText) {
194+
fullBodies.set(message.id, utils.getBodyText(message.payload));
195+
}
137196
}
197+
// Drop the payload unless it was actually asked for — a `bodyText` request that
198+
// had to fetch full messages must not return the MIME tree as a side effect.
199+
if (!wantsPayload) {
200+
delete message.payload;
201+
}
202+
// `format: "full"` is a request the pluck must honor.
203+
const always = [
204+
"id",
205+
"threadId",
206+
...wantsPayload
207+
? [
208+
"payload",
209+
]
210+
: [],
211+
];
212+
const plucked = utils.pluckFields(message, fields, always);
138213

139-
results.push(message);
214+
// A ~200-char snippet with no body alongside it is the shape that produces
215+
// confidently wrong summaries: the caller cannot tell that the sentence it needs
216+
// was cut off. Say so. Flagged after the pluck so it appears exactly when the
217+
// snippet actually survives into the output, and skipped when a body is present
218+
// (then it's noise).
219+
if (!wantsBodyText && !wantsPayload
220+
&& (plucked.snippet?.length ?? 0) >= SNIPPET_TRUNCATED_AT) {
221+
plucked.snippetTruncated = true;
222+
}
223+
results.push(plucked);
140224
}
141225

142-
let kept = results;
143-
let truncated = false;
144-
while (kept.length > 0 && JSON.stringify(kept).length > MAX_RESPONSE_CHARS) {
145-
kept = kept.slice(0, -1);
146-
truncated = true;
226+
// Bodies were requested and the response is too big: shrink them toward a floor
227+
// BEFORE dropping whole messages, because a per-message `bodyTruncated` flag is
228+
// recoverable (re-fetch that one message) while a missing message is not.
229+
let bodyCap = this.bodyChars ?? DEFAULT_BODY_CHARS;
230+
if (wantsBodyText) {
231+
const applyCap = (cap) => {
232+
for (const message of results) {
233+
const body = fullBodies.get(message.id) ?? "";
234+
message.bodyText = body.slice(0, cap);
235+
if (body.length > cap) {
236+
message.bodyTruncated = true;
237+
// Deliberately NOT `bodyChars` — that is the INPUT prop (the cap). This is
238+
// the body's true length, so the caller can see how much it is missing.
239+
message.bodyTotalChars = body.length;
240+
} else {
241+
delete message.bodyTruncated;
242+
delete message.bodyTotalChars;
243+
}
244+
}
245+
};
246+
applyCap(bodyCap);
247+
while (JSON.stringify(results).length > MAX_RESPONSE_CHARS && bodyCap > MIN_BODY_CHARS) {
248+
bodyCap = Math.max(MIN_BODY_CHARS, Math.floor(bodyCap / 2));
249+
applyCap(bodyCap);
250+
}
147251
}
148-
const payload = truncated
252+
253+
const {
254+
messages: kept, compacted, dropped,
255+
} = utils.fitToBudget(results, MAX_RESPONSE_CHARS, {
256+
compactFields: COMPACT_FIELDS,
257+
callerChoseFields: Boolean(fields?.length),
258+
});
259+
260+
const shrunk = wantsBodyText && bodyCap < (this.bodyChars ?? DEFAULT_BODY_CHARS);
261+
const payload = (compacted || dropped || shrunk)
149262
? {
150263
messages: kept,
264+
matchCount: results.length,
151265
truncated: true,
152-
note: `[truncated] response exceeded ${MAX_RESPONSE_CHARS} chars — dropped ${results.length - kept.length} of ${results.length} messages from the tail. Narrow the query, lower maxResults, or keep format:"metadata".`,
266+
note: `[truncated] response exceeded ${MAX_RESPONSE_CHARS} chars. `
267+
+ `${compacted
268+
? `Every message was reduced to ${COMPACT_FIELDS.join(", ")}; all ${results.length} matches are still listed, so counts remain accurate. `
269+
: ""}`
270+
+ `${shrunk
271+
? `\`bodyText\` was capped at ${bodyCap} chars per message (messages cut this way carry \`bodyTruncated: true\`). `
272+
: ""}`
273+
+ `${dropped
274+
? `${dropped} of ${results.length} messages were dropped from the tail — THIS LIST IS INCOMPLETE, ${kept.length} of ${results.length} matches are shown. Narrow \`q\`, lower \`maxResults\`, or request fewer fields, then retry. `
275+
: ""}`,
153276
}
154277
: results;
155278

156279
$.export("$summary", `Found ${results.length} message${results.length === 1
157280
? ""
158281
: "s"}${this.q
159282
? ` matching "${this.q}"`
160-
: ""}${truncated
161-
? ` (truncated to ${kept.length})`
162-
: ""}`);
283+
: ""}${dropped
284+
? ` (showing ${kept.length}, ${dropped} dropped to fit)`
285+
: (compacted || shrunk)
286+
? " (trimmed to fit)"
287+
: ""}`);
163288
return payload;
164289
},
165290
};

components/gmail/actions/get-current-user/get-current-user.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export default {
44
key: "gmail-get-current-user",
55
name: "Get Current User",
66
description: "Returns the authenticated Gmail user's name, email address, and mailbox stats (total messages and threads). Call this first when the user says 'my emails', 'my inbox', or needs identity context. Use the returned `emailAddress` to identify the user's own messages in **Find Emails** results. [See the documentation](https://developers.google.com/gmail/api/reference/rest/v1/users/getProfile).",
7-
version: "0.0.3",
7+
version: "0.0.4",
88
type: "action",
99
annotations: {
1010
destructiveHint: false,

components/gmail/actions/get-send-as-alias/get-send-as-alias.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export default {
55
key: "gmail-get-send-as-alias",
66
name: "Get Send As Alias",
77
description: "Get a send as alias for the authenticated user. [See the documentation](https://developers.google.com/workspace/gmail/api/reference/rest/v1/users.settings.sendAs/get)",
8-
version: "0.0.7",
8+
version: "0.0.8",
99
annotations: {
1010
destructiveHint: false,
1111
openWorldHint: true,

components/gmail/actions/list-delegate-options/list-delegate-options.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export default {
44
key: "gmail-list-delegate-options",
55
name: "List Send as a Delegate Options",
66
description: "Retrieves available options for the Send as a Delegate field.",
7-
version: "0.0.3",
7+
version: "0.0.4",
88
type: "action",
99
annotations: {
1010
destructiveHint: false,

components/gmail/actions/list-labels/list-labels.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export default {
88
+ " Call this before **Modify Labels** or **Find Emails** when you need to target a label that the user named rather than an obvious system label — it resolves a name like `Clients/Acme` to its opaque label ID."
99
+ " User labels are returned first, then system labels."
1010
+ " [See the documentation](https://developers.google.com/gmail/api/reference/rest/v1/users.labels/list).",
11-
version: "0.1.1",
11+
version: "0.1.2",
1212
annotations: {
1313
destructiveHint: false,
1414
openWorldHint: true,

0 commit comments

Comments
 (0)