gogcli-mcp 2.24.0 → 2.26.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,82 @@ 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 rawTextResult(text) {
31045
+ return { content: [{ type: "text", text }] };
31046
+ }
31047
+ function errorResult(message) {
31048
+ return {
31049
+ content: [{ type: "text", text: redactSecrets(message) }],
31050
+ isError: true
31051
+ };
31052
+ }
31053
+
31003
31054
  // ../../node_modules/@chrischall/mcp-utils/dist/server/index.js
31055
+ function hintResultOrRethrow(err) {
31056
+ if (err instanceof McpToolError && err.hint) {
31057
+ return errorResult(`${err.message}
31058
+
31059
+ Hint: ${err.hint}`);
31060
+ }
31061
+ throw err;
31062
+ }
31063
+ function surfaceToolHints(server) {
31064
+ const register = server.registerTool.bind(server);
31065
+ server.registerTool = (name, config2, cb) => register(name, config2, (...args) => {
31066
+ let result;
31067
+ try {
31068
+ result = cb(...args);
31069
+ } catch (err) {
31070
+ return hintResultOrRethrow(err);
31071
+ }
31072
+ return result instanceof Promise ? result.catch(hintResultOrRethrow) : result;
31073
+ });
31074
+ }
31004
31075
  async function createMcpServer(opts) {
31005
31076
  const server = new McpServer({ name: opts.name, version: opts.version });
31077
+ if (opts.surfaceHints !== false)
31078
+ surfaceToolHints(server);
31006
31079
  if (opts.banner !== void 0) {
31007
31080
  console.error(opts.banner);
31008
31081
  }
@@ -31047,46 +31120,6 @@ async function runMcp(opts) {
31047
31120
  return server;
31048
31121
  }
31049
31122
 
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 rawTextResult(text) {
31081
- return { content: [{ type: "text", text }] };
31082
- }
31083
- function errorResult(message) {
31084
- return {
31085
- content: [{ type: "text", text: redactSecrets(message) }],
31086
- isError: true
31087
- };
31088
- }
31089
-
31090
31123
  // ../../node_modules/@chrischall/mcp-utils/dist/config/index.js
31091
31124
  var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
31092
31125
  function readEnvVar(key, opts = {}) {
@@ -31177,10 +31210,11 @@ function sanitizedEnv() {
31177
31210
  }
31178
31211
  return result;
31179
31212
  }
31213
+ var TOKEN_LEFT_BOUNDARY = "(?<![A-Za-z0-9+/])";
31180
31214
  var GOOGLE_TOKEN_PATTERNS = [
31181
- /ya29\.[A-Za-z0-9._\-]+/g,
31215
+ new RegExp(`${TOKEN_LEFT_BOUNDARY}ya29\\.[A-Za-z0-9._\\-]+`, "g"),
31182
31216
  // OAuth2 access tokens
31183
- /1\/\/[A-Za-z0-9._\-]+/g
31217
+ new RegExp(`${TOKEN_LEFT_BOUNDARY}1//[A-Za-z0-9._\\-]+`, "g")
31184
31218
  // OAuth2 refresh tokens
31185
31219
  ];
31186
31220
  function redactGoogleTokens(text) {
@@ -31193,6 +31227,26 @@ function redactGoogleTokens(text) {
31193
31227
  function redactSecrets2(text) {
31194
31228
  return redactGoogleTokens(redactSecrets(text));
31195
31229
  }
31230
+ var OPAQUE_FIELD_VALUE = "[A-Za-z0-9+/_-]{16,}={0,2}";
31231
+ var opaquePlaceholder = (i) => `\0gogOpaque${i}\0`;
31232
+ function redactPreservingOpaqueFields(text, fields, redact) {
31233
+ const lifted = [];
31234
+ let staged = text;
31235
+ for (const field of fields) {
31236
+ const escaped = field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
31237
+ const re = new RegExp(`("${escaped}"\\s*:\\s*")(${OPAQUE_FIELD_VALUE})(")`, "g");
31238
+ staged = staged.replace(re, (_m, open, value, close) => {
31239
+ lifted.push(value);
31240
+ return `${open}${opaquePlaceholder(lifted.length - 1)}${close}`;
31241
+ });
31242
+ }
31243
+ if (lifted.length === 0) return redact(text);
31244
+ let redacted = redact(staged);
31245
+ lifted.forEach((value, i) => {
31246
+ redacted = redacted.split(opaquePlaceholder(i)).join(value);
31247
+ });
31248
+ return redacted;
31249
+ }
31196
31250
  function augmentedPath() {
31197
31251
  const home = process.env.HOME;
31198
31252
  const candidates = [
@@ -31223,19 +31277,24 @@ function formatTimeout(ms) {
31223
31277
  return `${ms}ms`;
31224
31278
  }
31225
31279
  async function spawnWithTempFiles(args, opts) {
31226
- const { mkdtemp, writeFile, rm } = await import("node:fs/promises");
31280
+ const { mkdtemp, mkdir, writeFile, rm } = await import("node:fs/promises");
31227
31281
  const { tmpdir } = await import("node:os");
31228
31282
  const dir = await mkdtemp(join(tmpdir(), "gogcli-mcp-"));
31229
31283
  try {
31230
31284
  const argv = [];
31285
+ let seq = 0;
31231
31286
  for (const arg of args) {
31232
31287
  if (!isGogFileArg(arg)) {
31233
31288
  argv.push(arg);
31234
31289
  continue;
31235
31290
  }
31236
- const path = join(dir, `${arg.flag}.${arg.ext ?? "txt"}`);
31237
- await writeFile(path, arg.contents, { encoding: "utf8", mode: 384 });
31238
- argv.push(`--${arg.flag}=${path}`);
31291
+ const sub = join(dir, String(seq));
31292
+ seq += 1;
31293
+ await mkdir(sub, { recursive: true, mode: 448 });
31294
+ const path = join(sub, arg.filename ?? `${arg.flag}.${arg.ext ?? "txt"}`);
31295
+ const data = arg.encoding === "base64" ? Buffer.from(arg.contents, "base64") : Buffer.from(arg.contents, "utf8");
31296
+ await writeFile(path, data, { mode: 384 });
31297
+ argv.push(arg.positional ? path : `--${arg.flag}=${path}`);
31239
31298
  }
31240
31299
  return await spawnGog(argv, opts);
31241
31300
  } finally {
@@ -31320,8 +31379,9 @@ function assembleArgs(args, opts) {
31320
31379
  return fullArgs;
31321
31380
  }
31322
31381
  async function run(args, options = {}) {
31323
- const { account, spawner, interactive = false, timeout, readonly: readonly2 = false, redactMode = "full" } = options;
31324
- const redact = redactMode === "tokens" ? redactGoogleTokens : redactSecrets2;
31382
+ const { account, spawner, interactive = false, timeout, readonly: readonly2 = false, redactMode = "full", opaqueFields } = options;
31383
+ const base = redactMode === "tokens" ? redactGoogleTokens : redactSecrets2;
31384
+ const redact = opaqueFields?.length ? (text) => redactPreservingOpaqueFields(text, opaqueFields, base) : base;
31325
31385
  const fullArgs = assembleArgs(args, { account, interactive, readonly: readonly2 });
31326
31386
  const store = activeExecutor();
31327
31387
  try {
@@ -31335,7 +31395,7 @@ async function run(args, options = {}) {
31335
31395
  }
31336
31396
  return redact(output);
31337
31397
  } catch (err) {
31338
- const message = redact(err instanceof Error ? err.message : String(err));
31398
+ const message = base(err instanceof Error ? err.message : String(err));
31339
31399
  if (isRunnerTransportError(err)) {
31340
31400
  throw new RunnerTransportError(message, err.kind, err.status);
31341
31401
  }
@@ -31651,6 +31711,12 @@ var paginationParams = {
31651
31711
  page: pageAliasParam,
31652
31712
  all: external_exports.boolean().optional().describe("Fetch all pages")
31653
31713
  };
31714
+ function pushPaginationFlags(args, p) {
31715
+ if (p.max !== void 0) args.push(`--max=${p.max}`);
31716
+ const token = resolvePageToken(p);
31717
+ if (token) args.push(`--page=${token}`);
31718
+ if (p.all) args.push("--all");
31719
+ }
31654
31720
  function registerRunTool(server, options) {
31655
31721
  const { service, examples, omitAccount = false, note } = options;
31656
31722
  const baseDescription = `Run any gog ${service} subcommand not covered by the other tools. Run \`gog ${service} --help\` for the full list of subcommands, or \`gog ${service} <subcommand> --help\` for flags on a specific subcommand.`;
@@ -31824,6 +31890,117 @@ function registerApiTools(server) {
31824
31890
  });
31825
31891
  }
31826
31892
 
31893
+ // src/tools/appscript.ts
31894
+ function registerAppScriptTools(server) {
31895
+ const scriptIdParam = external_exports.string().describe(
31896
+ "Apps Script project ID \u2014 the long ID in script.google.com/\u2026/projects/<scriptId>/\u2026, NOT the Drive file ID of a container document"
31897
+ );
31898
+ const apiEnableNote = " Needs the Apps Script API enabled on the OAuth client's Google Cloud project; if it is not, gog says so and prints the console URL to enable it. That is a project setting, not a missing scope \u2014 re-authorizing will not fix it.";
31899
+ server.registerTool("gog_appscript_get", {
31900
+ description: "Get an Apps Script project's metadata: title, creator, create/update times, and the parent Drive file when the project is bound to a Sheet, Doc or Form. Use gog_appscript_content to read the actual code." + apiEnableNote,
31901
+ annotations: { readOnlyHint: true },
31902
+ inputSchema: {
31903
+ scriptId: scriptIdParam,
31904
+ account: accountParam
31905
+ }
31906
+ }, async ({ scriptId, account }) => {
31907
+ return runOrDiagnose(["appscript", "get", scriptId], { account });
31908
+ });
31909
+ server.registerTool("gog_appscript_content", {
31910
+ description: `Read a project's source \u2014 every .gs file and its appsscript.json manifest \u2014 INLINE in the response. This is the tool to reach for when the question is "what does this script do"; it needs no filesystem, so it works the same on a hosted deployment as it does locally, unlike gog_appscript_pull.` + apiEnableNote,
31911
+ annotations: { readOnlyHint: true },
31912
+ inputSchema: {
31913
+ scriptId: scriptIdParam,
31914
+ account: accountParam
31915
+ }
31916
+ }, async ({ scriptId, account }) => {
31917
+ return runOrDiagnose(["appscript", "content", scriptId], { account });
31918
+ });
31919
+ server.registerTool("gog_appscript_pull", {
31920
+ description: "Write a project's files into a local directory, for editing a script as ordinary files. THE DIRECTORY IS RESOLVED WHERE GOG RUNS, which is the caller's own machine only on a local (stdio) deployment: on the hosted connector, or any GOG_RUNNER_URL backend, the files land on that server where the caller cannot reach them. Use gog_appscript_content there instead \u2014 it returns the same source in the response. Existing files are left alone unless overwrite is set. Read-only as far as Google is concerned: nothing is pushed back." + apiEnableNote,
31921
+ inputSchema: {
31922
+ scriptId: scriptIdParam,
31923
+ dir: external_exports.string().describe("Destination directory, resolved on the machine where gog runs"),
31924
+ overwrite: external_exports.boolean().optional().describe("Overwrite files that already exist in dir"),
31925
+ account: accountParam
31926
+ }
31927
+ }, async ({ scriptId, dir, overwrite, account }) => {
31928
+ const args = ["appscript", "pull", scriptId, dir];
31929
+ if (overwrite) args.push("--overwrite");
31930
+ return runOrDiagnose(args, { account });
31931
+ });
31932
+ server.registerTool("gog_appscript_create", {
31933
+ description: "Create a new, empty Apps Script project. Pass parentId to bind it to a Drive file (a Sheet, Doc or Form), which is what makes the script a container-bound script with access to that document; omit it for a standalone project. gog cannot upload code, so the project starts empty either way." + apiEnableNote,
31934
+ inputSchema: {
31935
+ title: external_exports.string().describe("Project title"),
31936
+ parentId: external_exports.string().optional().describe("Drive file ID to bind the project to (Sheet, Doc or Form). Omit for a standalone project."),
31937
+ account: accountParam
31938
+ }
31939
+ }, async ({ title, parentId, account }) => {
31940
+ const args = ["appscript", "create", `--title=${title}`];
31941
+ if (parentId) args.push(`--parent-id=${parentId}`);
31942
+ return runOrDiagnose(args, { account });
31943
+ });
31944
+ server.registerTool("gog_appscript_deployments", {
31945
+ description: "List a project's deployments \u2014 the published web apps, add-ons and API executables, each pinned to a version. A deployment ID from here is what gog_appscript_run_function needs when a script is not running in dev mode." + apiEnableNote,
31946
+ annotations: { readOnlyHint: true },
31947
+ inputSchema: {
31948
+ scriptId: scriptIdParam,
31949
+ ...paginationParams,
31950
+ account: accountParam
31951
+ }
31952
+ }, async ({ scriptId, max, pageToken, page, all, account }) => {
31953
+ const args = ["appscript", "deployments", scriptId];
31954
+ pushPaginationFlags(args, { max, pageToken, page, all });
31955
+ return runOrDiagnose(args, { account });
31956
+ });
31957
+ server.registerTool("gog_appscript_versions", {
31958
+ description: `List a project's saved versions \u2014 the immutable snapshots deployments point at, with their numbers and descriptions. Useful for answering "what is actually deployed" next to gog_appscript_deployments.` + apiEnableNote,
31959
+ annotations: { readOnlyHint: true },
31960
+ inputSchema: {
31961
+ scriptId: scriptIdParam,
31962
+ ...paginationParams,
31963
+ account: accountParam
31964
+ }
31965
+ }, async ({ scriptId, max, pageToken, page, all, account }) => {
31966
+ const args = ["appscript", "versions", scriptId];
31967
+ pushPaginationFlags(args, { max, pageToken, page, all });
31968
+ return runOrDiagnose(args, { account });
31969
+ });
31970
+ server.registerTool("gog_appscript_run_function", {
31971
+ description: "Execute a function in a deployed Apps Script project. TREAT THIS AS ARBITRARY CODE EXECUTION: the script runs with this Google account's authority and can send mail, edit Drive files or call external services, and the wrapper cannot tell a read from a write \u2014 read the code with gog_appscript_content first if you did not write it. Requires the project to be deployed as an API executable and to share the OAuth client with the calling credentials, otherwise Google refuses regardless of scopes. devMode runs the latest saved code instead of the deployed version, and only works if the account owns the script. This is NOT the escape hatch \u2014 gog_appscript_run is that." + apiEnableNote,
31972
+ annotations: { destructiveHint: true },
31973
+ inputSchema: {
31974
+ scriptId: scriptIdParam,
31975
+ functionName: external_exports.string().describe('Name of the function to call, e.g. "doWork"'),
31976
+ params: external_exports.string().optional().describe(`Function parameters as a JSON ARRAY of positional arguments, e.g. '["a", 1]' \u2014 not an object`),
31977
+ devMode: external_exports.boolean().optional().describe("Run the latest saved code rather than the deployed version (owner only)"),
31978
+ account: accountParam
31979
+ }
31980
+ }, async ({ scriptId, functionName, params, devMode, account }) => {
31981
+ if (params !== void 0) {
31982
+ let parsed;
31983
+ try {
31984
+ parsed = JSON.parse(params);
31985
+ } catch {
31986
+ throw new Error(`params must be a JSON array of positional arguments, e.g. '["a", 1]'. Received: ${params}`);
31987
+ }
31988
+ if (!Array.isArray(parsed)) {
31989
+ throw new Error(`params must be a JSON ARRAY of positional arguments, e.g. '["a", 1]' \u2014 Apps Script takes positional arguments, not named ones. Received: ${params}`);
31990
+ }
31991
+ }
31992
+ const args = ["appscript", "run", scriptId, functionName];
31993
+ if (params !== void 0) args.push(`--params=${params}`);
31994
+ if (devMode) args.push("--dev-mode");
31995
+ return runOrDiagnose(args, { account });
31996
+ });
31997
+ registerRunTool(server, {
31998
+ service: "appscript",
31999
+ examples: '"get", "content", "deployments"',
32000
+ note: "To execute a function, use gog_appscript_run_function \u2014 this tool is the generic escape hatch."
32001
+ });
32002
+ }
32003
+
31827
32004
  // src/tools/auth.ts
31828
32005
  function registerAuthToolsWith(server, defaultServices) {
31829
32006
  const servicesDescribe = `Services to authorize: "all" or comma-separated list (e.g. "sheets,gmail,calendar"). Default: "${defaultServices}". Prefer the narrowest set you need \u2014 requesting a service whose Google API is not enabled on the OAuth client's project makes Google reject the WHOLE request with invalid_scope.`;
@@ -31956,6 +32133,29 @@ function registerAuthTools(server) {
31956
32133
  }
31957
32134
 
31958
32135
  // src/tools/calendar.ts
32136
+ var reminderParams = {
32137
+ reminders: external_exports.array(external_exports.string()).max(5).optional().describe(
32138
+ `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.`
32139
+ ),
32140
+ noReminders: external_exports.boolean().optional().describe(
32141
+ "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."
32142
+ )
32143
+ };
32144
+ function pushReminderFlags(args, p) {
32145
+ if (p.noReminders) {
32146
+ if (p.reminders !== void 0) {
32147
+ throw new Error("reminders and noReminders are mutually exclusive: pass reminders to set custom ones, noReminders for none, or an empty reminders array to restore the calendar defaults.");
32148
+ }
32149
+ args.push("--no-reminders");
32150
+ return;
32151
+ }
32152
+ if (p.reminders === void 0) return;
32153
+ if (p.reminders.length === 0) {
32154
+ args.push("--reminder=");
32155
+ return;
32156
+ }
32157
+ for (const reminder of p.reminders) args.push(`--reminder=${reminder}`);
32158
+ }
31959
32159
  function registerCalendarTools(server) {
31960
32160
  server.registerTool("gog_calendar_events", {
31961
32161
  description: 'List calendar events. Describe the window ONE way and one way only (gog >= 0.36.0 rejects the rest as ambiguous rather than silently discarding a flag): today on its own; or from + to; or from + days; or days on its own (a window of that many days starting today). today cannot be combined with from, to or days, and days cannot be combined with to. gog returns only 10 events by default, so a wide date range is USUALLY INCOMPLETE: raise max, or page with pageToken until the response carries no nextPageToken. A response carrying "truncated": true is an incomplete view \u2014 never conclude an event does not exist from one.',
@@ -32025,9 +32225,10 @@ function registerCalendarTools(server) {
32025
32225
  allDay: external_exports.boolean().optional().describe("All-day event (use date-only in from/to)"),
32026
32226
  timezone: external_exports.string().optional().describe("IANA timezone metadata applied to from/to (e.g. America/New_York). Sets both start and end timezone unless start/end timezone are overridden."),
32027
32227
  withZoom: external_exports.boolean().optional().describe("Create a Zoom video conference for this event (requires Zoom S2S OAuth setup)"),
32228
+ ...reminderParams,
32028
32229
  account: accountParam
32029
32230
  }
32030
- }, async ({ calendarId, summary, from, to, description, location, attendees, allDay, timezone, withZoom, account }) => {
32231
+ }, async ({ calendarId, summary, from, to, description, location, attendees, allDay, timezone, withZoom, reminders, noReminders, account }) => {
32031
32232
  const args = ["calendar", "create", calendarId, `--summary=${summary}`, `--from=${from}`, `--to=${to}`];
32032
32233
  if (description) args.push(`--description=${description}`);
32033
32234
  if (location) args.push(`--location=${location}`);
@@ -32035,6 +32236,7 @@ function registerCalendarTools(server) {
32035
32236
  if (allDay) args.push("--all-day");
32036
32237
  if (timezone) args.push(`--timezone=${timezone}`);
32037
32238
  if (withZoom) args.push("--with-zoom");
32239
+ pushReminderFlags(args, { reminders, noReminders });
32038
32240
  return runOrDiagnose(args, { account });
32039
32241
  });
32040
32242
  server.registerTool("gog_calendar_update", {
@@ -32055,9 +32257,10 @@ function registerCalendarTools(server) {
32055
32257
  regenerateZoom: external_exports.boolean().optional().describe("Replace the event's existing Zoom video conference"),
32056
32258
  removeZoom: external_exports.boolean().optional().describe("Remove the event's Zoom video conference"),
32057
32259
  removeMeet: external_exports.boolean().optional().describe("Remove the event's Google Meet video conference (clears conference data only)"),
32260
+ ...reminderParams,
32058
32261
  account: accountParam
32059
32262
  }
32060
- }, async ({ calendarId, eventId, summary, from, to, description, location, attendees, addAttendees, attachments, withZoom, regenerateZoom, removeZoom, removeMeet, account }) => {
32263
+ }, async ({ calendarId, eventId, summary, from, to, description, location, attendees, addAttendees, attachments, withZoom, regenerateZoom, removeZoom, removeMeet, reminders, noReminders, account }) => {
32061
32264
  const args = ["calendar", "update", calendarId, eventId];
32062
32265
  if (summary !== void 0) args.push(`--summary=${summary}`);
32063
32266
  if (from !== void 0) args.push(`--from=${from}`);
@@ -32071,6 +32274,7 @@ function registerCalendarTools(server) {
32071
32274
  if (regenerateZoom) args.push("--regenerate-zoom");
32072
32275
  if (removeZoom) args.push("--remove-zoom");
32073
32276
  if (removeMeet) args.push("--remove-meet");
32277
+ pushReminderFlags(args, { reminders, noReminders });
32074
32278
  return runOrDiagnose(args, { account });
32075
32279
  });
32076
32280
  server.registerTool("gog_calendar_delete", {
@@ -32102,6 +32306,255 @@ function registerCalendarTools(server) {
32102
32306
  registerRunTool(server, { service: "calendar", examples: '"calendars", "freebusy"' });
32103
32307
  }
32104
32308
 
32309
+ // src/attachments.ts
32310
+ var MAX_INLINE_ATTACHMENT_BYTES = 8 * 1024 * 1024;
32311
+ var RUNNER_MAX_BODY_BYTES = 32 * 1024 * 1024;
32312
+ var RUNNER_BODY_JSON_RESERVE_BYTES = 256 * 1024;
32313
+ var MAX_REQUEST_PAYLOAD_WIRE_BYTES = RUNNER_MAX_BODY_BYTES - RUNNER_BODY_JSON_RESERVE_BYTES;
32314
+ var MAX_INLINE_ATTACHMENT_TOTAL_BYTES = Math.floor(MAX_REQUEST_PAYLOAD_WIRE_BYTES * 3 / 4);
32315
+ function wireBytesOf(arg) {
32316
+ if (typeof arg === "string") return Buffer.byteLength(arg, "utf8");
32317
+ return arg.encoding === "base64" ? arg.contents.length : Buffer.byteLength(arg.contents, "utf8");
32318
+ }
32319
+ var formatMiB = (bytes) => `${Math.floor(bytes / (1024 * 1024))} MiB`;
32320
+ var INLINE_ATTACHMENT_LIMITS_TEXT = `up to ${formatMiB(MAX_INLINE_ATTACHMENT_BYTES)} per file and ${formatMiB(MAX_INLINE_ATTACHMENT_TOTAL_BYTES)} in total`;
32321
+ var inlineAttachmentSchema = external_exports.object({
32322
+ filename: external_exports.string().min(1).describe(
32323
+ `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.`
32324
+ ),
32325
+ contentBase64: external_exports.string().min(1).describe(
32326
+ "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."
32327
+ )
32328
+ });
32329
+ var attachInlineParam = external_exports.array(inlineAttachmentSchema).optional().describe(
32330
+ `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.`
32331
+ );
32332
+ function validateFilename(filename, where) {
32333
+ if (/[/\\]/.test(filename)) {
32334
+ throw new Error(
32335
+ `${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".`
32336
+ );
32337
+ }
32338
+ if (/[\x00-\x1f]/.test(filename) || /^\.+$/.test(filename) || filename.length > 200) {
32339
+ throw new Error(
32340
+ `${where}: filename ${JSON.stringify(filename)} is not a usable filename (no control characters, not "."/"..", 200 characters max).`
32341
+ );
32342
+ }
32343
+ }
32344
+ function decodedLength(contentBase64) {
32345
+ const buf = Buffer.from(contentBase64, "base64");
32346
+ return buf.toString("base64") === contentBase64 ? buf.length : null;
32347
+ }
32348
+ function inlineFileArg(flag, attachment, opts = {}) {
32349
+ const { filename, contentBase64 } = attachment;
32350
+ const where = opts.where ?? `attachInline entry ${JSON.stringify(filename)}`;
32351
+ validateFilename(filename, where);
32352
+ const bytes = decodedLength(contentBase64);
32353
+ if (bytes === null) {
32354
+ throw new Error(
32355
+ `${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.`
32356
+ );
32357
+ }
32358
+ if (bytes > MAX_INLINE_ATTACHMENT_BYTES) {
32359
+ throw new Error(
32360
+ `${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.`
32361
+ );
32362
+ }
32363
+ const arg = { kind: "file", flag, contents: contentBase64, encoding: "base64", filename };
32364
+ if (opts.positional) arg.positional = true;
32365
+ return { arg, bytes };
32366
+ }
32367
+ function inlineAttachmentArgs(flag, attachments, siblingArgs = []) {
32368
+ if (!attachments?.length) return [];
32369
+ const args = [];
32370
+ const siblingWire = siblingArgs.reduce((sum, arg) => sum + wireBytesOf(arg), 0);
32371
+ let attachmentWire = 0;
32372
+ let decodedTotal = 0;
32373
+ for (const attachment of attachments) {
32374
+ const { arg, bytes } = inlineFileArg(flag, attachment);
32375
+ attachmentWire += arg.contents.length;
32376
+ decodedTotal += bytes;
32377
+ if (siblingWire + attachmentWire > MAX_REQUEST_PAYLOAD_WIRE_BYTES) {
32378
+ 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.` : "";
32379
+ throw new Error(
32380
+ `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.`
32381
+ );
32382
+ }
32383
+ args.push(arg);
32384
+ }
32385
+ return args;
32386
+ }
32387
+
32388
+ // src/tools/chat.ts
32389
+ function registerChatTools(server) {
32390
+ const workspaceOnlyNote = ' WORKSPACE ONLY: Google Chat has no API for consumer accounts, so this fails on an @gmail.com account with "chat requires a Google Workspace account". That is the ACCOUNT, not the token \u2014 re-authorizing or adding scopes will not help.';
32391
+ const spaceParam = external_exports.string().describe(
32392
+ 'Space resource name, e.g. "spaces/AAAAsomeID" (from gog_chat_spaces_list, gog_chat_spaces_find or gog_chat_dm_space)'
32393
+ );
32394
+ const threadParam = external_exports.string().optional().describe(
32395
+ 'Thread resource name, e.g. "spaces/AAAA/threads/CCCC" \u2014 reply inside that thread instead of starting a new one'
32396
+ );
32397
+ server.registerTool("gog_chat_spaces_list", {
32398
+ description: "List the Google Chat spaces the account belongs to \u2014 named rooms and DMs alike \u2014 with their resource names. Start here when you do not yet have a space name; gog_chat_spaces_find is faster when you know the room's title." + workspaceOnlyNote,
32399
+ annotations: { readOnlyHint: true },
32400
+ inputSchema: {
32401
+ ...paginationParams,
32402
+ account: accountParam
32403
+ }
32404
+ }, async ({ max, pageToken, page, all, account }) => {
32405
+ const args = ["chat", "spaces", "list"];
32406
+ pushPaginationFlags(args, { max, pageToken, page, all });
32407
+ return runOrDiagnose(args, { account });
32408
+ });
32409
+ server.registerTool("gog_chat_spaces_find", {
32410
+ description: 'Find spaces whose display name matches. Substring and case-insensitive by default, which is what you want when the user names a room approximately ("the launch room"); pass exact=true to require the whole title. DMs have no display name \u2014 use gog_chat_dm_space to reach a person.' + workspaceOnlyNote,
32411
+ annotations: { readOnlyHint: true },
32412
+ inputSchema: {
32413
+ displayName: external_exports.string().describe("Space display name, or part of one"),
32414
+ exact: external_exports.boolean().optional().describe("Require an exact (still case-insensitive) match on the whole display name"),
32415
+ max: external_exports.number().int().optional().describe("Max results per page"),
32416
+ account: accountParam
32417
+ }
32418
+ }, async ({ displayName, exact, max, account }) => {
32419
+ const args = ["chat", "spaces", "find", displayName];
32420
+ if (exact) args.push("--exact");
32421
+ if (max !== void 0) args.push(`--max=${max}`);
32422
+ return runOrDiagnose(args, { account });
32423
+ });
32424
+ server.registerTool("gog_chat_spaces_create", {
32425
+ description: "Create a named Chat space, optionally seeding its membership. Members are added immediately and are notified \u2014 this is visible to other people the moment it runs, so confirm the member list before calling it." + workspaceOnlyNote,
32426
+ inputSchema: {
32427
+ displayName: external_exports.string().describe("Display name for the new space"),
32428
+ members: external_exports.array(external_exports.string()).optional().describe('Initial members, as email addresses or "users/..." resource names'),
32429
+ account: accountParam
32430
+ }
32431
+ }, async ({ displayName, members, account }) => {
32432
+ const args = ["chat", "spaces", "create", displayName];
32433
+ if (members) for (const member of members) args.push(`--member=${member}`);
32434
+ return runOrDiagnose(args, { account });
32435
+ });
32436
+ server.registerTool("gog_chat_threads_list", {
32437
+ description: "List the threads in a space, so a reply can be targeted at an existing conversation rather than starting a new one. Pass a thread name from here as `thread` to gog_chat_messages_send." + workspaceOnlyNote,
32438
+ annotations: { readOnlyHint: true },
32439
+ inputSchema: {
32440
+ space: spaceParam,
32441
+ ...paginationParams,
32442
+ account: accountParam
32443
+ }
32444
+ }, async ({ space, max, pageToken, page, all, account }) => {
32445
+ const args = ["chat", "threads", "list", space];
32446
+ pushPaginationFlags(args, { max, pageToken, page, all });
32447
+ return runOrDiagnose(args, { account });
32448
+ });
32449
+ server.registerTool("gog_chat_messages_list", {
32450
+ description: `Read messages in a space. The JSON carries each message's @-mentions and a summary of its emoji reactions (gog >= 0.38.0) alongside the text, so "who was tagged" and "did anyone react" are answerable without extra calls. unread=true returns only what arrived after the account last read the space \u2014 the cheap way to answer "what did I miss". Newest-first needs an explicit order="createTime desc"; Chat's own default is oldest-first.` + workspaceOnlyNote,
32451
+ annotations: { readOnlyHint: true },
32452
+ inputSchema: {
32453
+ space: spaceParam,
32454
+ thread: threadParam,
32455
+ unread: external_exports.boolean().optional().describe("Only messages posted after the account last read this space"),
32456
+ order: external_exports.enum(["createTime asc", "createTime desc", "lastUpdateTime asc", "lastUpdateTime desc"]).optional().describe('Sort order (Chat default: "createTime asc", i.e. OLDEST first \u2014 ask for "createTime desc" when you want the latest messages)'),
32457
+ ...paginationParams,
32458
+ account: accountParam
32459
+ }
32460
+ }, async ({ space, thread, unread, order, max, pageToken, page, all, account }) => {
32461
+ const args = ["chat", "messages", "list", space];
32462
+ if (thread) args.push(`--thread=${thread}`);
32463
+ if (unread) args.push("--unread");
32464
+ if (order) args.push(`--order=${order}`);
32465
+ pushPaginationFlags(args, { max, pageToken, page, all });
32466
+ return runOrDiagnose(args, { account });
32467
+ });
32468
+ server.registerTool("gog_chat_messages_send", {
32469
+ description: "Post a message to a Chat space. THIS IS IMMEDIATELY VISIBLE TO EVERYONE IN THE SPACE and cannot be unsent through this tool, so treat it like sending mail, not like saving a draft. Pass `thread` to reply inside an existing conversation (from gog_chat_threads_list or a message's thread field); omit it to start a new one. Text supports Chat's markdown-ish formatting (*bold*, _italic_, `code`)." + workspaceOnlyNote,
32470
+ inputSchema: {
32471
+ space: spaceParam,
32472
+ text: external_exports.string().optional().describe("Message text. Optional only when an attachment is supplied."),
32473
+ thread: threadParam,
32474
+ attach: external_exports.array(external_exports.string()).optional().describe(
32475
+ "Attachment file paths, read WHERE GOG RUNS. On a hosted or remote deployment that is not your machine \u2014 use attachInline there instead."
32476
+ ),
32477
+ attachInline: attachInlineParam,
32478
+ account: accountParam
32479
+ }
32480
+ }, async ({ space, text, thread, attach, attachInline, account }) => {
32481
+ if (text === void 0 && !attach?.length && !attachInline?.length) {
32482
+ throw new Error("A Chat message needs text, an attachment, or both.");
32483
+ }
32484
+ const args = ["chat", "messages", "send", space];
32485
+ if (text !== void 0) args.push(`--text=${text}`);
32486
+ if (thread) args.push(`--thread=${thread}`);
32487
+ if (attach) for (const path of attach) args.push(`--attach=${path}`);
32488
+ args.push(...inlineAttachmentArgs("attach", attachInline, args));
32489
+ return runOrDiagnose(args, { account });
32490
+ });
32491
+ server.registerTool("gog_chat_dm_send", {
32492
+ description: "Send a direct message to one person by email address, creating the DM space if this is the first message. Delivered immediately and cannot be unsent through this tool. For a room rather than a person, use gog_chat_messages_send." + workspaceOnlyNote,
32493
+ inputSchema: {
32494
+ email: external_exports.string().describe("Recipient email address"),
32495
+ text: external_exports.string().describe("Message text"),
32496
+ thread: threadParam,
32497
+ account: accountParam
32498
+ }
32499
+ }, async ({ email: email3, text, thread, account }) => {
32500
+ const args = ["chat", "dm", "send", email3, `--text=${text}`];
32501
+ if (thread) args.push(`--thread=${thread}`);
32502
+ return runOrDiagnose(args, { account });
32503
+ });
32504
+ server.registerTool("gog_chat_dm_space", {
32505
+ description: 'Resolve the DM space for an email address \u2014 the bridge from a person to the "spaces/..." name the message tools want. Creates the space if none exists yet, which is silent: it does not message the person.' + workspaceOnlyNote,
32506
+ inputSchema: {
32507
+ email: external_exports.string().describe("The other person's email address"),
32508
+ account: accountParam
32509
+ }
32510
+ }, async ({ email: email3, account }) => {
32511
+ return runOrDiagnose(["chat", "dm", "space", email3], { account });
32512
+ });
32513
+ server.registerTool("gog_chat_reactions_list", {
32514
+ description: "List the emoji reactions on one message, with who reacted. gog_chat_messages_list already returns a reaction SUMMARY per message; come here when you need the individual reactors, or the reaction resource names that gog_chat_reactions_delete takes." + workspaceOnlyNote,
32515
+ annotations: { readOnlyHint: true },
32516
+ inputSchema: {
32517
+ message: external_exports.string().describe('Message resource name ("spaces/AAAA/messages/BBBB"), or a bare message ID together with `space`'),
32518
+ space: external_exports.string().optional().describe("Space resource name \u2014 required only when `message` is a bare ID"),
32519
+ ...paginationParams,
32520
+ account: accountParam
32521
+ }
32522
+ }, async ({ message, space, max, pageToken, page, all, account }) => {
32523
+ const args = ["chat", "messages", "reactions", "list", message];
32524
+ if (space) args.push(`--space=${space}`);
32525
+ pushPaginationFlags(args, { max, pageToken, page, all });
32526
+ return runOrDiagnose(args, { account });
32527
+ });
32528
+ server.registerTool("gog_chat_reactions_create", {
32529
+ description: 'React to a message with an emoji. Visible to the space immediately. Pass the emoji itself ("\u{1F44D}"), not a :shortcode:.' + workspaceOnlyNote,
32530
+ inputSchema: {
32531
+ message: external_exports.string().describe('Message resource name ("spaces/AAAA/messages/BBBB"), or a bare message ID together with `space`'),
32532
+ emoji: external_exports.string().describe('The emoji character to react with, e.g. "\u{1F44D}"'),
32533
+ space: external_exports.string().optional().describe("Space resource name \u2014 required only when `message` is a bare ID"),
32534
+ account: accountParam
32535
+ }
32536
+ }, async ({ message, emoji: emoji3, space, account }) => {
32537
+ const args = ["chat", "messages", "reactions", "create", message, emoji3];
32538
+ if (space) args.push(`--space=${space}`);
32539
+ return runOrDiagnose(args, { account });
32540
+ });
32541
+ server.registerTool("gog_chat_reactions_delete", {
32542
+ description: `Remove one emoji reaction. Takes the REACTION's own resource name ("spaces/.../messages/.../reactions/..."), not the message's and not the emoji \u2014 get it from gog_chat_reactions_list. An account can only remove its own reaction.` + workspaceOnlyNote,
32543
+ annotations: { destructiveHint: true },
32544
+ inputSchema: {
32545
+ reaction: external_exports.string().describe('Reaction resource name, e.g. "spaces/AAAA/messages/BBBB/reactions/CCCC"'),
32546
+ account: accountParam
32547
+ }
32548
+ }, async ({ reaction, account }) => {
32549
+ return runOrDiagnose(["chat", "messages", "reactions", "delete", reaction], { account });
32550
+ });
32551
+ registerRunTool(server, {
32552
+ service: "chat",
32553
+ examples: '"spaces", "messages", "dm"',
32554
+ note: "Google Chat has no API for consumer accounts: every chat subcommand fails on an @gmail.com account regardless of scopes."
32555
+ });
32556
+ }
32557
+
32105
32558
  // src/tools/classroom.ts
32106
32559
  function registerClassroomTools(server) {
32107
32560
  server.registerTool("gog_classroom_courses_list", {
@@ -32917,6 +33370,7 @@ async function fetchGmailPages(runPage, itemsKey, maxPages, startToken) {
32917
33370
  const merged = [];
32918
33371
  let base;
32919
33372
  let token = startToken;
33373
+ const fetched = new Set(startToken === void 0 ? [] : [startToken]);
32920
33374
  for (let pages = 0; pages < maxPages; pages++) {
32921
33375
  const result = await runPage(token);
32922
33376
  const parsed = parsePage(result, itemsKey);
@@ -32925,8 +33379,17 @@ async function fetchGmailPages(runPage, itemsKey, maxPages, startToken) {
32925
33379
  }
32926
33380
  base = parsed;
32927
33381
  merged.push(...parsed[itemsKey]);
32928
- token = typeof parsed.nextPageToken === "string" && parsed.nextPageToken !== "" ? parsed.nextPageToken : void 0;
32929
- if (token === void 0) break;
33382
+ const next = typeof parsed.nextPageToken === "string" && parsed.nextPageToken !== "" ? parsed.nextPageToken : void 0;
33383
+ if (next === void 0) {
33384
+ token = void 0;
33385
+ break;
33386
+ }
33387
+ if (fetched.has(next)) {
33388
+ token = next;
33389
+ break;
33390
+ }
33391
+ fetched.add(next);
33392
+ token = next;
32930
33393
  }
32931
33394
  return finish(base, itemsKey, merged, token);
32932
33395
  }
@@ -33003,7 +33466,7 @@ function registerGmailTools(server) {
33003
33466
  return runOrDiagnose(args, { account });
33004
33467
  });
33005
33468
  server.registerTool("gog_gmail_send", {
33006
- description: "Send an email. When attach is used, the JSON result echoes the attached filenames and byte sizes \u2014 check it to confirm the files were found and embedded.",
33469
+ 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.',
33007
33470
  annotations: { destructiveHint: true },
33008
33471
  inputSchema: {
33009
33472
  to: external_exports.string().describe("Recipient(s), comma-separated"),
@@ -33013,16 +33476,19 @@ function registerGmailTools(server) {
33013
33476
  bcc: external_exports.string().optional().describe("BCC recipients, comma-separated"),
33014
33477
  replyToMessageId: external_exports.string().optional().describe("Message ID to reply to"),
33015
33478
  threadId: external_exports.string().optional().describe("Thread ID to reply within"),
33016
- attach: external_exports.array(external_exports.string()).optional().describe("Local file paths to attach (repeatable). Each file is read on the gog server (not this client), base64-encoded with a MIME type inferred from its extension, and added as a multipart attachment. Keep the total under Gmail's ~35 MB inline-upload limit."),
33479
+ 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.`),
33480
+ attachInline: attachInlineParam,
33017
33481
  account: accountParam
33018
33482
  }
33019
- }, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, attach, account }) => {
33483
+ }, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, attach, attachInline, account }) => {
33020
33484
  const args = ["gmail", "send", `--to=${to}`, `--subject=${subject}`, payloadArg("body", "body-file", body)];
33021
33485
  if (cc) args.push(`--cc=${cc}`);
33022
33486
  if (bcc) args.push(`--bcc=${bcc}`);
33023
33487
  if (replyToMessageId) args.push(`--reply-to-message-id=${replyToMessageId}`);
33024
33488
  if (threadId) args.push(`--thread-id=${threadId}`);
33025
33489
  if (attach) for (const path of attach) args.push(`--attach=${path}`);
33490
+ const inline = inlineAttachmentArgs("attach", attachInline, args);
33491
+ args.push(...inline);
33026
33492
  return runOrDiagnose(args, { account });
33027
33493
  });
33028
33494
  registerRunTool(server, { service: "gmail", examples: '"archive", "mark-read", "labels"' });
@@ -33352,11 +33818,13 @@ function registerTasksTools(server) {
33352
33818
  }
33353
33819
 
33354
33820
  // src/server.ts
33355
- var VERSION = true ? "2.24.0" : "0.0.0";
33821
+ var VERSION = true ? "2.26.0" : "0.0.0";
33356
33822
  var BASE_TOOL_REGISTRARS = [
33357
33823
  registerApiTools,
33824
+ registerAppScriptTools,
33358
33825
  registerAuthTools,
33359
33826
  registerCalendarTools,
33827
+ registerChatTools,
33360
33828
  registerClassroomTools,
33361
33829
  registerContactsTools,
33362
33830
  registerDocsTools,