gogcli-mcp 2.23.1 → 2.24.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.
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "metadata": {
9
9
  "description": "Google Sheets (and more) for Claude via gogcli — read, write, and manage spreadsheets",
10
- "version": "2.23.1"
10
+ "version": "2.24.0"
11
11
  },
12
12
  "plugins": [
13
13
  {
@@ -15,7 +15,7 @@
15
15
  "displayName": "gogcli",
16
16
  "source": "./",
17
17
  "description": "Google Sheets (and more) for Claude via gogcli — read, write, and manage spreadsheets",
18
- "version": "2.23.1",
18
+ "version": "2.24.0",
19
19
  "author": {
20
20
  "name": "Chris Hall"
21
21
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gogcli-mcp",
3
3
  "displayName": "gogcli",
4
- "version": "2.23.1",
4
+ "version": "2.24.0",
5
5
  "description": "Google Sheets (and more) for Claude via gogcli — read, write, and manage spreadsheets",
6
6
  "author": {
7
7
  "name": "Chris Hall",
package/dist/index.js CHANGED
@@ -31552,6 +31552,55 @@ function normalizeTimestamps(text, tz = displayTimeZone(), naiveTz = naiveSource
31552
31552
  return JSON.stringify(parsed, null, detectIndent(text));
31553
31553
  }
31554
31554
 
31555
+ // src/pagination.ts
31556
+ function detectIndent2(text) {
31557
+ const match = /\n(\s+)\S/.exec(text);
31558
+ return match ? match[1].replace(/\t/g, " ").length : 0;
31559
+ }
31560
+ function stripConsumedPageToken(text) {
31561
+ const trimmed = text.trim();
31562
+ if (trimmed === "" || !trimmed.startsWith("{")) return text;
31563
+ let parsed;
31564
+ try {
31565
+ parsed = JSON.parse(trimmed);
31566
+ } catch {
31567
+ return text;
31568
+ }
31569
+ const obj = parsed;
31570
+ if (obj.nextPageToken !== "") return text;
31571
+ delete obj.nextPageToken;
31572
+ return JSON.stringify(obj, null, detectIndent2(text));
31573
+ }
31574
+ function truncationWarning(returned, count) {
31575
+ const scope = count.total !== void 0 ? `returned ${returned} of ${count.total} matches` : count.atLeast !== void 0 ? `returned ${returned} of at least ${count.atLeast} matches` : `returned ${returned} matches and MORE EXIST beyond this page`;
31576
+ return `INCOMPLETE RESULT SET: ${scope}. Do not report an absence of results based on this response. Page with nextPageToken or narrow the query.`;
31577
+ }
31578
+ function annotateTruncation(out, returned, count) {
31579
+ out.truncated = true;
31580
+ out.returned = returned;
31581
+ if (count.total !== void 0) out.totalMatches = count.total;
31582
+ if (count.atLeast !== void 0) out.totalMatchesAtLeast = count.atLeast;
31583
+ out.warning = truncationWarning(returned, count);
31584
+ }
31585
+ function hasMorePages(parsed) {
31586
+ return typeof parsed.nextPageToken === "string" && parsed.nextPageToken !== "";
31587
+ }
31588
+ function annotateTruncatedList(result, itemsKey) {
31589
+ const first = result.content[0];
31590
+ if (result.isError || first?.type !== "text") return result;
31591
+ let parsed;
31592
+ try {
31593
+ parsed = JSON.parse(first.text);
31594
+ } catch {
31595
+ return result;
31596
+ }
31597
+ const items = parsed[itemsKey];
31598
+ if (!Array.isArray(items) || !hasMorePages(parsed)) return result;
31599
+ const out = { ...parsed };
31600
+ annotateTruncation(out, items.length, {});
31601
+ return rawTextResult(JSON.stringify(out));
31602
+ }
31603
+
31555
31604
  // src/tools/utils.ts
31556
31605
  var PAYLOAD_INLINE_MAX = 4096;
31557
31606
  function payloadArg(inlineFlag, fileFlag, value, ext) {
@@ -31587,9 +31636,19 @@ var ids = {
31587
31636
  // People API uses fully-qualified resource names ("people/c123") not bare IDs.
31588
31637
  person: external_exports.string().describe("Person resource name (people/...) or email")
31589
31638
  };
31639
+ var pageTokenParam = external_exports.string().optional().describe(
31640
+ "Cursor for the NEXT page. Pass back the nextPageToken from a previous response verbatim, keeping the query and max identical: call once, then call again with pageToken=<that value>. A response with NO nextPageToken is the last page."
31641
+ );
31642
+ var pageAliasParam = external_exports.string().optional().describe(
31643
+ "Deprecated alias for pageToken, accepted so existing callers keep working. Use pageToken \u2014 it matches the nextPageToken field in the response."
31644
+ );
31645
+ function resolvePageToken(p) {
31646
+ return p.pageToken ?? p.page;
31647
+ }
31590
31648
  var paginationParams = {
31591
31649
  max: external_exports.number().int().optional().describe("Max results"),
31592
- page: external_exports.string().optional().describe("Page token"),
31650
+ pageToken: pageTokenParam,
31651
+ page: pageAliasParam,
31593
31652
  all: external_exports.boolean().optional().describe("Fetch all pages")
31594
31653
  };
31595
31654
  function registerRunTool(server, options) {
@@ -31664,7 +31723,7 @@ ${accounts || "(none)"}${hint}`);
31664
31723
  async function runOrDiagnose(args, options) {
31665
31724
  try {
31666
31725
  const raw = await run(args, options);
31667
- return rawTextResult(options.lossless ? raw : normalizeTimestamps(raw));
31726
+ return rawTextResult(options.lossless ? raw : stripConsumedPageToken(normalizeTimestamps(raw)));
31668
31727
  } catch (err) {
31669
31728
  return diagnose(err);
31670
31729
  }
@@ -31768,6 +31827,7 @@ function registerApiTools(server) {
31768
31827
  // src/tools/auth.ts
31769
31828
  function registerAuthToolsWith(server, defaultServices) {
31770
31829
  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.`;
31830
+ const extraScopesDescribe = "Additional raw OAuth scope URIs to request, comma-separated, on top of the ones `services` implies. Use for scopes no service covers \u2014 e.g. https://www.googleapis.com/auth/bigquery.readonly, required before gog_sheets_datasource_* can read BigQuery-backed Connected Sheets. Leave unset otherwise: an extra scope whose API is not enabled on the OAuth client project makes Google reject the WHOLE authorization with invalid_scope.";
31771
31831
  server.registerTool("gog_auth_list", {
31772
31832
  description: "List the Google accounts stored in gogcli, with their scopes. This reads local configuration only \u2014 it does not contact Google and does NOT tell you whether an account still works: a signed-out account whose refresh token expired or was revoked is listed here exactly like a healthy one, scopes and all. Use gog_auth_health to check whether an account can actually authenticate.",
31773
31833
  annotations: { readOnlyHint: true },
@@ -31817,11 +31877,14 @@ function registerAuthToolsWith(server, defaultServices) {
31817
31877
  annotations: { destructiveHint: true },
31818
31878
  inputSchema: {
31819
31879
  email: external_exports.string().describe("Google account email to authorize"),
31820
- services: external_exports.string().optional().default(defaultServices).describe(servicesDescribe)
31880
+ services: external_exports.string().optional().default(defaultServices).describe(servicesDescribe),
31881
+ extraScopes: external_exports.string().optional().describe(extraScopesDescribe)
31821
31882
  }
31822
- }, async ({ email: email3, services = defaultServices }) => {
31883
+ }, async ({ email: email3, services = defaultServices, extraScopes }) => {
31823
31884
  try {
31824
- return rawTextResult(await run(["auth", "add", email3, "--services", services], {
31885
+ const args = ["auth", "add", email3, "--services", services];
31886
+ if (extraScopes) args.push(`--extra-scopes=${extraScopes}`, "--force-consent");
31887
+ return rawTextResult(await run(args, {
31825
31888
  interactive: true,
31826
31889
  timeout: 3e5
31827
31890
  }));
@@ -31833,14 +31896,14 @@ function registerAuthToolsWith(server, defaultServices) {
31833
31896
  description: "Begin REMOTE/headless Google authorization (step 1 of 2). Returns a sign-in URL to open in any browser \u2014 no local server or terminal on the gogcli host is needed, so this works over the hosted connector where the interactive gog_auth_add cannot. Hand the URL to the user; after they sign in, the browser is redirected to a localhost URL that fails to load \u2014 that is expected. They copy that full redirected URL (from the address bar) and you pass it to gog_auth_add_complete. The link is valid for 10 minutes. If you pass a custom `services` here, pass the SAME value to gog_auth_add_complete or the second step will not match this one.",
31834
31897
  inputSchema: {
31835
31898
  email: external_exports.string().describe("Google account email to authorize"),
31836
- services: external_exports.string().optional().default(defaultServices).describe(servicesDescribe)
31899
+ services: external_exports.string().optional().default(defaultServices).describe(servicesDescribe),
31900
+ extraScopes: external_exports.string().optional().describe(`${extraScopesDescribe} Pass the SAME value to gog_auth_add_complete.`)
31837
31901
  }
31838
- }, async ({ email: email3, services = defaultServices }) => {
31902
+ }, async ({ email: email3, services = defaultServices, extraScopes }) => {
31839
31903
  try {
31840
- return rawTextResult(await run(
31841
- ["auth", "add", email3, "--remote", "--step", "1", "--services", services, "--force-consent"],
31842
- { redactMode: "tokens" }
31843
- ));
31904
+ const args = ["auth", "add", email3, "--remote", "--step", "1", "--services", services, "--force-consent"];
31905
+ if (extraScopes) args.push(`--extra-scopes=${extraScopes}`);
31906
+ return rawTextResult(await run(args, { redactMode: "tokens" }));
31844
31907
  } catch (err) {
31845
31908
  return errorResult(errorText(err));
31846
31909
  }
@@ -31855,25 +31918,28 @@ function registerAuthToolsWith(server, defaultServices) {
31855
31918
  ),
31856
31919
  services: external_exports.string().optional().default(defaultServices).describe(
31857
31920
  `Services authorized \u2014 MUST match the value passed to gog_auth_add_url. Default: "${defaultServices}".`
31921
+ ),
31922
+ extraScopes: external_exports.string().optional().describe(
31923
+ "Extra OAuth scope URIs \u2014 MUST match the value passed to gog_auth_add_url, for the same reason `services` must: the two steps have to describe the same grant."
31858
31924
  )
31859
31925
  }
31860
- }, async ({ email: email3, redirectUrl, services = defaultServices }) => {
31926
+ }, async ({ email: email3, redirectUrl, services = defaultServices, extraScopes }) => {
31861
31927
  try {
31862
- return rawTextResult(await run(
31863
- [
31864
- "auth",
31865
- "add",
31866
- email3,
31867
- "--remote",
31868
- "--step",
31869
- "2",
31870
- "--auth-url",
31871
- redirectUrl,
31872
- "--services",
31873
- services,
31874
- "--force-consent"
31875
- ]
31876
- ));
31928
+ const args = [
31929
+ "auth",
31930
+ "add",
31931
+ email3,
31932
+ "--remote",
31933
+ "--step",
31934
+ "2",
31935
+ "--auth-url",
31936
+ redirectUrl,
31937
+ "--services",
31938
+ services,
31939
+ "--force-consent"
31940
+ ];
31941
+ if (extraScopes) args.push(`--extra-scopes=${extraScopes}`);
31942
+ return rawTextResult(await run(args));
31877
31943
  } catch (err) {
31878
31944
  return errorResult(errorText(err));
31879
31945
  }
@@ -31892,30 +31958,44 @@ function registerAuthTools(server) {
31892
31958
  // src/tools/calendar.ts
31893
31959
  function registerCalendarTools(server) {
31894
31960
  server.registerTool("gog_calendar_events", {
31895
- description: "List calendar events. Filters can be combined (e.g. --from + --to for a range, or --today for just today).",
31961
+ 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.',
31896
31962
  annotations: { readOnlyHint: true },
31897
31963
  inputSchema: {
31898
31964
  calendarId: external_exports.string().optional().describe("Calendar ID (default: primary calendar)"),
31899
31965
  from: external_exports.string().optional().describe("Start time filter (RFC3339, date, or natural language)"),
31900
- to: external_exports.string().optional().describe("End time filter (RFC3339, date, or natural language)"),
31901
- today: external_exports.boolean().optional().describe("Only show today's events"),
31966
+ to: external_exports.string().optional().describe("End time filter (RFC3339, date, or natural language). Mutually exclusive with today and with days."),
31967
+ // gog >= 0.36.0 (openclaw/gogcli#981). Before that release --days sat in
31968
+ // a switch arm evaluated ahead of --from, so `--from 2026-09-25 --days 5`
31969
+ // silently threw --from away and answered for today instead — at exit 0,
31970
+ // in a well-formed table. It is only exposed here now that it means what
31971
+ // it says.
31972
+ days: external_exports.number().int().positive().optional().describe('Window LENGTH in days (calendar days, DST-aware), measured from `from` when one is given and from today otherwise. Use from + days for "the week of the 25th"; days alone for "the next N days". Mutually exclusive with to and with today.'),
31973
+ today: external_exports.boolean().optional().describe("Only show today's events. A complete window on its own \u2014 mutually exclusive with from, to and days."),
31902
31974
  query: external_exports.string().optional().describe("Free text search within events"),
31903
- all: external_exports.boolean().optional().describe("Fetch events from all calendars"),
31975
+ max: external_exports.number().int().optional().describe("Max events to return. gog defaults to 10, which silently hides the rest \u2014 raise it, or page with pageToken."),
31976
+ pageToken: pageTokenParam,
31977
+ page: pageAliasParam,
31978
+ all: external_exports.boolean().optional().describe('Fetch events from ALL CALENDARS. NOTE: unlike the gmail search tools, this does NOT mean "all pages" \u2014 it widens the calendar set, not the page window. Use pageToken to reach later pages.'),
31904
31979
  eventTypes: external_exports.array(external_exports.enum(["default", "birthday", "focus-time", "from-gmail", "out-of-office", "working-location"])).optional().describe("Filter to specific event types (repeatable)"),
31905
31980
  timezone: external_exports.string().optional().describe(`Display timezone for event times (IANA name, e.g. America/New_York, or "local" for the system timezone). Default: each event's timezone, then its calendar's timezone.`),
31906
31981
  account: accountParam
31907
31982
  }
31908
- }, async ({ calendarId, from, to, today, query, all, eventTypes, timezone, account }) => {
31983
+ }, async ({ calendarId, from, to, days, today, query, max, pageToken, page, all, eventTypes, timezone, account }) => {
31909
31984
  const args = ["calendar", "events"];
31910
31985
  if (calendarId) args.push(calendarId);
31911
31986
  if (from) args.push(`--from=${from}`);
31912
31987
  if (to) args.push(`--to=${to}`);
31988
+ if (days !== void 0) args.push(`--days=${days}`);
31913
31989
  if (today) args.push("--today");
31914
31990
  if (query) args.push(`--query=${query}`);
31991
+ if (max !== void 0) args.push(`--max=${max}`);
31992
+ const token = resolvePageToken({ pageToken, page });
31993
+ if (token) args.push(`--page=${token}`);
31915
31994
  if (all) args.push("--all");
31916
31995
  if (eventTypes) for (const t of eventTypes) args.push(`--event-types=${t}`);
31917
31996
  if (timezone) args.push(`--timezone=${timezone}`);
31918
- return runOrDiagnose(args, { account });
31997
+ const result = await runOrDiagnose(args, { account });
31998
+ return annotateTruncatedList(result, "events");
31919
31999
  });
31920
32000
  server.registerTool("gog_calendar_get", {
31921
32001
  description: "Get a specific calendar event by ID.",
@@ -32032,17 +32112,19 @@ function registerClassroomTools(server) {
32032
32112
  teacher: external_exports.string().optional().describe("Filter by teacher user ID"),
32033
32113
  student: external_exports.string().optional().describe("Filter by student user ID"),
32034
32114
  max: external_exports.number().optional().describe("Max results per page"),
32035
- page: external_exports.string().optional().describe("Page token for pagination"),
32115
+ pageToken: pageTokenParam,
32116
+ page: pageAliasParam,
32036
32117
  all: external_exports.boolean().optional().describe("Fetch all pages"),
32037
32118
  account: accountParam
32038
32119
  }
32039
- }, async ({ state, teacher, student, max, page, all, account }) => {
32120
+ }, async ({ state, teacher, student, max, pageToken, page, all, account }) => {
32040
32121
  const args = ["classroom", "courses", "list"];
32041
32122
  if (state) args.push(`--state=${state}`);
32042
32123
  if (teacher) args.push(`--teacher=${teacher}`);
32043
32124
  if (student) args.push(`--student=${student}`);
32044
32125
  if (max !== void 0) args.push(`--max=${max}`);
32045
- if (page) args.push(`--page=${page}`);
32126
+ const token = resolvePageToken({ pageToken, page });
32127
+ if (token) args.push(`--page=${token}`);
32046
32128
  if (all) args.push("--all");
32047
32129
  return runOrDiagnose(args, { account });
32048
32130
  });
@@ -32062,14 +32144,16 @@ function registerClassroomTools(server) {
32062
32144
  inputSchema: {
32063
32145
  courseId: external_exports.string().describe("Course ID"),
32064
32146
  max: external_exports.number().optional().describe("Max results per page"),
32065
- page: external_exports.string().optional().describe("Page token"),
32147
+ pageToken: pageTokenParam,
32148
+ page: pageAliasParam,
32066
32149
  all: external_exports.boolean().optional().describe("Fetch all pages"),
32067
32150
  account: accountParam
32068
32151
  }
32069
- }, async ({ courseId, max, page, all, account }) => {
32152
+ }, async ({ courseId, max, pageToken, page, all, account }) => {
32070
32153
  const args = ["classroom", "students", "list", courseId];
32071
32154
  if (max !== void 0) args.push(`--max=${max}`);
32072
- if (page) args.push(`--page=${page}`);
32155
+ const token = resolvePageToken({ pageToken, page });
32156
+ if (token) args.push(`--page=${token}`);
32073
32157
  if (all) args.push("--all");
32074
32158
  return runOrDiagnose(args, { account });
32075
32159
  });
@@ -32090,14 +32174,16 @@ function registerClassroomTools(server) {
32090
32174
  inputSchema: {
32091
32175
  courseId: external_exports.string().describe("Course ID"),
32092
32176
  max: external_exports.number().optional().describe("Max results per page"),
32093
- page: external_exports.string().optional().describe("Page token"),
32177
+ pageToken: pageTokenParam,
32178
+ page: pageAliasParam,
32094
32179
  all: external_exports.boolean().optional().describe("Fetch all pages"),
32095
32180
  account: accountParam
32096
32181
  }
32097
- }, async ({ courseId, max, page, all, account }) => {
32182
+ }, async ({ courseId, max, pageToken, page, all, account }) => {
32098
32183
  const args = ["classroom", "teachers", "list", courseId];
32099
32184
  if (max !== void 0) args.push(`--max=${max}`);
32100
- if (page) args.push(`--page=${page}`);
32185
+ const token = resolvePageToken({ pageToken, page });
32186
+ if (token) args.push(`--page=${token}`);
32101
32187
  if (all) args.push("--all");
32102
32188
  return runOrDiagnose(args, { account });
32103
32189
  });
@@ -32120,16 +32206,18 @@ function registerClassroomTools(server) {
32120
32206
  students: external_exports.boolean().optional().describe("Include students only"),
32121
32207
  teachers: external_exports.boolean().optional().describe("Include teachers only"),
32122
32208
  max: external_exports.number().optional().describe("Max results per page"),
32123
- page: external_exports.string().optional().describe("Page token"),
32209
+ pageToken: pageTokenParam,
32210
+ page: pageAliasParam,
32124
32211
  all: external_exports.boolean().optional().describe("Fetch all pages"),
32125
32212
  account: accountParam
32126
32213
  }
32127
- }, async ({ courseId, students, teachers, max, page, all, account }) => {
32214
+ }, async ({ courseId, students, teachers, max, pageToken, page, all, account }) => {
32128
32215
  const args = ["classroom", "roster", courseId];
32129
32216
  if (students) args.push("--students");
32130
32217
  if (teachers) args.push("--teachers");
32131
32218
  if (max !== void 0) args.push(`--max=${max}`);
32132
- if (page) args.push(`--page=${page}`);
32219
+ const token = resolvePageToken({ pageToken, page });
32220
+ if (token) args.push(`--page=${token}`);
32133
32221
  if (all) args.push("--all");
32134
32222
  return runOrDiagnose(args, { account });
32135
32223
  });
@@ -32142,18 +32230,20 @@ function registerClassroomTools(server) {
32142
32230
  topic: external_exports.string().optional().describe("Filter by topic ID"),
32143
32231
  orderBy: external_exports.string().optional().describe('Sort order (e.g. "updateTime desc")'),
32144
32232
  max: external_exports.number().optional().describe("Max results per page"),
32145
- page: external_exports.string().optional().describe("Page token"),
32233
+ pageToken: pageTokenParam,
32234
+ page: pageAliasParam,
32146
32235
  all: external_exports.boolean().optional().describe("Fetch all pages"),
32147
32236
  scanPages: external_exports.number().optional().describe("Max pages to scan when filtering"),
32148
32237
  account: accountParam
32149
32238
  }
32150
- }, async ({ courseId, state, topic, orderBy, max, page, all, scanPages, account }) => {
32239
+ }, async ({ courseId, state, topic, orderBy, max, pageToken, page, all, scanPages, account }) => {
32151
32240
  const args = ["classroom", "coursework", "list", courseId];
32152
32241
  if (state) args.push(`--state=${state}`);
32153
32242
  if (topic) args.push(`--topic=${topic}`);
32154
32243
  if (orderBy) args.push(`--order-by=${orderBy}`);
32155
32244
  if (max !== void 0) args.push(`--max=${max}`);
32156
- if (page) args.push(`--page=${page}`);
32245
+ const token = resolvePageToken({ pageToken, page });
32246
+ if (token) args.push(`--page=${token}`);
32157
32247
  if (all) args.push("--all");
32158
32248
  if (scanPages !== void 0) args.push(`--scan-pages=${scanPages}`);
32159
32249
  return runOrDiagnose(args, { account });
@@ -32179,17 +32269,19 @@ function registerClassroomTools(server) {
32179
32269
  late: external_exports.enum(["late", "not-late"]).optional().describe("Filter by late status"),
32180
32270
  user: external_exports.string().optional().describe("Filter by student user ID"),
32181
32271
  max: external_exports.number().optional().describe("Max results per page"),
32182
- page: external_exports.string().optional().describe("Page token"),
32272
+ pageToken: pageTokenParam,
32273
+ page: pageAliasParam,
32183
32274
  all: external_exports.boolean().optional().describe("Fetch all pages"),
32184
32275
  account: accountParam
32185
32276
  }
32186
- }, async ({ courseId, courseworkId, state, late: late2, user, max, page, all, account }) => {
32277
+ }, async ({ courseId, courseworkId, state, late: late2, user, max, pageToken, page, all, account }) => {
32187
32278
  const args = ["classroom", "submissions", "list", courseId, courseworkId];
32188
32279
  if (state) args.push(`--state=${state}`);
32189
32280
  if (late2) args.push(`--late=${late2}`);
32190
32281
  if (user) args.push(`--user=${user}`);
32191
32282
  if (max !== void 0) args.push(`--max=${max}`);
32192
- if (page) args.push(`--page=${page}`);
32283
+ const token = resolvePageToken({ pageToken, page });
32284
+ if (token) args.push(`--page=${token}`);
32193
32285
  if (all) args.push("--all");
32194
32286
  return runOrDiagnose(args, { account });
32195
32287
  });
@@ -32266,16 +32358,18 @@ function registerClassroomTools(server) {
32266
32358
  state: external_exports.string().optional().describe("Filter by announcement state"),
32267
32359
  orderBy: external_exports.string().optional().describe("Sort order"),
32268
32360
  max: external_exports.number().optional().describe("Max results per page"),
32269
- page: external_exports.string().optional().describe("Page token"),
32361
+ pageToken: pageTokenParam,
32362
+ page: pageAliasParam,
32270
32363
  all: external_exports.boolean().optional().describe("Fetch all pages"),
32271
32364
  account: accountParam
32272
32365
  }
32273
- }, async ({ courseId, state, orderBy, max, page, all, account }) => {
32366
+ }, async ({ courseId, state, orderBy, max, pageToken, page, all, account }) => {
32274
32367
  const args = ["classroom", "announcements", "list", courseId];
32275
32368
  if (state) args.push(`--state=${state}`);
32276
32369
  if (orderBy) args.push(`--order-by=${orderBy}`);
32277
32370
  if (max !== void 0) args.push(`--max=${max}`);
32278
- if (page) args.push(`--page=${page}`);
32371
+ const token = resolvePageToken({ pageToken, page });
32372
+ if (token) args.push(`--page=${token}`);
32279
32373
  if (all) args.push("--all");
32280
32374
  return runOrDiagnose(args, { account });
32281
32375
  });
@@ -32311,14 +32405,16 @@ function registerClassroomTools(server) {
32311
32405
  inputSchema: {
32312
32406
  courseId: external_exports.string().describe("Course ID"),
32313
32407
  max: external_exports.number().optional().describe("Max results per page"),
32314
- page: external_exports.string().optional().describe("Page token"),
32408
+ pageToken: pageTokenParam,
32409
+ page: pageAliasParam,
32315
32410
  all: external_exports.boolean().optional().describe("Fetch all pages"),
32316
32411
  account: accountParam
32317
32412
  }
32318
- }, async ({ courseId, max, page, all, account }) => {
32413
+ }, async ({ courseId, max, pageToken, page, all, account }) => {
32319
32414
  const args = ["classroom", "topics", "list", courseId];
32320
32415
  if (max !== void 0) args.push(`--max=${max}`);
32321
- if (page) args.push(`--page=${page}`);
32416
+ const token = resolvePageToken({ pageToken, page });
32417
+ if (token) args.push(`--page=${token}`);
32322
32418
  if (all) args.push("--all");
32323
32419
  return runOrDiagnose(args, { account });
32324
32420
  });
@@ -32340,16 +32436,18 @@ function registerClassroomTools(server) {
32340
32436
  course: external_exports.string().optional().describe("Filter by course ID"),
32341
32437
  user: external_exports.string().optional().describe("Filter by user ID"),
32342
32438
  max: external_exports.number().optional().describe("Max results per page"),
32343
- page: external_exports.string().optional().describe("Page token"),
32439
+ pageToken: pageTokenParam,
32440
+ page: pageAliasParam,
32344
32441
  all: external_exports.boolean().optional().describe("Fetch all pages"),
32345
32442
  account: accountParam
32346
32443
  }
32347
- }, async ({ course, user, max, page, all, account }) => {
32444
+ }, async ({ course, user, max, pageToken, page, all, account }) => {
32348
32445
  const args = ["classroom", "invitations", "list"];
32349
32446
  if (course) args.push(`--course=${course}`);
32350
32447
  if (user) args.push(`--user=${user}`);
32351
32448
  if (max !== void 0) args.push(`--max=${max}`);
32352
- if (page) args.push(`--page=${page}`);
32449
+ const token = resolvePageToken({ pageToken, page });
32450
+ if (token) args.push(`--page=${token}`);
32353
32451
  if (all) args.push("--all");
32354
32452
  return runOrDiagnose(args, { account });
32355
32453
  });
@@ -32559,16 +32657,18 @@ function registerDriveTools(server) {
32559
32657
  inputSchema: {
32560
32658
  folderId: external_exports.string().optional().describe("Folder ID to list (default: root)"),
32561
32659
  max: external_exports.number().optional().describe("Max results (default: 20)"),
32562
- page: external_exports.string().optional().describe("Page token for pagination"),
32660
+ pageToken: pageTokenParam,
32661
+ page: pageAliasParam,
32563
32662
  query: external_exports.string().optional().describe(`Drive query filter (e.g. "name contains 'budget'")`),
32564
32663
  allDrives: external_exports.boolean().optional().describe("Include shared drives (default: true). Set false for My Drive only."),
32565
32664
  account: accountParam
32566
32665
  }
32567
- }, async ({ folderId, max, page, query, allDrives, account }) => {
32666
+ }, async ({ folderId, max, pageToken, page, query, allDrives, account }) => {
32568
32667
  const args = ["drive", "ls"];
32569
32668
  if (folderId) args.push(`--parent=${folderId}`);
32570
32669
  if (max !== void 0) args.push(`--max=${max}`);
32571
- if (page) args.push(`--page=${page}`);
32670
+ const token = resolvePageToken({ pageToken, page });
32671
+ if (token) args.push(`--page=${token}`);
32572
32672
  if (query) args.push(`--query=${query}`);
32573
32673
  if (allDrives === false) args.push("--no-all-drives");
32574
32674
  return runOrDiagnose(args, { account });
@@ -32758,34 +32858,148 @@ function registerDriveTools(server) {
32758
32858
  registerRunTool(server, { service: "drive", examples: '"copy", "upload", "download", "permissions"' });
32759
32859
  }
32760
32860
 
32861
+ // src/gmail-results.ts
32862
+ function sortKey(item) {
32863
+ for (const raw of [item.internalDateIso, item.date]) {
32864
+ if (typeof raw !== "string" || !raw) continue;
32865
+ const t = Date.parse(raw);
32866
+ if (!Number.isNaN(t)) return t;
32867
+ }
32868
+ return Number.NEGATIVE_INFINITY;
32869
+ }
32870
+ function sortNewestFirst(items) {
32871
+ return [...items].sort((a, b) => {
32872
+ const ka = sortKey(a);
32873
+ const kb = sortKey(b);
32874
+ return ka === kb ? 0 : kb - ka;
32875
+ });
32876
+ }
32877
+ var COUNT_PROBE_PAGE_SIZE = 500;
32878
+ async function countMatches(method, itemsKey, query, account) {
32879
+ try {
32880
+ const params = JSON.stringify({
32881
+ userId: "me",
32882
+ q: query,
32883
+ maxResults: COUNT_PROBE_PAGE_SIZE,
32884
+ fields: `${itemsKey}/id,nextPageToken`
32885
+ });
32886
+ const raw = await run(["api", "call", "gmail", "v1", method, `--params=${params}`], { account });
32887
+ const parsed = JSON.parse(raw);
32888
+ const items = parsed[itemsKey];
32889
+ if (!Array.isArray(items)) return {};
32890
+ const more = typeof parsed.nextPageToken === "string" && parsed.nextPageToken !== "";
32891
+ return more ? { atLeast: items.length } : { total: items.length };
32892
+ } catch {
32893
+ return {};
32894
+ }
32895
+ }
32896
+ async function finalizeGmailSearch(result, options) {
32897
+ const { itemsKey, method, query, account, queryIsExact = true } = options;
32898
+ const first = result.content[0];
32899
+ if (result.isError || first?.type !== "text") return result;
32900
+ let parsed;
32901
+ try {
32902
+ parsed = JSON.parse(first.text);
32903
+ } catch {
32904
+ return result;
32905
+ }
32906
+ const items = parsed[itemsKey];
32907
+ if (!Array.isArray(items)) return result;
32908
+ const sorted = sortNewestFirst(items);
32909
+ const out = { ...parsed, [itemsKey]: sorted };
32910
+ if (hasMorePages(parsed)) {
32911
+ const count = queryIsExact ? await countMatches(method, itemsKey, query, account) : {};
32912
+ annotateTruncation(out, sorted.length, count);
32913
+ }
32914
+ return rawTextResult(JSON.stringify(out));
32915
+ }
32916
+ async function fetchGmailPages(runPage, itemsKey, maxPages, startToken) {
32917
+ const merged = [];
32918
+ let base;
32919
+ let token = startToken;
32920
+ for (let pages = 0; pages < maxPages; pages++) {
32921
+ const result = await runPage(token);
32922
+ const parsed = parsePage(result, itemsKey);
32923
+ if (parsed === void 0) {
32924
+ return base === void 0 ? result : finish(base, itemsKey, merged, token);
32925
+ }
32926
+ base = parsed;
32927
+ merged.push(...parsed[itemsKey]);
32928
+ token = typeof parsed.nextPageToken === "string" && parsed.nextPageToken !== "" ? parsed.nextPageToken : void 0;
32929
+ if (token === void 0) break;
32930
+ }
32931
+ return finish(base, itemsKey, merged, token);
32932
+ }
32933
+ function parsePage(result, itemsKey) {
32934
+ const first = result.content[0];
32935
+ if (result.isError || first?.type !== "text") return void 0;
32936
+ let parsed;
32937
+ try {
32938
+ parsed = JSON.parse(first.text);
32939
+ } catch {
32940
+ return void 0;
32941
+ }
32942
+ if (parsed === null || typeof parsed !== "object") return void 0;
32943
+ const obj = parsed;
32944
+ return Array.isArray(obj[itemsKey]) ? obj : void 0;
32945
+ }
32946
+ function finish(base, itemsKey, merged, token) {
32947
+ const out = { ...base, [itemsKey]: merged };
32948
+ if (token === void 0) delete out.nextPageToken;
32949
+ else out.nextPageToken = token;
32950
+ return rawTextResult(JSON.stringify(out));
32951
+ }
32952
+
32761
32953
  // src/tools/gmail.ts
32762
32954
  function registerGmailTools(server) {
32763
32955
  server.registerTool("gog_gmail_search", {
32764
- 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).`,
32956
+ 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.',
32765
32957
  annotations: { readOnlyHint: true },
32766
32958
  inputSchema: {
32767
32959
  query: external_exports.string().describe("Gmail search query"),
32768
32960
  max: external_exports.number().int().optional().describe("Max results to return (default: 10)"),
32961
+ pageToken: pageTokenParam,
32962
+ page: pageAliasParam,
32963
+ maxPages: external_exports.number().int().positive().max(20).optional().describe('Walk up to this many pages in ONE call and merge the results, instead of returning a single page. Use it for existence questions ("is there any mail matching X?"), which a single page cannot answer. Stops early at the last page; if pages remain when the cap is hit the response is still marked truncated. Prefer this over all=true, which is unbounded.'),
32964
+ all: external_exports.boolean().optional().describe('Fetch every page instead of one. Removes truncation entirely, at the cost of one API round-trip per page \u2014 the reliable way to answer "does any message match?" for a query with few expected hits.'),
32769
32965
  fromContact: external_exports.string().optional().describe("Resolve a Google Contact (name or email) to its addresses and AND a from:(addr OR addr) clause onto the query \u2014 saves looking the contact up first when you only know who, not which address."),
32770
32966
  account: accountParam
32771
32967
  }
32772
- }, async ({ query, max, fromContact, account }) => {
32968
+ }, async ({ query, max, pageToken, page, maxPages, all, fromContact, account }) => {
32773
32969
  const args = ["gmail", "search", query];
32774
32970
  if (max !== void 0) args.push(`--max=${max}`);
32971
+ if (all) args.push("--all");
32775
32972
  if (fromContact) args.push(`--from-contact=${fromContact}`);
32776
- return runOrDiagnose(args, { account });
32973
+ const runPage = (tok) => runOrDiagnose(tok ? [...args, `--page=${tok}`] : args, { account });
32974
+ const token = resolvePageToken({ pageToken, page });
32975
+ const result = maxPages !== void 0 ? await fetchGmailPages(runPage, "threads", maxPages, token) : await runPage(token);
32976
+ return finalizeGmailSearch(result, {
32977
+ itemsKey: "threads",
32978
+ method: "users.threads.list",
32979
+ query,
32980
+ account,
32981
+ // --from-contact is expanded INSIDE gog, against the People API, so the
32982
+ // query Gmail actually saw is not the one we hold here.
32983
+ queryIsExact: !fromContact
32984
+ });
32777
32985
  });
32778
32986
  server.registerTool("gog_gmail_get", {
32779
- description: "Get a Gmail message by ID.",
32987
+ description: "Get a Gmail message by ID. For a long message, sanitizeContent is the cheapest way to keep it in context: it drops the raw MIME payload and the HTML part, which are usually the bulk of the response.",
32780
32988
  annotations: { readOnlyHint: true },
32781
32989
  inputSchema: {
32782
32990
  messageId: external_exports.string().describe("Message ID"),
32783
32991
  format: external_exports.enum(["full", "metadata", "raw"]).optional().describe("Message format (default: full)"),
32992
+ // Requires gog >= 0.37.0. Before that (openclaw/gogcli#992) the JSON
32993
+ // carried the headers and body TWICE — once inside `message`, once
32994
+ // copied to the top level — so the flag meant to shrink the payload
32995
+ // enlarged it. MIN_GOG_VERSION is the guard; there is no runtime check.
32996
+ sanitizeContent: external_exports.boolean().optional().describe("Return agent-oriented sanitized content: HTML stripped, HTTP(S) URLs removed, raw Gmail payloads omitted from the JSON. The largest payload-size reduction available here. Note the URL removal is lossy \u2014 omit this when you need to follow a link out of the message."),
32784
32997
  account: accountParam
32785
32998
  }
32786
- }, async ({ messageId, format, account }) => {
32999
+ }, async ({ messageId, format, sanitizeContent, account }) => {
32787
33000
  const args = ["gmail", "get", messageId];
32788
33001
  if (format) args.push(`--format=${format}`);
33002
+ if (sanitizeContent) args.push("--sanitize-content");
32789
33003
  return runOrDiagnose(args, { account });
32790
33004
  });
32791
33005
  server.registerTool("gog_gmail_send", {
@@ -33138,7 +33352,7 @@ function registerTasksTools(server) {
33138
33352
  }
33139
33353
 
33140
33354
  // src/server.ts
33141
- var VERSION = true ? "2.23.1" : "0.0.0";
33355
+ var VERSION = true ? "2.24.0" : "0.0.0";
33142
33356
  var BASE_TOOL_REGISTRARS = [
33143
33357
  registerApiTools,
33144
33358
  registerAuthTools,