gogcli-mcp-gmail 2.25.0 → 2.27.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/dist/index.js CHANGED
@@ -31000,9 +31000,87 @@ var StdioServerTransport = class {
31000
31000
  }
31001
31001
  };
31002
31002
 
31003
+ // ../../node_modules/@chrischall/mcp-utils/dist/errors/index.js
31004
+ var McpToolError = class extends Error {
31005
+ /** Actionable remediation text, when one applies. */
31006
+ hint;
31007
+ constructor(message, opts) {
31008
+ super(message, opts?.cause !== void 0 ? { cause: opts.cause } : void 0);
31009
+ this.name = "McpToolError";
31010
+ if (opts?.hint !== void 0)
31011
+ this.hint = opts.hint;
31012
+ Object.setPrototypeOf(this, new.target.prototype);
31013
+ }
31014
+ };
31015
+ var BEARER_RE = /(bearer\s+)[A-Za-z0-9._~+/=-]{8,}/gi;
31016
+ var JWT_RE = /\b[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{8,}\b/g;
31017
+ var BASIC_AUTH_RE = /(authorization\s*:\s*basic\s+)[A-Za-z0-9+/=_-]{6,}/gi;
31018
+ var SET_COOKIE_RE = /(\bset-cookie\s*:\s*)([^=;,\s]+)=[^;,\s]*/gi;
31019
+ var COOKIE_HEADER_RE = /((?<!set-)\bcookie\s*:\s*)((?:[^=;,\s]+=[^;,\s]*)(?:;\s*[^=;,\s]+=[^;,\s]*)*)/gi;
31020
+ var API_KEY_RE = new RegExp([
31021
+ "sk-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])",
31022
+ // OpenAI / Anthropic (incl. sk-ant-…)
31023
+ "gh[pousr]_[A-Za-z0-9]{36,}\\b",
31024
+ // GitHub ghp_/gho_/ghu_/ghs_/ghr_
31025
+ "xox[baprs]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])",
31026
+ // Slack
31027
+ "AIza[0-9A-Za-z_-]{35}(?![0-9A-Za-z_-])",
31028
+ // Google API key (39 chars total)
31029
+ "AKIA[0-9A-Z]{16}\\b",
31030
+ // AWS access key id (20 chars total)
31031
+ "whsec_[A-Za-z0-9]{16,}\\b"
31032
+ // webhook signing secret (Stripe-style)
31033
+ ].map((p) => `\\b${p}`).join("|"), "g");
31034
+ var QUERY_SECRET_RE = /([?&](?:access_token|refresh_token|client_secret|api_?key|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
31035
+ var AWS_SIGV4_RE = /([?&]X-Amz-(?:Signature|Security-Token|Credential)=)[^&#\s"'<>`]+/gi;
31036
+ var JSON_SECRET_KEYS = "access_token|refresh_token|client_secret|api_?key|password|passwd|secret|token";
31037
+ var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")[^"]*(")`, "gi");
31038
+ var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')[^']*(')`, "gi");
31039
+ function redactSecrets(text) {
31040
+ return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JWT_RE, "[REDACTED]");
31041
+ }
31042
+
31043
+ // ../../node_modules/@chrischall/mcp-utils/dist/response/index.js
31044
+ function textResult(data) {
31045
+ return {
31046
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
31047
+ };
31048
+ }
31049
+ function rawTextResult(text) {
31050
+ return { content: [{ type: "text", text }] };
31051
+ }
31052
+ function errorResult(message) {
31053
+ return {
31054
+ content: [{ type: "text", text: redactSecrets(message) }],
31055
+ isError: true
31056
+ };
31057
+ }
31058
+
31003
31059
  // ../../node_modules/@chrischall/mcp-utils/dist/server/index.js
31060
+ function hintResultOrRethrow(err) {
31061
+ if (err instanceof McpToolError && err.hint) {
31062
+ return errorResult(`${err.message}
31063
+
31064
+ Hint: ${err.hint}`);
31065
+ }
31066
+ throw err;
31067
+ }
31068
+ function surfaceToolHints(server) {
31069
+ const register = server.registerTool.bind(server);
31070
+ server.registerTool = (name, config2, cb) => register(name, config2, (...args) => {
31071
+ let result;
31072
+ try {
31073
+ result = cb(...args);
31074
+ } catch (err) {
31075
+ return hintResultOrRethrow(err);
31076
+ }
31077
+ return result instanceof Promise ? result.catch(hintResultOrRethrow) : result;
31078
+ });
31079
+ }
31004
31080
  async function createMcpServer(opts) {
31005
31081
  const server = new McpServer({ name: opts.name, version: opts.version });
31082
+ if (opts.surfaceHints !== false)
31083
+ surfaceToolHints(server);
31006
31084
  if (opts.banner !== void 0) {
31007
31085
  console.error(opts.banner);
31008
31086
  }
@@ -31047,51 +31125,6 @@ async function runMcp(opts) {
31047
31125
  return server;
31048
31126
  }
31049
31127
 
31050
- // ../../node_modules/@chrischall/mcp-utils/dist/errors/index.js
31051
- var BEARER_RE = /(bearer\s+)[A-Za-z0-9._~+/=-]{8,}/gi;
31052
- var JWT_RE = /\b[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{8,}\b/g;
31053
- var BASIC_AUTH_RE = /(authorization\s*:\s*basic\s+)[A-Za-z0-9+/=_-]{6,}/gi;
31054
- var SET_COOKIE_RE = /(\bset-cookie\s*:\s*)([^=;,\s]+)=[^;,\s]*/gi;
31055
- var COOKIE_HEADER_RE = /((?<!set-)\bcookie\s*:\s*)((?:[^=;,\s]+=[^;,\s]*)(?:;\s*[^=;,\s]+=[^;,\s]*)*)/gi;
31056
- var API_KEY_RE = new RegExp([
31057
- "sk-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])",
31058
- // OpenAI / Anthropic (incl. sk-ant-…)
31059
- "gh[pousr]_[A-Za-z0-9]{36,}\\b",
31060
- // GitHub ghp_/gho_/ghu_/ghs_/ghr_
31061
- "xox[baprs]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])",
31062
- // Slack
31063
- "AIza[0-9A-Za-z_-]{35}(?![0-9A-Za-z_-])",
31064
- // Google API key (39 chars total)
31065
- "AKIA[0-9A-Z]{16}\\b",
31066
- // AWS access key id (20 chars total)
31067
- "whsec_[A-Za-z0-9]{16,}\\b"
31068
- // webhook signing secret (Stripe-style)
31069
- ].map((p) => `\\b${p}`).join("|"), "g");
31070
- var QUERY_SECRET_RE = /([?&](?:access_token|refresh_token|client_secret|api_?key|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
31071
- var AWS_SIGV4_RE = /([?&]X-Amz-(?:Signature|Security-Token|Credential)=)[^&#\s"'<>`]+/gi;
31072
- var JSON_SECRET_KEYS = "access_token|refresh_token|client_secret|api_?key|password|passwd|secret|token";
31073
- var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")[^"]*(")`, "gi");
31074
- var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')[^']*(')`, "gi");
31075
- function redactSecrets(text) {
31076
- return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JWT_RE, "[REDACTED]");
31077
- }
31078
-
31079
- // ../../node_modules/@chrischall/mcp-utils/dist/response/index.js
31080
- function textResult(data) {
31081
- return {
31082
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
31083
- };
31084
- }
31085
- function rawTextResult(text) {
31086
- return { content: [{ type: "text", text }] };
31087
- }
31088
- function errorResult(message) {
31089
- return {
31090
- content: [{ type: "text", text: redactSecrets(message) }],
31091
- isError: true
31092
- };
31093
- }
31094
-
31095
31128
  // ../../node_modules/@chrischall/mcp-utils/dist/config/index.js
31096
31129
  var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
31097
31130
  function readEnvVar(key, opts = {}) {
@@ -31776,6 +31809,13 @@ function formatAuthHealth(raw, now) {
31776
31809
  }
31777
31810
  return accounts.map((a) => formatOneAccountHealth(a, now)).join("\n\n");
31778
31811
  }
31812
+ function assertNotBoth(inlineParam, fileParam, inlineValue, fileValue) {
31813
+ if (inlineValue !== void 0 && fileValue !== void 0) {
31814
+ throw new Error(
31815
+ `${inlineParam} and ${fileParam} are mutually exclusive \u2014 gog accepts only one of them. Pass ${inlineParam} with the content itself (it is written to a temp file automatically when large), or ${fileParam} with a path that already exists on the gog server.`
31816
+ );
31817
+ }
31818
+ }
31779
31819
 
31780
31820
  // ../gogcli-mcp/src/tools/auth.ts
31781
31821
  function registerAuthToolsWith(server, defaultServices) {
@@ -31908,6 +31948,95 @@ function authToolsFor(defaultServices) {
31908
31948
  return (server) => registerAuthToolsWith(server, defaultServices);
31909
31949
  }
31910
31950
 
31951
+ // ../gogcli-mcp/src/tools/calendar.ts
31952
+ var reminderParams = {
31953
+ reminders: external_exports.array(external_exports.string()).max(5).optional().describe(
31954
+ `Reminders as method:duration, e.g. ["popup:30m", "email:1d"]. Method is popup or email; duration accepts m/h/d (max 40320 minutes = 4 weeks). Google allows at most 5. These REPLACE the event's reminders \u2014 on update, pass an EMPTY array to drop custom reminders and go back to the calendar's defaults. Cannot be combined with noReminders.`
31955
+ ),
31956
+ noReminders: external_exports.boolean().optional().describe(
31957
+ "Give the event no reminders at all, overriding the calendar's defaults. Different from an empty reminders array, which RESTORES those defaults. Cannot be combined with reminders."
31958
+ )
31959
+ };
31960
+
31961
+ // ../gogcli-mcp/src/attachments.ts
31962
+ var MAX_INLINE_ATTACHMENT_BYTES = 8 * 1024 * 1024;
31963
+ var RUNNER_MAX_BODY_BYTES = 32 * 1024 * 1024;
31964
+ var RUNNER_BODY_JSON_RESERVE_BYTES = 256 * 1024;
31965
+ var MAX_REQUEST_PAYLOAD_WIRE_BYTES = RUNNER_MAX_BODY_BYTES - RUNNER_BODY_JSON_RESERVE_BYTES;
31966
+ var MAX_INLINE_ATTACHMENT_TOTAL_BYTES = Math.floor(MAX_REQUEST_PAYLOAD_WIRE_BYTES * 3 / 4);
31967
+ function wireBytesOf(arg) {
31968
+ if (typeof arg === "string") return Buffer.byteLength(arg, "utf8");
31969
+ return arg.encoding === "base64" ? arg.contents.length : Buffer.byteLength(arg.contents, "utf8");
31970
+ }
31971
+ var formatMiB = (bytes) => `${Math.floor(bytes / (1024 * 1024))} MiB`;
31972
+ var INLINE_ATTACHMENT_LIMITS_TEXT = `up to ${formatMiB(MAX_INLINE_ATTACHMENT_BYTES)} per file and ${formatMiB(MAX_INLINE_ATTACHMENT_TOTAL_BYTES)} in total`;
31973
+ var inlineAttachmentSchema = external_exports.object({
31974
+ filename: external_exports.string().min(1).describe(
31975
+ `Filename the recipient will see, e.g. "pendant-layouts.png". gog infers the attachment's MIME type from this extension, so give it the right one \u2014 a .png sent as "layouts" arrives as an untyped blob. Must be a single filename, not a path.`
31976
+ ),
31977
+ contentBase64: external_exports.string().min(1).describe(
31978
+ "The file's bytes, base64-encoded (standard alphabet, with padding). This is the whole point of this parameter: the bytes travel with the request, so nothing needs to exist on the gog server's filesystem."
31979
+ )
31980
+ });
31981
+ var attachInlineParam = external_exports.array(inlineAttachmentSchema).optional().describe(
31982
+ `Attachments supplied as BYTES rather than as server-side paths \u2014 use this whenever you hold a file and the gog server does not, which is always the case on the hosted connector and on any remote deployment. Each entry is {filename, contentBase64} (${INLINE_ATTACHMENT_LIMITS_TEXT}). Can be combined with \`attach\`: the two name disjoint files (paths read on the server vs. bytes sent with the call), and both end up as ordinary attachments on the message.`
31983
+ );
31984
+ function validateFilename(filename, where) {
31985
+ if (/[/\\]/.test(filename)) {
31986
+ throw new Error(
31987
+ `${where}: filename ${JSON.stringify(filename)} must be a bare filename, not a path. Pass just the name the recipient should see, e.g. "report.pdf".`
31988
+ );
31989
+ }
31990
+ if (/[\x00-\x1f]/.test(filename) || /^\.+$/.test(filename) || filename.length > 200) {
31991
+ throw new Error(
31992
+ `${where}: filename ${JSON.stringify(filename)} is not a usable filename (no control characters, not "."/"..", 200 characters max).`
31993
+ );
31994
+ }
31995
+ }
31996
+ function decodedLength(contentBase64) {
31997
+ const buf = Buffer.from(contentBase64, "base64");
31998
+ return buf.toString("base64") === contentBase64 ? buf.length : null;
31999
+ }
32000
+ function inlineFileArg(flag, attachment, opts = {}) {
32001
+ const { filename, contentBase64 } = attachment;
32002
+ const where = opts.where ?? `attachInline entry ${JSON.stringify(filename)}`;
32003
+ validateFilename(filename, where);
32004
+ const bytes = decodedLength(contentBase64);
32005
+ if (bytes === null) {
32006
+ throw new Error(
32007
+ `${where}: contents are not valid base64. Send the standard alphabet with padding and no line breaks \u2014 the value must survive a decode/re-encode round trip unchanged.`
32008
+ );
32009
+ }
32010
+ if (bytes > MAX_INLINE_ATTACHMENT_BYTES) {
32011
+ throw new Error(
32012
+ `${where}: ${bytes} bytes exceeds the ${MAX_INLINE_ATTACHMENT_BYTES}-byte (${formatMiB(MAX_INLINE_ATTACHMENT_BYTES)}) per-file limit for inline content. Upload it to Drive and link to it instead, or send it from a local (stdio) deployment using a real server-side path.`
32013
+ );
32014
+ }
32015
+ const arg = { kind: "file", flag, contents: contentBase64, encoding: "base64", filename };
32016
+ if (opts.positional) arg.positional = true;
32017
+ return { arg, bytes };
32018
+ }
32019
+ function inlineAttachmentArgs(flag, attachments, siblingArgs = []) {
32020
+ if (!attachments?.length) return [];
32021
+ const args = [];
32022
+ const siblingWire = siblingArgs.reduce((sum, arg) => sum + wireBytesOf(arg), 0);
32023
+ let attachmentWire = 0;
32024
+ let decodedTotal = 0;
32025
+ for (const attachment of attachments) {
32026
+ const { arg, bytes } = inlineFileArg(flag, attachment);
32027
+ attachmentWire += arg.contents.length;
32028
+ decodedTotal += bytes;
32029
+ if (siblingWire + attachmentWire > MAX_REQUEST_PAYLOAD_WIRE_BYTES) {
32030
+ const blame = attachmentWire <= MAX_REQUEST_PAYLOAD_WIRE_BYTES ? ` These attachments would fit on their own; the rest of the message (its body, mostly) spends ${siblingWire} bytes of the same budget.` : "";
32031
+ throw new Error(
32032
+ `This message is too large to send: ${decodedTotal} bytes of attachments (${attachmentWire} bytes once base64-encoded for transit) exceed the ${MAX_REQUEST_PAYLOAD_WIRE_BYTES}-byte request limit.${blame} The ceiling for attachments alone is ${MAX_INLINE_ATTACHMENT_TOTAL_BYTES} bytes (${formatMiB(MAX_INLINE_ATTACHMENT_TOTAL_BYTES)}); a long body lowers it. Send fewer or smaller files per message, shorten the body, or upload the large files to Drive and link them.`
32033
+ );
32034
+ }
32035
+ args.push(arg);
32036
+ }
32037
+ return args;
32038
+ }
32039
+
31911
32040
  // ../gogcli-mcp/src/gmail-results.ts
31912
32041
  function sortKey(item) {
31913
32042
  for (const raw of [item.internalDateIso, item.date]) {
@@ -31967,6 +32096,7 @@ async function fetchGmailPages(runPage, itemsKey, maxPages, startToken) {
31967
32096
  const merged = [];
31968
32097
  let base;
31969
32098
  let token = startToken;
32099
+ const fetched = new Set(startToken === void 0 ? [] : [startToken]);
31970
32100
  for (let pages = 0; pages < maxPages; pages++) {
31971
32101
  const result = await runPage(token);
31972
32102
  const parsed = parsePage(result, itemsKey);
@@ -31975,8 +32105,17 @@ async function fetchGmailPages(runPage, itemsKey, maxPages, startToken) {
31975
32105
  }
31976
32106
  base = parsed;
31977
32107
  merged.push(...parsed[itemsKey]);
31978
- token = typeof parsed.nextPageToken === "string" && parsed.nextPageToken !== "" ? parsed.nextPageToken : void 0;
31979
- if (token === void 0) break;
32108
+ const next = typeof parsed.nextPageToken === "string" && parsed.nextPageToken !== "" ? parsed.nextPageToken : void 0;
32109
+ if (next === void 0) {
32110
+ token = void 0;
32111
+ break;
32112
+ }
32113
+ if (fetched.has(next)) {
32114
+ token = next;
32115
+ break;
32116
+ }
32117
+ fetched.add(next);
32118
+ token = next;
31980
32119
  }
31981
32120
  return finish(base, itemsKey, merged, token);
31982
32121
  }
@@ -32000,86 +32139,46 @@ function finish(base, itemsKey, merged, token) {
32000
32139
  return rawTextResult(JSON.stringify(out));
32001
32140
  }
32002
32141
 
32003
- // ../gogcli-mcp/src/attachments.ts
32004
- var MAX_INLINE_ATTACHMENT_BYTES = 8 * 1024 * 1024;
32005
- var RUNNER_MAX_BODY_BYTES = 32 * 1024 * 1024;
32006
- var RUNNER_BODY_JSON_RESERVE_BYTES = 256 * 1024;
32007
- var MAX_REQUEST_PAYLOAD_WIRE_BYTES = RUNNER_MAX_BODY_BYTES - RUNNER_BODY_JSON_RESERVE_BYTES;
32008
- var MAX_INLINE_ATTACHMENT_TOTAL_BYTES = Math.floor(MAX_REQUEST_PAYLOAD_WIRE_BYTES * 3 / 4);
32009
- function wireBytesOf(arg) {
32010
- if (typeof arg === "string") return Buffer.byteLength(arg, "utf8");
32011
- return arg.encoding === "base64" ? arg.contents.length : Buffer.byteLength(arg.contents, "utf8");
32012
- }
32013
- var formatMiB = (bytes) => `${Math.floor(bytes / (1024 * 1024))} MiB`;
32014
- var INLINE_ATTACHMENT_LIMITS_TEXT = `up to ${formatMiB(MAX_INLINE_ATTACHMENT_BYTES)} per file and ${formatMiB(MAX_INLINE_ATTACHMENT_TOTAL_BYTES)} in total`;
32015
- var inlineAttachmentSchema = external_exports.object({
32016
- filename: external_exports.string().min(1).describe(
32017
- `Filename the recipient will see, e.g. "pendant-layouts.png". gog infers the attachment's MIME type from this extension, so give it the right one \u2014 a .png sent as "layouts" arrives as an untyped blob. Must be a single filename, not a path.`
32018
- ),
32019
- contentBase64: external_exports.string().min(1).describe(
32020
- "The file's bytes, base64-encoded (standard alphabet, with padding). This is the whole point of this parameter: the bytes travel with the request, so nothing needs to exist on the gog server's filesystem."
32021
- )
32022
- });
32023
- var attachInlineParam = external_exports.array(inlineAttachmentSchema).optional().describe(
32024
- `Attachments supplied as BYTES rather than as server-side paths \u2014 use this whenever you hold a file and the gog server does not, which is always the case on the hosted connector and on any remote deployment. Each entry is {filename, contentBase64} (${INLINE_ATTACHMENT_LIMITS_TEXT}). Can be combined with \`attach\`: the two name disjoint files (paths read on the server vs. bytes sent with the call), and both end up as ordinary attachments on the message.`
32025
- );
32026
- function validateFilename(filename, where) {
32027
- if (/[/\\]/.test(filename)) {
32028
- throw new Error(
32029
- `${where}: filename ${JSON.stringify(filename)} must be a bare filename, not a path. Pass just the name the recipient should see, e.g. "report.pdf".`
32030
- );
32031
- }
32032
- if (/[\x00-\x1f]/.test(filename) || /^\.+$/.test(filename) || filename.length > 200) {
32033
- throw new Error(
32034
- `${where}: filename ${JSON.stringify(filename)} is not a usable filename (no control characters, not "."/"..", 200 characters max).`
32035
- );
32036
- }
32037
- }
32038
- function decodedLength(contentBase64) {
32039
- const buf = Buffer.from(contentBase64, "base64");
32040
- return buf.toString("base64") === contentBase64 ? buf.length : null;
32041
- }
32042
- function inlineFileArg(flag, attachment, opts = {}) {
32043
- const { filename, contentBase64 } = attachment;
32044
- const where = opts.where ?? `attachInline entry ${JSON.stringify(filename)}`;
32045
- validateFilename(filename, where);
32046
- const bytes = decodedLength(contentBase64);
32047
- if (bytes === null) {
32048
- throw new Error(
32049
- `${where}: contents are not valid base64. Send the standard alphabet with padding and no line breaks \u2014 the value must survive a decode/re-encode round trip unchanged.`
32050
- );
32051
- }
32052
- if (bytes > MAX_INLINE_ATTACHMENT_BYTES) {
32053
- throw new Error(
32054
- `${where}: ${bytes} bytes exceeds the ${MAX_INLINE_ATTACHMENT_BYTES}-byte (${formatMiB(MAX_INLINE_ATTACHMENT_BYTES)}) per-file limit for inline content. Upload it to Drive and link to it instead, or send it from a local (stdio) deployment using a real server-side path.`
32055
- );
32056
- }
32057
- const arg = { kind: "file", flag, contents: contentBase64, encoding: "base64", filename };
32058
- if (opts.positional) arg.positional = true;
32059
- return { arg, bytes };
32060
- }
32061
- function inlineAttachmentArgs(flag, attachments, siblingArgs = []) {
32062
- if (!attachments?.length) return [];
32063
- const args = [];
32064
- const siblingWire = siblingArgs.reduce((sum, arg) => sum + wireBytesOf(arg), 0);
32065
- let attachmentWire = 0;
32066
- let decodedTotal = 0;
32067
- for (const attachment of attachments) {
32068
- const { arg, bytes } = inlineFileArg(flag, attachment);
32069
- attachmentWire += arg.contents.length;
32070
- decodedTotal += bytes;
32071
- if (siblingWire + attachmentWire > MAX_REQUEST_PAYLOAD_WIRE_BYTES) {
32072
- const blame = attachmentWire <= MAX_REQUEST_PAYLOAD_WIRE_BYTES ? ` These attachments would fit on their own; the rest of the message (its body, mostly) spends ${siblingWire} bytes of the same budget.` : "";
32073
- throw new Error(
32074
- `This message is too large to send: ${decodedTotal} bytes of attachments (${attachmentWire} bytes once base64-encoded for transit) exceed the ${MAX_REQUEST_PAYLOAD_WIRE_BYTES}-byte request limit.${blame} The ceiling for attachments alone is ${MAX_INLINE_ATTACHMENT_TOTAL_BYTES} bytes (${formatMiB(MAX_INLINE_ATTACHMENT_TOTAL_BYTES)}); a long body lowers it. Send fewer or smaller files per message, shorten the body, or upload the large files to Drive and link them.`
32075
- );
32076
- }
32077
- args.push(arg);
32078
- }
32079
- return args;
32080
- }
32081
-
32082
32142
  // ../gogcli-mcp/src/tools/gmail.ts
32143
+ var replySchema = {
32144
+ messageId: external_exports.string().describe("Gmail message ID to reply to \u2014 the short hex `id` from gog_gmail_get / _search (or gog_gmail_messages_search, gogcli-mcp-gmail only). NOT the threadId, NOT the RFC822 `<\u2026@host>` Message-Id header."),
32145
+ body: external_exports.string().optional().describe("Reply body (plain text; required unless bodyHtml or bodyHtmlFile is set). Any size \u2014 a large body is written to a temp file on the gog server rather than inlined into the command line. Note gog strips trailing newlines from a file-delivered body."),
32146
+ bodyHtml: external_exports.string().optional().describe("Reply body (HTML; optional). Pass the HTML itself at any size \u2014 a large body is written to a temp file on the gog server rather than inlined into the command line. Mutually exclusive with bodyHtmlFile."),
32147
+ bodyHtmlFile: external_exports.string().optional().describe(`Path to an HTML file that ALREADY EXISTS on the gog server for the reply body. gog also accepts "-" for stdin, but this server never writes to gog's stdin, so "-" would hang until the call times out. Mutually exclusive with bodyHtml \u2014 supplying both is rejected. You rarely need this: bodyHtml handles large bodies on its own.`),
32148
+ to: external_exports.array(external_exports.string()).optional().describe("Add or move recipients to To (repeatable). Added on top of the recipients inherited from the original message."),
32149
+ cc: external_exports.array(external_exports.string()).optional().describe("Add or move recipients to Cc (repeatable)"),
32150
+ bcc: external_exports.array(external_exports.string()).optional().describe("Add or move recipients to Bcc (repeatable)"),
32151
+ remove: external_exports.array(external_exports.string()).optional().describe("Remove these recipients from all fields (repeatable) \u2014 e.g. to drop someone from a reply-all."),
32152
+ subject: external_exports.string().optional().describe('Override reply subject (default: "Re: <original>"). A changed subject starts a NEW Gmail thread.'),
32153
+ noQuote: external_exports.boolean().optional().describe("Do not include the original message quoted below the reply (default: the original is quoted)"),
32154
+ attach: external_exports.array(external_exports.string()).optional().describe(`File paths to attach (repeatable), resolved ON THE GOG SERVER's filesystem \u2014 NOT this client's. Only usable when gog runs on the same machine you do (local stdio); on the hosted connector or any GOG_RUNNER_URL backend these paths do not exist and the call fails with "no such file or directory" \u2014 use attachInline there. Read on the server, base64-encoded with a MIME type inferred from the extension.`),
32155
+ attachInline: attachInlineParam,
32156
+ from: external_exports.string().optional().describe("Send from this email address (must be a verified send-as alias)"),
32157
+ autoFromAddressedAlias: external_exports.boolean().optional().describe("When from is omitted, send from the verified send-as alias the original message was addressed TO, instead of the account's primary address \u2014 so a reply to mail sent to an alias goes back out from that alias. Ignored when from is set."),
32158
+ signature: external_exports.boolean().optional().describe("Append the Gmail signature from the active send-as address"),
32159
+ signatureFrom: external_exports.string().optional().describe("Append the Gmail signature from this send-as email address"),
32160
+ signatureFile: external_exports.string().optional().describe("Append a local signature file (plain text or HTML), read on the gog server"),
32161
+ account: accountParam
32162
+ };
32163
+ function appendReplyFlags(args, f) {
32164
+ assertNotBoth("bodyHtml", "bodyHtmlFile", f.bodyHtml, f.bodyHtmlFile);
32165
+ if (f.body) args.push(payloadArg("body", "body-file", f.body));
32166
+ if (f.bodyHtml) args.push(payloadArg("body-html", "body-html-file", f.bodyHtml, "html"));
32167
+ else if (f.bodyHtmlFile) args.push(`--body-html-file=${f.bodyHtmlFile}`);
32168
+ if (f.to) for (const r of f.to) args.push(`--to=${r}`);
32169
+ if (f.cc) for (const r of f.cc) args.push(`--cc=${r}`);
32170
+ if (f.bcc) for (const r of f.bcc) args.push(`--bcc=${r}`);
32171
+ if (f.remove) for (const r of f.remove) args.push(`--remove=${r}`);
32172
+ if (f.subject) args.push(`--subject=${f.subject}`);
32173
+ if (f.noQuote) args.push("--no-quote");
32174
+ if (f.attach) for (const p of f.attach) args.push(`--attach=${p}`);
32175
+ args.push(...inlineAttachmentArgs("attach", f.attachInline, args));
32176
+ if (f.from) args.push(`--from=${f.from}`);
32177
+ if (f.signature) args.push("--signature");
32178
+ if (f.signatureFrom) args.push(`--signature-from=${f.signatureFrom}`);
32179
+ if (f.signatureFile) args.push(`--signature-file=${f.signatureFile}`);
32180
+ args.push(f.autoFromAddressedAlias ? "--auto-from-addressed-alias" : "--auto-from-addressed-alias=false");
32181
+ }
32083
32182
  function registerGmailTools(server) {
32084
32183
  server.registerTool("gog_gmail_search", {
32085
32184
  description: 'Search Gmail threads using Gmail query syntax (e.g. "from:alice subject:invoice is:unread"). The query is passed verbatim to Gmail; a bare name token (from:alison) matches per Gmail\'s own heuristics, a full address (from:alison@example.com) is exact. To match a contact across several addresses, OR them: from:(a@x.com OR b@y.com). Results are ALWAYS newest-first by Gmail\'s internalDate \u2014 the wrapper sorts them, so the first result is the most recent match and a recent message can never be buried below older ones. IMPORTANT \u2014 a response carrying "truncated": true is an INCOMPLETE view of the matches: NEVER report that a message does not exist, or that there is no such mail, on the strength of one. Page through it (pass nextPageToken back as `pageToken`), set maxPages to walk several pages in one call, or narrow the query, and only then draw a conclusion. If you already know the thread, do not search for it at all \u2014 read it directly with gog_gmail_thread_get, which returns the whole thread and cannot be truncated or mis-ranked.',
@@ -32132,7 +32231,7 @@ function registerGmailTools(server) {
32132
32231
  return runOrDiagnose(args, { account });
32133
32232
  });
32134
32233
  server.registerTool("gog_gmail_send", {
32135
- description: 'Send an email. Two ways to attach a file: `attach` takes paths READ ON THE GOG SERVER, and `attachInline` takes the bytes themselves. Use attachInline unless you know the file exists on the same machine gog runs on \u2014 on the hosted connector and any remote deployment there is no shared filesystem, so no path you can name resolves there and `attach` will fail with "no such file or directory". When either is used, the JSON result echoes the attached filenames and byte sizes \u2014 check it to confirm the files were embedded.',
32234
+ description: 'Send an email. Two ways to attach a file: `attach` takes paths READ ON THE GOG SERVER, and `attachInline` takes the bytes themselves. Use attachInline unless you know the file exists on the same machine gog runs on \u2014 on the hosted connector and any remote deployment there is no shared filesystem, so no path you can name resolves there and `attach` will fail with "no such file or directory". When either is used, the JSON result echoes the attached filenames and byte sizes \u2014 check it to confirm the files were embedded. NOT the tool for answering a message: replyToMessageId only files this in the right thread \u2014 the subject, recipients and body are entirely yours, and the original is not quoted unless you set quote. Use gog_gmail_reply / gog_gmail_reply_all instead, which inherit all three.',
32136
32235
  annotations: { destructiveHint: true },
32137
32236
  inputSchema: {
32138
32237
  to: external_exports.string().describe("Recipient(s), comma-separated"),
@@ -32140,23 +32239,43 @@ function registerGmailTools(server) {
32140
32239
  body: external_exports.string().describe("Email body (plain text). Any size \u2014 a large body is written to a temp file on the gog server rather than inlined into the command line. Note gog strips trailing newlines from a file-delivered body."),
32141
32240
  cc: external_exports.string().optional().describe("CC recipients, comma-separated"),
32142
32241
  bcc: external_exports.string().optional().describe("BCC recipients, comma-separated"),
32143
- replyToMessageId: external_exports.string().optional().describe("Message ID to reply to"),
32144
- threadId: external_exports.string().optional().describe("Thread ID to reply within"),
32242
+ replyToMessageId: external_exports.string().optional().describe('Message ID to thread this message against \u2014 sets In-Reply-To/References only. It does NOT quote the original (pass quote for that), inherit its recipients, or prefix the subject with "Re:". For an actual reply use gog_gmail_reply.'),
32243
+ threadId: external_exports.string().optional().describe("Thread ID to thread this message within. Same caveat as replyToMessageId: threading only, no quote and no inherited subject or recipients."),
32244
+ quote: external_exports.boolean().optional().describe("Include the original message quoted below the body. Requires replyToMessageId or threadId. gog quotes by DEFAULT on gmail reply but never on gmail send, so without this a threaded send arrives with the original nowhere in it."),
32145
32245
  attach: external_exports.array(external_exports.string()).optional().describe(`File paths to attach (repeatable), resolved ON THE GOG SERVER's filesystem \u2014 NOT this client's. Only usable when gog runs on the same machine you do (local stdio); on the hosted connector or any GOG_RUNNER_URL backend these paths do not exist and the call fails with "no such file or directory" \u2014 use attachInline there. Each file is read on the server, base64-encoded with a MIME type inferred from its extension, and added as a multipart attachment.`),
32146
32246
  attachInline: attachInlineParam,
32147
32247
  account: accountParam
32148
32248
  }
32149
- }, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, attach, attachInline, account }) => {
32249
+ }, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, quote, attach, attachInline, account }) => {
32150
32250
  const args = ["gmail", "send", `--to=${to}`, `--subject=${subject}`, payloadArg("body", "body-file", body)];
32151
32251
  if (cc) args.push(`--cc=${cc}`);
32152
32252
  if (bcc) args.push(`--bcc=${bcc}`);
32153
32253
  if (replyToMessageId) args.push(`--reply-to-message-id=${replyToMessageId}`);
32154
32254
  if (threadId) args.push(`--thread-id=${threadId}`);
32255
+ if (quote) args.push("--quote");
32155
32256
  if (attach) for (const path of attach) args.push(`--attach=${path}`);
32156
32257
  const inline = inlineAttachmentArgs("attach", attachInline, args);
32157
32258
  args.push(...inline);
32158
32259
  return runOrDiagnose(args, { account });
32159
32260
  });
32261
+ server.registerTool("gog_gmail_reply", {
32262
+ description: 'Reply to a Gmail message (goes to the original sender only). USE THIS, not gog_gmail_send, whenever you are answering a message: it threads off the original AND inherits its "Re:" subject and quotes its body below yours, which gog_gmail_send does not \u2014 a send with replyToMessageId lands in the right thread but reads as a brand-new message, with the original nowhere in it. To answer every participant use gog_gmail_reply_all. The gogcli-mcp-gmail package adds two more routes with the same composition: gog_gmail_autoreply to reply across every message matching a query, and gog_gmail_drafts_reply to stage this exact reply as a draft instead of sending it.',
32263
+ annotations: { destructiveHint: true },
32264
+ inputSchema: replySchema
32265
+ }, async ({ messageId, account, ...flags }) => {
32266
+ const args = ["gmail", "reply", messageId];
32267
+ appendReplyFlags(args, flags);
32268
+ return runOrDiagnose(args, { account });
32269
+ });
32270
+ server.registerTool("gog_gmail_reply_all", {
32271
+ description: 'Reply to all participants of a Gmail message (the sender plus every To/Cc recipient). Same inherited "Re:" subject and quoted original as gog_gmail_reply. Use the remove flag to drop specific recipients from the reply-all. To stage it as a draft rather than send it, use gog_gmail_drafts_reply_all (gogcli-mcp-gmail only).',
32272
+ annotations: { destructiveHint: true },
32273
+ inputSchema: replySchema
32274
+ }, async ({ messageId, account, ...flags }) => {
32275
+ const args = ["gmail", "reply-all", messageId];
32276
+ appendReplyFlags(args, flags);
32277
+ return runOrDiagnose(args, { account });
32278
+ });
32160
32279
  registerRunTool(server, { service: "gmail", examples: '"archive", "mark-read", "labels"' });
32161
32280
  }
32162
32281
 
@@ -32170,7 +32289,7 @@ var failIfNotEmptyParam = external_exports.boolean().optional().describe(
32170
32289
  );
32171
32290
 
32172
32291
  // ../gogcli-mcp/src/server.ts
32173
- var VERSION = true ? "2.25.0" : "0.0.0";
32292
+ var VERSION = true ? "2.27.0" : "0.0.0";
32174
32293
 
32175
32294
  // ../gogcli-mcp/src/auth-log.ts
32176
32295
  var FAILURES = /* @__PURE__ */ new Set([
@@ -32657,13 +32776,6 @@ function useRemoteGogRunner(env = process.env) {
32657
32776
  }
32658
32777
 
32659
32778
  // src/tools/gmail-extra.ts
32660
- function assertNotBoth(inlineParam, fileParam, inlineValue, fileValue) {
32661
- if (inlineValue !== void 0 && fileValue !== void 0) {
32662
- throw new Error(
32663
- `${inlineParam} and ${fileParam} are mutually exclusive \u2014 gog accepts only one of them. Pass ${inlineParam} with the content itself (it is written to a temp file automatically when large), or ${fileParam} with a path that already exists on the gog server.`
32664
- );
32665
- }
32666
- }
32667
32779
  function resultText(result) {
32668
32780
  const first = result.content[0];
32669
32781
  return first?.type === "text" ? first.text : void 0;
@@ -34139,7 +34251,7 @@ function registerExtraGmailTools(server) {
34139
34251
  replyToMessageId: external_exports.string().optional().describe("Reply to a specific Gmail MESSAGE id \u2014 the short hex `id` field from gog_gmail_get / _search / _thread_get (e.g. 19e7593d77fd9636), NOT a thread id and NOT the RFC822 `<\u2026@host>` Message-Id header. Anchors In-Reply-To/References to that exact message. To reply to a thread when you don't know the latest message, use replyToThreadId instead. If both are given, replyToMessageId wins."),
34140
34252
  replyToThreadId: external_exports.string().optional().describe(`Reply to a Gmail THREAD id \u2014 passed to gog as --thread-id, which threads the draft using the thread's latest-message headers (In-Reply-To/References). This is what "reply to this thread" almost always means. Mutually exclusive with replyToMessageId (which wins if both are set). Thread ids and message ids are both 16-hex strings and easy to confuse \u2014 use this param, not replyToMessageId, when the id came from a thread.`),
34141
34253
  replyTo: external_exports.string().optional().describe("Reply-To header address"),
34142
- quote: external_exports.boolean().optional().describe("Include quoted original message in reply (requires replyToMessageId or replyToThreadId)"),
34254
+ quote: external_exports.boolean().optional().describe('Include the original message quoted below the body. Requires replyToMessageId or replyToThreadId. DEFAULTS OFF: a draft created with a reply target but without this threads correctly and still reads as a brand-new message, because gog only quotes by default on its reply subcommands. For a real reply draft prefer gog_gmail_drafts_reply / gog_gmail_drafts_reply_all, which also inherit the recipients and the "Re:" subject that this tool leaves to you.'),
34143
34255
  replyAll: external_exports.boolean().optional().describe("Auto-populate recipients from the original message (reply-all), inferring To/Cc from it. Requires replyToMessageId or replyToThreadId. Explicit to/cc/bcc still apply on top; omitRecipients still suppresses them."),
34144
34256
  attach: external_exports.array(external_exports.string()).optional().describe(`File paths to attach (repeatable), resolved ON THE GOG SERVER's filesystem \u2014 NOT this client's. Only usable when gog runs on the same machine you do (local stdio); on the hosted connector or any GOG_RUNNER_URL backend these paths do not exist and the call fails with "no such file or directory" \u2014 use attachInline there. Read on the server, base64-encoded with a MIME type inferred from the extension. The JSON result echoes attached filenames and byte sizes \u2014 check it to confirm the files were found and embedded. On gog_gmail_drafts_update, supplying attach REPLACES the draft's existing attachments; omitting it preserves them (use clearAttachments to remove all).`),
34145
34257
  attachInline: attachInlineParam,
@@ -34327,63 +34439,6 @@ function registerExtraGmailTools(server) {
34327
34439
  if (skipAttachments) args.push("--skip-attachments");
34328
34440
  return runOrDiagnose(args, { account });
34329
34441
  });
34330
- const replySchema = {
34331
- messageId: external_exports.string().describe("Gmail message ID to reply to \u2014 the short hex `id` from gog_gmail_get / _search / _messages_search (NOT the threadId, NOT the RFC822 `<\u2026@host>` Message-Id header)."),
34332
- body: external_exports.string().optional().describe("Reply body (plain text; required unless bodyHtml or bodyHtmlFile is set). Any size \u2014 a large body is written to a temp file on the gog server rather than inlined into the command line. Note gog strips trailing newlines from a file-delivered body."),
34333
- bodyHtml: external_exports.string().optional().describe("Reply body (HTML; optional). Pass the HTML itself at any size \u2014 a large body is written to a temp file on the gog server rather than inlined into the command line. Mutually exclusive with bodyHtmlFile."),
34334
- bodyHtmlFile: external_exports.string().optional().describe(`Path to an HTML file that ALREADY EXISTS on the gog server for the reply body. gog also accepts "-" for stdin, but this server never writes to gog's stdin, so "-" would hang until the call times out. Mutually exclusive with bodyHtml \u2014 supplying both is rejected. You rarely need this: bodyHtml handles large bodies on its own.`),
34335
- to: external_exports.array(external_exports.string()).optional().describe("Add or move recipients to To (repeatable). Added on top of the recipients inherited from the original message."),
34336
- cc: external_exports.array(external_exports.string()).optional().describe("Add or move recipients to Cc (repeatable)"),
34337
- bcc: external_exports.array(external_exports.string()).optional().describe("Add or move recipients to Bcc (repeatable)"),
34338
- remove: external_exports.array(external_exports.string()).optional().describe("Remove these recipients from all fields (repeatable) \u2014 e.g. to drop someone from a reply-all."),
34339
- subject: external_exports.string().optional().describe('Override reply subject (default: "Re: <original>"). A changed subject starts a NEW Gmail thread.'),
34340
- noQuote: external_exports.boolean().optional().describe("Do not include the original message quoted below the reply (default: the original is quoted)"),
34341
- attach: external_exports.array(external_exports.string()).optional().describe(`File paths to attach (repeatable), resolved ON THE GOG SERVER's filesystem \u2014 NOT this client's. Only usable when gog runs on the same machine you do (local stdio); on the hosted connector or any GOG_RUNNER_URL backend these paths do not exist and the call fails with "no such file or directory" \u2014 use attachInline there. Read on the server, base64-encoded with a MIME type inferred from the extension.`),
34342
- attachInline: attachInlineParam,
34343
- from: external_exports.string().optional().describe("Send from this email address (must be a verified send-as alias)"),
34344
- autoFromAddressedAlias: external_exports.boolean().optional().describe("When from is omitted, send from the verified send-as alias the original message was addressed TO, instead of the account's primary address \u2014 so a reply to mail sent to an alias goes back out from that alias. Ignored when from is set."),
34345
- signature: external_exports.boolean().optional().describe("Append the Gmail signature from the active send-as address"),
34346
- signatureFrom: external_exports.string().optional().describe("Append the Gmail signature from this send-as email address"),
34347
- signatureFile: external_exports.string().optional().describe("Append a local signature file (plain text or HTML), read on the gog server"),
34348
- account: accountParam
34349
- };
34350
- function appendReplyFlags(args, f) {
34351
- assertNotBoth("bodyHtml", "bodyHtmlFile", f.bodyHtml, f.bodyHtmlFile);
34352
- if (f.body) args.push(payloadArg("body", "body-file", f.body));
34353
- if (f.bodyHtml) args.push(payloadArg("body-html", "body-html-file", f.bodyHtml, "html"));
34354
- else if (f.bodyHtmlFile) args.push(`--body-html-file=${f.bodyHtmlFile}`);
34355
- if (f.to) for (const r of f.to) args.push(`--to=${r}`);
34356
- if (f.cc) for (const r of f.cc) args.push(`--cc=${r}`);
34357
- if (f.bcc) for (const r of f.bcc) args.push(`--bcc=${r}`);
34358
- if (f.remove) for (const r of f.remove) args.push(`--remove=${r}`);
34359
- if (f.subject) args.push(`--subject=${f.subject}`);
34360
- if (f.noQuote) args.push("--no-quote");
34361
- if (f.attach) for (const p of f.attach) args.push(`--attach=${p}`);
34362
- args.push(...inlineAttachmentArgs("attach", f.attachInline, args));
34363
- if (f.from) args.push(`--from=${f.from}`);
34364
- if (f.signature) args.push("--signature");
34365
- if (f.signatureFrom) args.push(`--signature-from=${f.signatureFrom}`);
34366
- if (f.signatureFile) args.push(`--signature-file=${f.signatureFile}`);
34367
- args.push(f.autoFromAddressedAlias ? "--auto-from-addressed-alias" : "--auto-from-addressed-alias=false");
34368
- }
34369
- server.registerTool("gog_gmail_reply", {
34370
- description: 'Reply to a Gmail message (sends to the original sender only). Threads off the message and inherits a "Re:" subject and the quoted original by default. For replying to every participant use gog_gmail_reply_all; to reply across many messages matching a query use gog_gmail_autoreply; to stage this same reply without sending it use gog_gmail_drafts_reply, which composes exactly what this tool would send.',
34371
- annotations: { destructiveHint: true },
34372
- inputSchema: replySchema
34373
- }, async ({ messageId, account, ...flags }) => {
34374
- const args = ["gmail", "reply", messageId];
34375
- appendReplyFlags(args, flags);
34376
- return runOrDiagnose(args, { account });
34377
- });
34378
- server.registerTool("gog_gmail_reply_all", {
34379
- description: 'Reply to all participants of a Gmail message (sender plus every To/Cc recipient). Same inherited "Re:" subject and quoting as gog_gmail_reply. Use the remove flag to drop specific recipients from the reply-all. To stage it without sending use gog_gmail_drafts_reply_all.',
34380
- annotations: { destructiveHint: true },
34381
- inputSchema: replySchema
34382
- }, async ({ messageId, account, ...flags }) => {
34383
- const args = ["gmail", "reply-all", messageId];
34384
- appendReplyFlags(args, flags);
34385
- return runOrDiagnose(args, { account });
34386
- });
34387
34442
  const draftReplyNote = ' Composes exactly what gog_gmail_reply%s would send \u2014 inherited recipients, "Re:" subject and quoted original \u2014 but SAVES IT AS A DRAFT instead of sending. Nothing leaves the mailbox; send it later with gog_gmail_drafts_send, or edit it first with gog_gmail_drafts_update (which overwrites the whole body, quote included \u2014 read the draft back before editing).';
34388
34443
  server.registerTool("gog_gmail_drafts_reply", {
34389
34444
  description: "Save a reply to a Gmail message as a draft (to the original sender only)." + draftReplyNote.replace("%s", "") + " Prefer this over gog_gmail_drafts_create + replyToMessageId when the draft is a real reply: that route threads the draft but leaves recipients and quoting for you to reconstruct.",
package/manifest.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "manifest_version": "0.3",
4
4
  "name": "gogcli-mcp-gmail",
5
5
  "display_name": "gogcli (Gmail)",
6
- "version": "2.25.0",
6
+ "version": "2.27.0",
7
7
  "description": "Extended Gmail for Claude via gogcli — auth + full Gmail support (threads, labels, drafts, attachments, forward, autoreply, bulk operations)",
8
8
  "author": {
9
9
  "name": "Chris Hall",
package/mint.yaml ADDED
@@ -0,0 +1,107 @@
1
+ version: 1
2
+ name: Google Gmail (gogcli)
3
+ slug: gogcli-mcp-gmail
4
+ summary: >-
5
+ Extended Gmail MCP server via gogcli — auth + full Gmail support (threads,
6
+ labels, drafts, attachments, forward, autoreply, bulk operations)
7
+ command:
8
+ # This package publishes a single bin; naming it keeps the install
9
+ # unambiguous alongside its eight sibling packages.
10
+ bin: gogcli-mcp-gmail
11
+ env:
12
+ - name: GOG_CLIENT_ID
13
+ required: false
14
+ help: >-
15
+ Google OAuth client id. NOTE: on the local-spawn path the child `gog`
16
+ receives a sanitized env — runner.ts drops GOG_ACCESS_TOKEN and every
17
+ *_TOKEN / *_SECRET / *_API_KEY / *_PRIVATE_KEY variable — so the CLI
18
+ authenticates from its own stored credentials under $HOME (see
19
+ state.dataDir), not from these variables being passed through.
20
+ - name: GOG_CLIENT_SECRET
21
+ secret: true
22
+ required: false
23
+ help: >-
24
+ Google OAuth client secret. Stripped from the spawned CLI's environment
25
+ by runner.ts's *_SECRET rule — see GOG_CLIENT_ID.
26
+ - name: GOG_REFRESH_TOKEN
27
+ secret: true
28
+ required: false
29
+ help: >-
30
+ Google OAuth refresh token. Stripped from the spawned CLI's environment
31
+ by runner.ts's *_TOKEN rule — see GOG_CLIENT_ID. A hosted deployment must
32
+ therefore carry gog's authorised-account state in its persisted data dir,
33
+ or drive a remote runner via GOG_RUNNER_URL.
34
+ - name: GOG_ACCESS_TOKEN
35
+ secret: true
36
+ required: false
37
+ help: >-
38
+ Deliberately removed from the spawned CLI's environment by runner.ts, so
39
+ that a stale directly-passed token cannot shadow gog's stored refresh
40
+ credential. Leave unset.
41
+ - name: GOG_ACCOUNT
42
+ required: false
43
+ help: >-
44
+ Which configured Google account to act as, when more than one is
45
+ authorised. Defaults to the single/most recent account.
46
+ - name: GOG_READONLY
47
+ required: false
48
+ help: >-
49
+ Set to 1 to refuse every mutating operation. Recommended when the
50
+ connector is shared or you only need reads.
51
+ - name: GOG_PATH
52
+ required: false
53
+ help: >-
54
+ Path to the `gog` binary. Leave unset when the dependency below supplies
55
+ it; set it only to point at a binary you manage yourself.
56
+ - name: GOG_RUNNER_URL
57
+ required: false
58
+ help: >-
59
+ URL of a remote gog runner to execute against instead of spawning the
60
+ local binary. If you set this, add its host to egress.allow.
61
+ - name: GOG_RUNNER_KEY
62
+ secret: true
63
+ required: false
64
+ help: >-
65
+ Shared key authenticating calls to GOG_RUNNER_URL. Required whenever that
66
+ is set.
67
+ - name: GOG_TIMEZONE
68
+ required: false
69
+ help: >-
70
+ The IANA zone gog itself formats its naive timestamps in. The wrapper
71
+ reads it (naiveSourceTimeZone) to re-attach the correct offset, so it
72
+ should match gog's own configuration. Falls back to DISPLAY_TZ, then to
73
+ America/New_York.
74
+ - name: DISPLAY_TZ
75
+ required: false
76
+ help: >-
77
+ The IANA zone every rendered *Display field uses (displayTimeZone,
78
+ default America/New_York). It does not read GOG_TIMEZONE — the fallback
79
+ runs the other way, so this is also what GOG_TIMEZONE falls back to. An
80
+ invalid value degrades to the default rather than throwing.
81
+ dependencies:
82
+ # Every tool shells out to the `gog` CLI; without it the server starts and
83
+ # then fails on the first call. This tag must track
84
+ # packages/gogcli-mcp/src/runner.ts's MIN_GOG_VERSION (the floor the tools
85
+ # assume) and the fly-gog-runner/Dockerfile GOG_VERSION build arg. See
86
+ # CLAUDE.md "Required gog version" — bumping the floor means bumping these
87
+ # nine pins too.
88
+ - kind: github-release
89
+ repo: openclaw/gogcli
90
+ tag: v0.38.1
91
+ asset: "gogcli_*_linux_amd64.tar.gz"
92
+ bin: [gog]
93
+ state:
94
+ dataDir: true
95
+ reason: >-
96
+ `gog` keeps its authorised-account state and token cache under $HOME.
97
+ Without a persistent data dir every cold start has no account configured and
98
+ every tool call fails until the credentials are re-supplied.
99
+ egress:
100
+ allow:
101
+ # Google OAuth token exchange and the Google REST APIs the CLI calls.
102
+ - oauth2.googleapis.com
103
+ - www.googleapis.com
104
+ - googleapis.com
105
+ #
106
+ # NOTE: if you set GOG_RUNNER_URL to run against a remote gog runner, add
107
+ # that host here too — it is supplied at runtime and cannot be declared.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gogcli-mcp-gmail",
3
- "version": "2.25.0",
3
+ "version": "2.27.0",
4
4
  "mcpName": "io.github.chrischall/gogcli-mcp-gmail",
5
5
  "description": "Extended Gmail MCP server via gogcli — auth + full Gmail support (threads, labels, drafts, attachments, forward, autoreply, bulk operations)",
6
6
  "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -24,7 +24,7 @@
24
24
  "test:coverage": "vitest run --coverage"
25
25
  },
26
26
  "dependencies": {
27
- "@chrischall/mcp-utils": "^0.14.0",
27
+ "@chrischall/mcp-utils": "^0.15.0",
28
28
  "@modelcontextprotocol/sdk": "^1.30.0",
29
29
  "zod": "^4.4.3"
30
30
  },
@@ -2,29 +2,9 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { z } from 'zod';
3
3
  import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
4
4
  import { rawTextResult, textResult, errorResult } from '@chrischall/mcp-utils';
5
- import { accountParam, runOrDiagnose, run, diagnose, payloadArg, runExecutor, normalizeTimestamps, finalizeGmailSearch, fetchGmailPages, pageTokenParam, pageAliasParam, resolvePageToken, attachInlineParam, inlineAttachmentArgs} from '../../../gogcli-mcp/src/lib.js';
5
+ import { accountParam, runOrDiagnose, run, diagnose, payloadArg, runExecutor, normalizeTimestamps, finalizeGmailSearch, fetchGmailPages, pageTokenParam, pageAliasParam, resolvePageToken, attachInlineParam, inlineAttachmentArgs, assertNotBoth, replySchema, appendReplyFlags} from '../../../gogcli-mcp/src/lib.js';
6
6
  import type { GogArg, InlineAttachmentInput } from '../../../gogcli-mcp/src/lib.js';
7
7
 
8
- // gog rejects an inline flag together with its --*-file twin — `gmail drafts
9
- // create` errors with "use only one of --body-html or --body-html-file", and
10
- // `gmail forward` does the same for --note (misreporting it as --body). Catch
11
- // the conflict here so the caller gets a message naming the TOOL params it
12
- // actually passed, instead of a gog error naming flags it never saw.
13
- function assertNotBoth(
14
- inlineParam: string,
15
- fileParam: string,
16
- inlineValue: string | undefined,
17
- fileValue: string | undefined,
18
- ): void {
19
- if (inlineValue !== undefined && fileValue !== undefined) {
20
- throw new Error(
21
- `${inlineParam} and ${fileParam} are mutually exclusive — gog accepts only one of them. ` +
22
- `Pass ${inlineParam} with the content itself (it is written to a temp file automatically when large), ` +
23
- `or ${fileParam} with a path that already exists on the gog server.`,
24
- );
25
- }
26
- }
27
-
28
8
  // Pull the text out of a single-text-block tool result; undefined for any
29
9
  // other shape (an error result is still a text block, so it parses below).
30
10
  function resultText(result: CallToolResult): string | undefined {
@@ -2889,7 +2869,7 @@ export function registerExtraGmailTools(server: McpServer): void {
2889
2869
  replyToMessageId: z.string().optional().describe('Reply to a specific Gmail MESSAGE id — the short hex `id` field from gog_gmail_get / _search / _thread_get (e.g. 19e7593d77fd9636), NOT a thread id and NOT the RFC822 `<…@host>` Message-Id header. Anchors In-Reply-To/References to that exact message. To reply to a thread when you don\'t know the latest message, use replyToThreadId instead. If both are given, replyToMessageId wins.'),
2890
2870
  replyToThreadId: z.string().optional().describe('Reply to a Gmail THREAD id — passed to gog as --thread-id, which threads the draft using the thread\'s latest-message headers (In-Reply-To/References). This is what "reply to this thread" almost always means. Mutually exclusive with replyToMessageId (which wins if both are set). Thread ids and message ids are both 16-hex strings and easy to confuse — use this param, not replyToMessageId, when the id came from a thread.'),
2891
2871
  replyTo: z.string().optional().describe('Reply-To header address'),
2892
- quote: z.boolean().optional().describe('Include quoted original message in reply (requires replyToMessageId or replyToThreadId)'),
2872
+ quote: z.boolean().optional().describe('Include the original message quoted below the body. Requires replyToMessageId or replyToThreadId. DEFAULTS OFF: a draft created with a reply target but without this threads correctly and still reads as a brand-new message, because gog only quotes by default on its reply subcommands. For a real reply draft prefer gog_gmail_drafts_reply / gog_gmail_drafts_reply_all, which also inherit the recipients and the "Re:" subject that this tool leaves to you.'),
2893
2873
  replyAll: z.boolean().optional().describe('Auto-populate recipients from the original message (reply-all), inferring To/Cc from it. Requires replyToMessageId or replyToThreadId. Explicit to/cc/bcc still apply on top; omitRecipients still suppresses them.'),
2894
2874
  attach: z.array(z.string()).optional().describe('File paths to attach (repeatable), resolved ON THE GOG SERVER\'s filesystem — NOT this client\'s. Only usable when gog runs on the same machine you do (local stdio); on the hosted connector or any GOG_RUNNER_URL backend these paths do not exist and the call fails with "no such file or directory" — use attachInline there. Read on the server, base64-encoded with a MIME type inferred from the extension. The JSON result echoes attached filenames and byte sizes — check it to confirm the files were found and embedded. On gog_gmail_drafts_update, supplying attach REPLACES the draft\'s existing attachments; omitting it preserves them (use clearAttachments to remove all).'),
2895
2875
  attachInline: attachInlineParam,
@@ -3216,97 +3196,11 @@ export function registerExtraGmailTools(server: McpServer): void {
3216
3196
  return runOrDiagnose(args, { account });
3217
3197
  });
3218
3198
 
3219
- // gmail reply / reply-all share an identical flag set (gog 0.27+); they differ
3220
- // only in the subcommand and default recipient set (reply → sender; reply-all
3221
- // → every participant). Recipient flags are repeatable on the CLI, so they are
3222
- // arrays here. --to/--cc/--bcc ADD or MOVE recipients onto the inherited reply
3223
- // set; --remove drops them. Body/HTML follow the same inline-or-file shape as
3224
- // the draft tools.
3225
- const replySchema = {
3226
- messageId: z.string().describe('Gmail message ID to reply to — the short hex `id` from gog_gmail_get / _search / _messages_search (NOT the threadId, NOT the RFC822 `<…@host>` Message-Id header).'),
3227
- body: z.string().optional().describe('Reply body (plain text; required unless bodyHtml or bodyHtmlFile is set). Any size — a large body is written to a temp file on the gog server rather than inlined into the command line. Note gog strips trailing newlines from a file-delivered body.'),
3228
- bodyHtml: z.string().optional().describe('Reply body (HTML; optional). Pass the HTML itself at any size — a large body is written to a temp file on the gog server rather than inlined into the command line. Mutually exclusive with bodyHtmlFile.'),
3229
- bodyHtmlFile: z.string().optional().describe('Path to an HTML file that ALREADY EXISTS on the gog server for the reply body. gog also accepts "-" for stdin, but this server never writes to gog\'s stdin, so "-" would hang until the call times out. Mutually exclusive with bodyHtml — supplying both is rejected. You rarely need this: bodyHtml handles large bodies on its own.'),
3230
- to: z.array(z.string()).optional().describe('Add or move recipients to To (repeatable). Added on top of the recipients inherited from the original message.'),
3231
- cc: z.array(z.string()).optional().describe('Add or move recipients to Cc (repeatable)'),
3232
- bcc: z.array(z.string()).optional().describe('Add or move recipients to Bcc (repeatable)'),
3233
- remove: z.array(z.string()).optional().describe('Remove these recipients from all fields (repeatable) — e.g. to drop someone from a reply-all.'),
3234
- subject: z.string().optional().describe('Override reply subject (default: "Re: <original>"). A changed subject starts a NEW Gmail thread.'),
3235
- noQuote: z.boolean().optional().describe('Do not include the original message quoted below the reply (default: the original is quoted)'),
3236
- attach: z.array(z.string()).optional().describe('File paths to attach (repeatable), resolved ON THE GOG SERVER\'s filesystem — NOT this client\'s. Only usable when gog runs on the same machine you do (local stdio); on the hosted connector or any GOG_RUNNER_URL backend these paths do not exist and the call fails with "no such file or directory" — use attachInline there. Read on the server, base64-encoded with a MIME type inferred from the extension.'),
3237
- attachInline: attachInlineParam,
3238
- from: z.string().optional().describe('Send from this email address (must be a verified send-as alias)'),
3239
- autoFromAddressedAlias: z.boolean().optional().describe('When from is omitted, send from the verified send-as alias the original message was addressed TO, instead of the account\'s primary address — so a reply to mail sent to an alias goes back out from that alias. Ignored when from is set.'),
3240
- signature: z.boolean().optional().describe('Append the Gmail signature from the active send-as address'),
3241
- signatureFrom: z.string().optional().describe('Append the Gmail signature from this send-as email address'),
3242
- signatureFile: z.string().optional().describe('Append a local signature file (plain text or HTML), read on the gog server'),
3243
- account: accountParam,
3244
- };
3245
-
3246
- type ReplyFlags = {
3247
- body?: string;
3248
- bodyHtml?: string;
3249
- bodyHtmlFile?: string;
3250
- to?: string[];
3251
- cc?: string[];
3252
- bcc?: string[];
3253
- remove?: string[];
3254
- subject?: string;
3255
- noQuote?: boolean;
3256
- attach?: string[];
3257
- attachInline?: InlineAttachmentInput[];
3258
- from?: string;
3259
- autoFromAddressedAlias?: boolean;
3260
- signature?: boolean;
3261
- signatureFrom?: string;
3262
- signatureFile?: string;
3263
- };
3264
-
3265
- function appendReplyFlags(args: GogArg[], f: ReplyFlags): void {
3266
- assertNotBoth('bodyHtml', 'bodyHtmlFile', f.bodyHtml, f.bodyHtmlFile);
3267
- if (f.body) args.push(payloadArg('body', 'body-file', f.body));
3268
- if (f.bodyHtml) args.push(payloadArg('body-html', 'body-html-file', f.bodyHtml, 'html'));
3269
- else if (f.bodyHtmlFile) args.push(`--body-html-file=${f.bodyHtmlFile}`);
3270
- if (f.to) for (const r of f.to) args.push(`--to=${r}`);
3271
- if (f.cc) for (const r of f.cc) args.push(`--cc=${r}`);
3272
- if (f.bcc) for (const r of f.bcc) args.push(`--bcc=${r}`);
3273
- if (f.remove) for (const r of f.remove) args.push(`--remove=${r}`);
3274
- if (f.subject) args.push(`--subject=${f.subject}`);
3275
- if (f.noQuote) args.push('--no-quote');
3276
- if (f.attach) for (const p of f.attach) args.push(`--attach=${p}`);
3277
- args.push(...inlineAttachmentArgs('attach', f.attachInline, args)); // see appendDraftFlags
3278
- if (f.from) args.push(`--from=${f.from}`);
3279
- if (f.signature) args.push('--signature');
3280
- if (f.signatureFrom) args.push(`--signature-from=${f.signatureFrom}`);
3281
- if (f.signatureFile) args.push(`--signature-file=${f.signatureFile}`);
3282
- args.push(f.autoFromAddressedAlias ? '--auto-from-addressed-alias' : '--auto-from-addressed-alias=false'); // PINNED — see appendDraftFlags
3283
- }
3284
-
3285
- server.registerTool('gog_gmail_reply', {
3286
- description: 'Reply to a Gmail message (sends to the original sender only). Threads off the message and inherits a "Re:" subject and the quoted original by default. For replying to every participant use gog_gmail_reply_all; to reply across many messages matching a query use gog_gmail_autoreply; to stage this same reply without sending it use gog_gmail_drafts_reply, which composes exactly what this tool would send.',
3287
- annotations: { destructiveHint: true },
3288
- inputSchema: replySchema,
3289
- }, async ({ messageId, account, ...flags }) => {
3290
- const args: GogArg[] = ['gmail', 'reply', messageId];
3291
- appendReplyFlags(args, flags);
3292
- return runOrDiagnose(args, { account });
3293
- });
3294
-
3295
- server.registerTool('gog_gmail_reply_all', {
3296
- description: 'Reply to all participants of a Gmail message (sender plus every To/Cc recipient). Same inherited "Re:" subject and quoting as gog_gmail_reply. Use the remove flag to drop specific recipients from the reply-all. To stage it without sending use gog_gmail_drafts_reply_all.',
3297
- annotations: { destructiveHint: true },
3298
- inputSchema: replySchema,
3299
- }, async ({ messageId, account, ...flags }) => {
3300
- const args: GogArg[] = ['gmail', 'reply-all', messageId];
3301
- appendReplyFlags(args, flags);
3302
- return runOrDiagnose(args, { account });
3303
- });
3304
-
3305
3199
  // gog >= 0.36.0: the draft-side twins of reply / reply-all / forward. They
3306
3200
  // take the SAME flag set as the send-side commands and share the composition
3307
- // path with them, so the schemas above are reused verbatim rather than
3308
- // re-declared the only difference is the subcommand and that NOTHING IS
3309
- // SENT.
3201
+ // path with them, so replySchema/appendReplyFlags are imported from the base
3202
+ // package (where gog_gmail_reply itself now lives) rather than re-declared
3203
+ // the only difference is the subcommand and that NOTHING IS SENT.
3310
3204
  //
3311
3205
  // These exist because staging a reply used to mean gog_gmail_drafts_create
3312
3206
  // with replyToMessageId/replyToThreadId, which threads the draft but does NOT
@@ -1742,96 +1742,6 @@ describe('gog_gmail_forward', () => {
1742
1742
  });
1743
1743
  });
1744
1744
 
1745
- describe('gog_gmail_reply', () => {
1746
- it('calls runOrDiagnose with messageId and --body', async () => {
1747
- await harness.callTool('gog_gmail_reply', { messageId: 'm1', body: 'Thanks' });
1748
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1749
- ['gmail', 'reply', 'm1', '--body=Thanks', '--auto-from-addressed-alias=false'],
1750
- { account: undefined },
1751
- );
1752
- });
1753
-
1754
- it('passes all reply flags including repeatable recipients', async () => {
1755
- await harness.callTool('gog_gmail_reply', {
1756
- messageId: 'm1',
1757
- body: 'Hi',
1758
- bodyHtml: '<p>Hi</p>',
1759
- to: ['a@b.com', 'c@d.com'],
1760
- cc: ['cc@x.com'],
1761
- bcc: ['bcc@x.com'],
1762
- remove: ['old@x.com'],
1763
- subject: 'New subject',
1764
- noQuote: true,
1765
- attach: ['/tmp/a.pdf', '/tmp/b.pdf'],
1766
- from: 'me@x.com',
1767
- signature: true,
1768
- signatureFrom: 'alias@x.com',
1769
- signatureFile: '/tmp/sig.txt',
1770
- account: 'me@gmail.com',
1771
- });
1772
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1773
- [
1774
- 'gmail', 'reply', 'm1',
1775
- '--body=Hi',
1776
- '--body-html=<p>Hi</p>',
1777
- '--to=a@b.com',
1778
- '--to=c@d.com',
1779
- '--cc=cc@x.com',
1780
- '--bcc=bcc@x.com',
1781
- '--remove=old@x.com',
1782
- '--subject=New subject',
1783
- '--no-quote',
1784
- '--attach=/tmp/a.pdf',
1785
- '--attach=/tmp/b.pdf',
1786
- '--from=me@x.com',
1787
- '--signature',
1788
- '--signature-from=alias@x.com',
1789
- '--signature-file=/tmp/sig.txt', '--auto-from-addressed-alias=false'
1790
- ],
1791
- { account: 'me@gmail.com' },
1792
- );
1793
- });
1794
-
1795
- it('omits --no-quote and --signature when false', async () => {
1796
- await harness.callTool('gog_gmail_reply', { messageId: 'm1', body: 'Hi', noQuote: false, signature: false });
1797
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1798
- ['gmail', 'reply', 'm1', '--body=Hi', '--auto-from-addressed-alias=false'],
1799
- { account: undefined },
1800
- );
1801
- });
1802
- });
1803
-
1804
- describe('gog_gmail_reply_all', () => {
1805
- it('uses the reply-all subcommand', async () => {
1806
- await harness.callTool('gog_gmail_reply_all', { messageId: 'm1', body: 'Thanks all' });
1807
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1808
- ['gmail', 'reply-all', 'm1', '--body=Thanks all', '--auto-from-addressed-alias=false'],
1809
- { account: undefined },
1810
- );
1811
- });
1812
-
1813
- it('passes repeatable recipient and signature flags', async () => {
1814
- await harness.callTool('gog_gmail_reply_all', {
1815
- messageId: 'm1',
1816
- bodyHtml: '<p>Hi</p>',
1817
- cc: ['x@y.com', 'z@y.com'],
1818
- remove: ['drop@y.com'],
1819
- signatureFile: '/tmp/sig.html',
1820
- });
1821
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1822
- [
1823
- 'gmail', 'reply-all', 'm1',
1824
- '--body-html=<p>Hi</p>',
1825
- '--cc=x@y.com',
1826
- '--cc=z@y.com',
1827
- '--remove=drop@y.com',
1828
- '--signature-file=/tmp/sig.html', '--auto-from-addressed-alias=false'
1829
- ],
1830
- { account: undefined },
1831
- );
1832
- });
1833
- });
1834
-
1835
1745
  // gog 0.36.0 (openclaw/gogcli#977) added the draft-side twins of reply /
1836
1746
  // reply-all / forward. The point of these tests is the SUBCOMMAND: the flag
1837
1747
  // handling is the send path's, shared verbatim, and a copy of it here would
@@ -2524,24 +2434,6 @@ describe('large payloads route to file args', () => {
2524
2434
  expect(args()).not.toContain('--thread-id=t1');
2525
2435
  });
2526
2436
 
2527
- it('gog_gmail_reply routes a large body and bodyHtml to file args', async () => {
2528
- await harness.callTool('gog_gmail_reply', { messageId: 'm1', body: big, bodyHtml: bigHtml });
2529
- expect(args()).toEqual([
2530
- 'gmail', 'reply', 'm1',
2531
- { kind: 'file', flag: 'body-file', contents: big, ext: undefined },
2532
- { kind: 'file', flag: 'body-html-file', contents: bigHtml, ext: 'html' }, '--auto-from-addressed-alias=false'
2533
- ]);
2534
- });
2535
-
2536
- it('gog_gmail_reply_all routes a large body to --body-file, leaving the signature boolean a bare flag', async () => {
2537
- await harness.callTool('gog_gmail_reply_all', { messageId: 'm1', body: big, signature: true });
2538
- expect(args()).toEqual([
2539
- 'gmail', 'reply-all', 'm1',
2540
- { kind: 'file', flag: 'body-file', contents: big, ext: undefined },
2541
- '--signature', '--auto-from-addressed-alias=false'
2542
- ]);
2543
- });
2544
-
2545
2437
  it('gog_gmail_forward routes a large note to --note-file', async () => {
2546
2438
  await harness.callTool('gog_gmail_forward', { messageId: 'm1', to: 'a@b.com', note: big });
2547
2439
  expect(args()).toEqual([
@@ -2587,33 +2479,6 @@ describe('inline/file param conflicts are rejected before gog runs', () => {
2587
2479
  expect((res.content[0] as { text: string }).text).toContain('mutually exclusive');
2588
2480
  expect(lib.runOrDiagnose).not.toHaveBeenCalled();
2589
2481
  });
2590
-
2591
- it('gog_gmail_reply rejects bodyHtml plus bodyHtmlFile', async () => {
2592
- const res = await harness.callTool('gog_gmail_reply', {
2593
- messageId: 'm1', bodyHtml: '<p>Hi</p>', bodyHtmlFile: '/tmp/b.html',
2594
- });
2595
- expect(res.isError).toBe(true);
2596
- expect((res.content[0] as { text: string }).text).toContain('bodyHtml and bodyHtmlFile are mutually exclusive');
2597
- expect(lib.runOrDiagnose).not.toHaveBeenCalled();
2598
- });
2599
-
2600
- it('an empty-string bodyHtml still counts as supplied and conflicts', async () => {
2601
- // Guards the `!== undefined` check against a falsy-but-present value
2602
- // sliding through to gog, which rejects the pair regardless of content.
2603
- const res = await harness.callTool('gog_gmail_reply', {
2604
- messageId: 'm1', body: 'B', bodyHtml: '', bodyHtmlFile: '/tmp/b.html',
2605
- });
2606
- expect(res.isError).toBe(true);
2607
- expect(lib.runOrDiagnose).not.toHaveBeenCalled();
2608
- });
2609
-
2610
- it('bodyHtmlFile alone still passes through as --body-html-file', async () => {
2611
- await harness.callTool('gog_gmail_reply', { messageId: 'm1', body: 'Hi', bodyHtmlFile: '/tmp/b.html' });
2612
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(
2613
- ['gmail', 'reply', 'm1', '--body=Hi', '--body-html-file=/tmp/b.html', '--auto-from-addressed-alias=false'],
2614
- { account: undefined },
2615
- );
2616
- });
2617
2482
  });
2618
2483
 
2619
2484
  // ---------------------------------------------------------------------------
@@ -2808,15 +2673,6 @@ describe('gog 0.35.0 — --auto-from-addressed-alias is pinned on every send-sha
2808
2673
  await harness.callTool('gog_gmail_drafts_update', { draftId: 'd1', subject: 'S', body: 'B', autoFromAddressedAlias: true });
2809
2674
  expect(args()).toEqual(['gmail', 'drafts', 'update', 'd1', '--subject=S', '--body=B', '--auto-from-addressed-alias']);
2810
2675
  });
2811
-
2812
- it('gog_gmail_reply and gog_gmail_reply_all pin it', async () => {
2813
- await harness.callTool('gog_gmail_reply', { messageId: 'm1', body: 'Hi' });
2814
- expect(args()).toEqual(['gmail', 'reply', 'm1', '--body=Hi', '--auto-from-addressed-alias=false']);
2815
- vi.clearAllMocks();
2816
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
2817
- await harness.callTool('gog_gmail_reply_all', { messageId: 'm1', body: 'Hi', autoFromAddressedAlias: true });
2818
- expect(args()).toEqual(['gmail', 'reply-all', 'm1', '--body=Hi', '--auto-from-addressed-alias']);
2819
- });
2820
2676
  });
2821
2677
 
2822
2678
  describe('gog 0.35.0 — gog_gmail_import', () => {
@@ -2871,8 +2727,6 @@ describe('server-side file params never advertise stdin as usable', () => {
2871
2727
  ['gog_gmail_import', 'file'],
2872
2728
  ['gog_gmail_drafts_create', 'bodyHtmlFile'],
2873
2729
  ['gog_gmail_drafts_update', 'bodyHtmlFile'],
2874
- ['gog_gmail_reply', 'bodyHtmlFile'],
2875
- ['gog_gmail_reply_all', 'bodyHtmlFile'],
2876
2730
  ];
2877
2731
 
2878
2732
  async function paramDescriptions(): Promise<Map<string, Record<string, { description?: string }>>> {