gogcli-mcp 2.23.2 → 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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/index.js +52 -32
- package/dist/lib.js +53 -33
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/gmail-results.ts +15 -4
- package/src/runner.ts +1 -1
- package/src/tools/auth.ts +39 -12
- package/src/tools/calendar.ts +12 -4
- package/src/tools/gmail.ts +8 -2
- package/src/worker.ts +1 -1
- package/tests/tools/auth.test.ts +60 -0
- package/tests/tools/calendar.test.ts +30 -3
- package/tests/tools/gmail.test.ts +17 -0
|
@@ -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.
|
|
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.
|
|
18
|
+
"version": "2.24.0",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
package/dist/index.js
CHANGED
|
@@ -31827,6 +31827,7 @@ function registerApiTools(server) {
|
|
|
31827
31827
|
// src/tools/auth.ts
|
|
31828
31828
|
function registerAuthToolsWith(server, defaultServices) {
|
|
31829
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.";
|
|
31830
31831
|
server.registerTool("gog_auth_list", {
|
|
31831
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.",
|
|
31832
31833
|
annotations: { readOnlyHint: true },
|
|
@@ -31876,11 +31877,14 @@ function registerAuthToolsWith(server, defaultServices) {
|
|
|
31876
31877
|
annotations: { destructiveHint: true },
|
|
31877
31878
|
inputSchema: {
|
|
31878
31879
|
email: external_exports.string().describe("Google account email to authorize"),
|
|
31879
|
-
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)
|
|
31880
31882
|
}
|
|
31881
|
-
}, async ({ email: email3, services = defaultServices }) => {
|
|
31883
|
+
}, async ({ email: email3, services = defaultServices, extraScopes }) => {
|
|
31882
31884
|
try {
|
|
31883
|
-
|
|
31885
|
+
const args = ["auth", "add", email3, "--services", services];
|
|
31886
|
+
if (extraScopes) args.push(`--extra-scopes=${extraScopes}`, "--force-consent");
|
|
31887
|
+
return rawTextResult(await run(args, {
|
|
31884
31888
|
interactive: true,
|
|
31885
31889
|
timeout: 3e5
|
|
31886
31890
|
}));
|
|
@@ -31892,14 +31896,14 @@ function registerAuthToolsWith(server, defaultServices) {
|
|
|
31892
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.",
|
|
31893
31897
|
inputSchema: {
|
|
31894
31898
|
email: external_exports.string().describe("Google account email to authorize"),
|
|
31895
|
-
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.`)
|
|
31896
31901
|
}
|
|
31897
|
-
}, async ({ email: email3, services = defaultServices }) => {
|
|
31902
|
+
}, async ({ email: email3, services = defaultServices, extraScopes }) => {
|
|
31898
31903
|
try {
|
|
31899
|
-
|
|
31900
|
-
|
|
31901
|
-
|
|
31902
|
-
));
|
|
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" }));
|
|
31903
31907
|
} catch (err) {
|
|
31904
31908
|
return errorResult(errorText(err));
|
|
31905
31909
|
}
|
|
@@ -31914,25 +31918,28 @@ function registerAuthToolsWith(server, defaultServices) {
|
|
|
31914
31918
|
),
|
|
31915
31919
|
services: external_exports.string().optional().default(defaultServices).describe(
|
|
31916
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."
|
|
31917
31924
|
)
|
|
31918
31925
|
}
|
|
31919
|
-
}, async ({ email: email3, redirectUrl, services = defaultServices }) => {
|
|
31926
|
+
}, async ({ email: email3, redirectUrl, services = defaultServices, extraScopes }) => {
|
|
31920
31927
|
try {
|
|
31921
|
-
|
|
31922
|
-
|
|
31923
|
-
|
|
31924
|
-
|
|
31925
|
-
|
|
31926
|
-
|
|
31927
|
-
|
|
31928
|
-
|
|
31929
|
-
|
|
31930
|
-
|
|
31931
|
-
|
|
31932
|
-
|
|
31933
|
-
|
|
31934
|
-
|
|
31935
|
-
));
|
|
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));
|
|
31936
31943
|
} catch (err) {
|
|
31937
31944
|
return errorResult(errorText(err));
|
|
31938
31945
|
}
|
|
@@ -31951,13 +31958,19 @@ function registerAuthTools(server) {
|
|
|
31951
31958
|
// src/tools/calendar.ts
|
|
31952
31959
|
function registerCalendarTools(server) {
|
|
31953
31960
|
server.registerTool("gog_calendar_events", {
|
|
31954
|
-
description: 'List calendar events.
|
|
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.',
|
|
31955
31962
|
annotations: { readOnlyHint: true },
|
|
31956
31963
|
inputSchema: {
|
|
31957
31964
|
calendarId: external_exports.string().optional().describe("Calendar ID (default: primary calendar)"),
|
|
31958
31965
|
from: external_exports.string().optional().describe("Start time filter (RFC3339, date, or natural language)"),
|
|
31959
|
-
to: external_exports.string().optional().describe("End time filter (RFC3339, date, or natural language)"),
|
|
31960
|
-
|
|
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."),
|
|
31961
31974
|
query: external_exports.string().optional().describe("Free text search within events"),
|
|
31962
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."),
|
|
31963
31976
|
pageToken: pageTokenParam,
|
|
@@ -31967,11 +31980,12 @@ function registerCalendarTools(server) {
|
|
|
31967
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.`),
|
|
31968
31981
|
account: accountParam
|
|
31969
31982
|
}
|
|
31970
|
-
}, async ({ calendarId, from, to, today, query, max, pageToken, page, all, eventTypes, timezone, account }) => {
|
|
31983
|
+
}, async ({ calendarId, from, to, days, today, query, max, pageToken, page, all, eventTypes, timezone, account }) => {
|
|
31971
31984
|
const args = ["calendar", "events"];
|
|
31972
31985
|
if (calendarId) args.push(calendarId);
|
|
31973
31986
|
if (from) args.push(`--from=${from}`);
|
|
31974
31987
|
if (to) args.push(`--to=${to}`);
|
|
31988
|
+
if (days !== void 0) args.push(`--days=${days}`);
|
|
31975
31989
|
if (today) args.push("--today");
|
|
31976
31990
|
if (query) args.push(`--query=${query}`);
|
|
31977
31991
|
if (max !== void 0) args.push(`--max=${max}`);
|
|
@@ -32970,16 +32984,22 @@ function registerGmailTools(server) {
|
|
|
32970
32984
|
});
|
|
32971
32985
|
});
|
|
32972
32986
|
server.registerTool("gog_gmail_get", {
|
|
32973
|
-
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.",
|
|
32974
32988
|
annotations: { readOnlyHint: true },
|
|
32975
32989
|
inputSchema: {
|
|
32976
32990
|
messageId: external_exports.string().describe("Message ID"),
|
|
32977
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."),
|
|
32978
32997
|
account: accountParam
|
|
32979
32998
|
}
|
|
32980
|
-
}, async ({ messageId, format, account }) => {
|
|
32999
|
+
}, async ({ messageId, format, sanitizeContent, account }) => {
|
|
32981
33000
|
const args = ["gmail", "get", messageId];
|
|
32982
33001
|
if (format) args.push(`--format=${format}`);
|
|
33002
|
+
if (sanitizeContent) args.push("--sanitize-content");
|
|
32983
33003
|
return runOrDiagnose(args, { account });
|
|
32984
33004
|
});
|
|
32985
33005
|
server.registerTool("gog_gmail_send", {
|
|
@@ -33332,7 +33352,7 @@ function registerTasksTools(server) {
|
|
|
33332
33352
|
}
|
|
33333
33353
|
|
|
33334
33354
|
// src/server.ts
|
|
33335
|
-
var VERSION = true ? "2.
|
|
33355
|
+
var VERSION = true ? "2.24.0" : "0.0.0";
|
|
33336
33356
|
var BASE_TOOL_REGISTRARS = [
|
|
33337
33357
|
registerApiTools,
|
|
33338
33358
|
registerAuthTools,
|
package/dist/lib.js
CHANGED
|
@@ -23042,7 +23042,7 @@ function activeExecutor() {
|
|
|
23042
23042
|
return runExecutor.getStore() ?? defaultExecutor;
|
|
23043
23043
|
}
|
|
23044
23044
|
var TIMEOUT_MS = 3e4;
|
|
23045
|
-
var MIN_GOG_VERSION = "0.
|
|
23045
|
+
var MIN_GOG_VERSION = "0.37.0";
|
|
23046
23046
|
function readonlyEnvEnabled() {
|
|
23047
23047
|
return readEnvVar("GOG_READONLY") !== void 0 && parseBoolEnv("GOG_READONLY", { default: true });
|
|
23048
23048
|
}
|
|
@@ -23712,6 +23712,7 @@ function registerApiTools(server) {
|
|
|
23712
23712
|
// src/tools/auth.ts
|
|
23713
23713
|
function registerAuthToolsWith(server, defaultServices) {
|
|
23714
23714
|
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.`;
|
|
23715
|
+
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.";
|
|
23715
23716
|
server.registerTool("gog_auth_list", {
|
|
23716
23717
|
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.",
|
|
23717
23718
|
annotations: { readOnlyHint: true },
|
|
@@ -23761,11 +23762,14 @@ function registerAuthToolsWith(server, defaultServices) {
|
|
|
23761
23762
|
annotations: { destructiveHint: true },
|
|
23762
23763
|
inputSchema: {
|
|
23763
23764
|
email: external_exports.string().describe("Google account email to authorize"),
|
|
23764
|
-
services: external_exports.string().optional().default(defaultServices).describe(servicesDescribe)
|
|
23765
|
+
services: external_exports.string().optional().default(defaultServices).describe(servicesDescribe),
|
|
23766
|
+
extraScopes: external_exports.string().optional().describe(extraScopesDescribe)
|
|
23765
23767
|
}
|
|
23766
|
-
}, async ({ email: email3, services = defaultServices }) => {
|
|
23768
|
+
}, async ({ email: email3, services = defaultServices, extraScopes }) => {
|
|
23767
23769
|
try {
|
|
23768
|
-
|
|
23770
|
+
const args = ["auth", "add", email3, "--services", services];
|
|
23771
|
+
if (extraScopes) args.push(`--extra-scopes=${extraScopes}`, "--force-consent");
|
|
23772
|
+
return rawTextResult(await run(args, {
|
|
23769
23773
|
interactive: true,
|
|
23770
23774
|
timeout: 3e5
|
|
23771
23775
|
}));
|
|
@@ -23777,14 +23781,14 @@ function registerAuthToolsWith(server, defaultServices) {
|
|
|
23777
23781
|
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.",
|
|
23778
23782
|
inputSchema: {
|
|
23779
23783
|
email: external_exports.string().describe("Google account email to authorize"),
|
|
23780
|
-
services: external_exports.string().optional().default(defaultServices).describe(servicesDescribe)
|
|
23784
|
+
services: external_exports.string().optional().default(defaultServices).describe(servicesDescribe),
|
|
23785
|
+
extraScopes: external_exports.string().optional().describe(`${extraScopesDescribe} Pass the SAME value to gog_auth_add_complete.`)
|
|
23781
23786
|
}
|
|
23782
|
-
}, async ({ email: email3, services = defaultServices }) => {
|
|
23787
|
+
}, async ({ email: email3, services = defaultServices, extraScopes }) => {
|
|
23783
23788
|
try {
|
|
23784
|
-
|
|
23785
|
-
|
|
23786
|
-
|
|
23787
|
-
));
|
|
23789
|
+
const args = ["auth", "add", email3, "--remote", "--step", "1", "--services", services, "--force-consent"];
|
|
23790
|
+
if (extraScopes) args.push(`--extra-scopes=${extraScopes}`);
|
|
23791
|
+
return rawTextResult(await run(args, { redactMode: "tokens" }));
|
|
23788
23792
|
} catch (err) {
|
|
23789
23793
|
return errorResult(errorText(err));
|
|
23790
23794
|
}
|
|
@@ -23799,25 +23803,28 @@ function registerAuthToolsWith(server, defaultServices) {
|
|
|
23799
23803
|
),
|
|
23800
23804
|
services: external_exports.string().optional().default(defaultServices).describe(
|
|
23801
23805
|
`Services authorized \u2014 MUST match the value passed to gog_auth_add_url. Default: "${defaultServices}".`
|
|
23806
|
+
),
|
|
23807
|
+
extraScopes: external_exports.string().optional().describe(
|
|
23808
|
+
"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."
|
|
23802
23809
|
)
|
|
23803
23810
|
}
|
|
23804
|
-
}, async ({ email: email3, redirectUrl, services = defaultServices }) => {
|
|
23811
|
+
}, async ({ email: email3, redirectUrl, services = defaultServices, extraScopes }) => {
|
|
23805
23812
|
try {
|
|
23806
|
-
|
|
23807
|
-
|
|
23808
|
-
|
|
23809
|
-
|
|
23810
|
-
|
|
23811
|
-
|
|
23812
|
-
|
|
23813
|
-
|
|
23814
|
-
|
|
23815
|
-
|
|
23816
|
-
|
|
23817
|
-
|
|
23818
|
-
|
|
23819
|
-
|
|
23820
|
-
));
|
|
23813
|
+
const args = [
|
|
23814
|
+
"auth",
|
|
23815
|
+
"add",
|
|
23816
|
+
email3,
|
|
23817
|
+
"--remote",
|
|
23818
|
+
"--step",
|
|
23819
|
+
"2",
|
|
23820
|
+
"--auth-url",
|
|
23821
|
+
redirectUrl,
|
|
23822
|
+
"--services",
|
|
23823
|
+
services,
|
|
23824
|
+
"--force-consent"
|
|
23825
|
+
];
|
|
23826
|
+
if (extraScopes) args.push(`--extra-scopes=${extraScopes}`);
|
|
23827
|
+
return rawTextResult(await run(args));
|
|
23821
23828
|
} catch (err) {
|
|
23822
23829
|
return errorResult(errorText(err));
|
|
23823
23830
|
}
|
|
@@ -23839,13 +23846,19 @@ function authToolsFor(defaultServices) {
|
|
|
23839
23846
|
// src/tools/calendar.ts
|
|
23840
23847
|
function registerCalendarTools(server) {
|
|
23841
23848
|
server.registerTool("gog_calendar_events", {
|
|
23842
|
-
description: 'List calendar events.
|
|
23849
|
+
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.',
|
|
23843
23850
|
annotations: { readOnlyHint: true },
|
|
23844
23851
|
inputSchema: {
|
|
23845
23852
|
calendarId: external_exports.string().optional().describe("Calendar ID (default: primary calendar)"),
|
|
23846
23853
|
from: external_exports.string().optional().describe("Start time filter (RFC3339, date, or natural language)"),
|
|
23847
|
-
to: external_exports.string().optional().describe("End time filter (RFC3339, date, or natural language)"),
|
|
23848
|
-
|
|
23854
|
+
to: external_exports.string().optional().describe("End time filter (RFC3339, date, or natural language). Mutually exclusive with today and with days."),
|
|
23855
|
+
// gog >= 0.36.0 (openclaw/gogcli#981). Before that release --days sat in
|
|
23856
|
+
// a switch arm evaluated ahead of --from, so `--from 2026-09-25 --days 5`
|
|
23857
|
+
// silently threw --from away and answered for today instead — at exit 0,
|
|
23858
|
+
// in a well-formed table. It is only exposed here now that it means what
|
|
23859
|
+
// it says.
|
|
23860
|
+
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.'),
|
|
23861
|
+
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."),
|
|
23849
23862
|
query: external_exports.string().optional().describe("Free text search within events"),
|
|
23850
23863
|
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."),
|
|
23851
23864
|
pageToken: pageTokenParam,
|
|
@@ -23855,11 +23868,12 @@ function registerCalendarTools(server) {
|
|
|
23855
23868
|
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.`),
|
|
23856
23869
|
account: accountParam
|
|
23857
23870
|
}
|
|
23858
|
-
}, async ({ calendarId, from, to, today, query, max, pageToken, page, all, eventTypes, timezone, account }) => {
|
|
23871
|
+
}, async ({ calendarId, from, to, days, today, query, max, pageToken, page, all, eventTypes, timezone, account }) => {
|
|
23859
23872
|
const args = ["calendar", "events"];
|
|
23860
23873
|
if (calendarId) args.push(calendarId);
|
|
23861
23874
|
if (from) args.push(`--from=${from}`);
|
|
23862
23875
|
if (to) args.push(`--to=${to}`);
|
|
23876
|
+
if (days !== void 0) args.push(`--days=${days}`);
|
|
23863
23877
|
if (today) args.push("--today");
|
|
23864
23878
|
if (query) args.push(`--query=${query}`);
|
|
23865
23879
|
if (max !== void 0) args.push(`--max=${max}`);
|
|
@@ -24858,16 +24872,22 @@ function registerGmailTools(server) {
|
|
|
24858
24872
|
});
|
|
24859
24873
|
});
|
|
24860
24874
|
server.registerTool("gog_gmail_get", {
|
|
24861
|
-
description: "Get a Gmail message by ID.",
|
|
24875
|
+
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.",
|
|
24862
24876
|
annotations: { readOnlyHint: true },
|
|
24863
24877
|
inputSchema: {
|
|
24864
24878
|
messageId: external_exports.string().describe("Message ID"),
|
|
24865
24879
|
format: external_exports.enum(["full", "metadata", "raw"]).optional().describe("Message format (default: full)"),
|
|
24880
|
+
// Requires gog >= 0.37.0. Before that (openclaw/gogcli#992) the JSON
|
|
24881
|
+
// carried the headers and body TWICE — once inside `message`, once
|
|
24882
|
+
// copied to the top level — so the flag meant to shrink the payload
|
|
24883
|
+
// enlarged it. MIN_GOG_VERSION is the guard; there is no runtime check.
|
|
24884
|
+
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."),
|
|
24866
24885
|
account: accountParam
|
|
24867
24886
|
}
|
|
24868
|
-
}, async ({ messageId, format, account }) => {
|
|
24887
|
+
}, async ({ messageId, format, sanitizeContent, account }) => {
|
|
24869
24888
|
const args = ["gmail", "get", messageId];
|
|
24870
24889
|
if (format) args.push(`--format=${format}`);
|
|
24890
|
+
if (sanitizeContent) args.push("--sanitize-content");
|
|
24871
24891
|
return runOrDiagnose(args, { account });
|
|
24872
24892
|
});
|
|
24873
24893
|
server.registerTool("gog_gmail_send", {
|
|
@@ -25220,7 +25240,7 @@ function registerTasksTools(server) {
|
|
|
25220
25240
|
}
|
|
25221
25241
|
|
|
25222
25242
|
// src/server.ts
|
|
25223
|
-
var VERSION = true ? "2.
|
|
25243
|
+
var VERSION = true ? "2.24.0" : "0.0.0";
|
|
25224
25244
|
var BASE_TOOL_REGISTRARS = [
|
|
25225
25245
|
registerApiTools,
|
|
25226
25246
|
registerAuthTools,
|
package/manifest.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"manifest_version": "0.3",
|
|
4
4
|
"name": "gogcli-mcp",
|
|
5
5
|
"display_name": "gogcli",
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.24.0",
|
|
7
7
|
"description": "Google Sheets (and more) for Claude via gogcli — read, write, and manage spreadsheets",
|
|
8
8
|
"author": {
|
|
9
9
|
"name": "Chris Hall",
|
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"source": "github",
|
|
8
8
|
"subfolder": "packages/gogcli-mcp"
|
|
9
9
|
},
|
|
10
|
-
"version": "2.
|
|
10
|
+
"version": "2.24.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "gogcli-mcp",
|
|
15
|
-
"version": "2.
|
|
15
|
+
"version": "2.24.0",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
},
|
package/src/gmail-results.ts
CHANGED
|
@@ -71,10 +71,21 @@ const COUNT_PROBE_PAGE_SIZE = 500;
|
|
|
71
71
|
// a caller is most likely to draw a false negative from. A full page with more
|
|
72
72
|
// behind it yields an honest lower bound instead.
|
|
73
73
|
//
|
|
74
|
-
// gog
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
74
|
+
// gog CAN now supply this itself — `--count` on `gmail search` / `gmail
|
|
75
|
+
// messages search` (gog >= 0.36.0, openclaw/gogcli#985) is this same probe,
|
|
76
|
+
// upstreamed, down to the page size and the exact/lower-bound split. The probe
|
|
77
|
+
// stays here anyway, for two differences that both matter at this seam:
|
|
78
|
+
//
|
|
79
|
+
// * It is spent ONLY on a result set already known to be truncated. --count
|
|
80
|
+
// is decided before the search runs, so adopting it would cost every
|
|
81
|
+
// search an extra Gmail request to answer a question most of them never
|
|
82
|
+
// raise.
|
|
83
|
+
// * It is best-effort. gog returns the count probe's error from the whole
|
|
84
|
+
// command, so a failed count would turn a search that DID succeed into an
|
|
85
|
+
// error — trading a missing warning for a missing answer.
|
|
86
|
+
//
|
|
87
|
+
// Keep the two in step: a change to what "exact" means on either side should
|
|
88
|
+
// be made on both.
|
|
78
89
|
//
|
|
79
90
|
// Best-effort by construction: any failure must degrade the warning, never the
|
|
80
91
|
// search.
|
package/src/runner.ts
CHANGED
|
@@ -167,7 +167,7 @@ const TIMEOUT_MS = 30_000;
|
|
|
167
167
|
// so the requirement change is surfaced in the release notes (see
|
|
168
168
|
// .github/release.yml). This is the single source of truth for the required
|
|
169
169
|
// version; keep the README/CLAUDE.md mention in sync.
|
|
170
|
-
export const MIN_GOG_VERSION = '0.
|
|
170
|
+
export const MIN_GOG_VERSION = '0.37.0';
|
|
171
171
|
|
|
172
172
|
// Interpret the GOG_READONLY kill-switch. `readEnvVar` already treats blank
|
|
173
173
|
// values, 'undefined'/'null' sentinels, and unresolved .mcpb placeholders
|
package/src/tools/auth.ts
CHANGED
|
@@ -13,6 +13,20 @@ function registerAuthToolsWith(server: McpServer, defaultServices: string): void
|
|
|
13
13
|
`Default: "${defaultServices}". Prefer the narrowest set you need — requesting a service whose ` +
|
|
14
14
|
`Google API is not enabled on the OAuth client's project makes Google reject the WHOLE request ` +
|
|
15
15
|
`with invalid_scope.`;
|
|
16
|
+
// Additional raw OAuth scope URIs, appended after the service scopes gog
|
|
17
|
+
// derives from `services`. This exists for scopes no service selection can
|
|
18
|
+
// ask for: notably bigquery.readonly, which Google demands whenever a Sheets
|
|
19
|
+
// response CONTAINS BigQuery Connected Sheets data (gog_sheets_datasource_*)
|
|
20
|
+
// and which ordinary `sheets` authorization deliberately does not request.
|
|
21
|
+
// Same invalid_scope caveat as `services`: Google rejects the WHOLE request
|
|
22
|
+
// if the scope's API is not enabled on the OAuth client's project, and that
|
|
23
|
+
// happens in the user's browser, so this wrapper cannot catch it.
|
|
24
|
+
const extraScopesDescribe =
|
|
25
|
+
'Additional raw OAuth scope URIs to request, comma-separated, on top of the ones `services` implies. ' +
|
|
26
|
+
'Use for scopes no service covers — e.g. https://www.googleapis.com/auth/bigquery.readonly, required ' +
|
|
27
|
+
'before gog_sheets_datasource_* can read BigQuery-backed Connected Sheets. Leave unset otherwise: an ' +
|
|
28
|
+
'extra scope whose API is not enabled on the OAuth client project makes Google reject the WHOLE ' +
|
|
29
|
+
'authorization with invalid_scope.';
|
|
16
30
|
server.registerTool('gog_auth_list', {
|
|
17
31
|
description:
|
|
18
32
|
'List the Google accounts stored in gogcli, with their scopes. This reads local ' +
|
|
@@ -91,10 +105,19 @@ function registerAuthToolsWith(server: McpServer, defaultServices: string): void
|
|
|
91
105
|
inputSchema: {
|
|
92
106
|
email: z.string().describe('Google account email to authorize'),
|
|
93
107
|
services: z.string().optional().default(defaultServices).describe(servicesDescribe),
|
|
108
|
+
extraScopes: z.string().optional().describe(extraScopesDescribe),
|
|
94
109
|
},
|
|
95
|
-
}, async ({ email, services = defaultServices }) => {
|
|
110
|
+
}, async ({ email, services = defaultServices, extraScopes }) => {
|
|
96
111
|
try {
|
|
97
|
-
|
|
112
|
+
const args = ['auth', 'add', email, '--services', services];
|
|
113
|
+
// --force-consent rides along with extraScopes and only with them. Google
|
|
114
|
+
// re-prompts for a NEW scope only when consent is forced; without it the
|
|
115
|
+
// account can come back still missing the scope, with a success message —
|
|
116
|
+
// the exact shape of failure the caller cannot see. The other two tools
|
|
117
|
+
// force consent unconditionally; this one does not, so it must be added
|
|
118
|
+
// here rather than assumed.
|
|
119
|
+
if (extraScopes) args.push(`--extra-scopes=${extraScopes}`, '--force-consent');
|
|
120
|
+
return rawTextResult(await run(args, {
|
|
98
121
|
interactive: true,
|
|
99
122
|
timeout: 300_000,
|
|
100
123
|
}));
|
|
@@ -115,17 +138,17 @@ function registerAuthToolsWith(server: McpServer, defaultServices: string): void
|
|
|
115
138
|
inputSchema: {
|
|
116
139
|
email: z.string().describe('Google account email to authorize'),
|
|
117
140
|
services: z.string().optional().default(defaultServices).describe(servicesDescribe),
|
|
141
|
+
extraScopes: z.string().optional().describe(`${extraScopesDescribe} Pass the SAME value to gog_auth_add_complete.`),
|
|
118
142
|
},
|
|
119
|
-
}, async ({ email, services = defaultServices }) => {
|
|
143
|
+
}, async ({ email, services = defaultServices, extraScopes }) => {
|
|
120
144
|
try {
|
|
121
145
|
// --force-consent guarantees a refresh token even if a prior grant exists
|
|
122
146
|
// (the whole point when recovering from a dead one). redactMode 'tokens'
|
|
123
147
|
// keeps the consent URL's scope names intact (the shared redactor mangles
|
|
124
148
|
// them) while still stripping any real token — a step-1 URL carries none.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
));
|
|
149
|
+
const args = ['auth', 'add', email, '--remote', '--step', '1', '--services', services, '--force-consent'];
|
|
150
|
+
if (extraScopes) args.push(`--extra-scopes=${extraScopes}`);
|
|
151
|
+
return rawTextResult(await run(args, { redactMode: 'tokens' }));
|
|
129
152
|
} catch (err) {
|
|
130
153
|
return errorResult(errorText(err));
|
|
131
154
|
}
|
|
@@ -147,13 +170,17 @@ function registerAuthToolsWith(server: McpServer, defaultServices: string): void
|
|
|
147
170
|
services: z.string().optional().default(defaultServices).describe(
|
|
148
171
|
`Services authorized — MUST match the value passed to gog_auth_add_url. Default: "${defaultServices}".`,
|
|
149
172
|
),
|
|
173
|
+
extraScopes: z.string().optional().describe(
|
|
174
|
+
'Extra OAuth scope URIs — MUST match the value passed to gog_auth_add_url, for the same reason `services` must: ' +
|
|
175
|
+
'the two steps have to describe the same grant.',
|
|
176
|
+
),
|
|
150
177
|
},
|
|
151
|
-
}, async ({ email, redirectUrl, services = defaultServices }) => {
|
|
178
|
+
}, async ({ email, redirectUrl, services = defaultServices, extraScopes }) => {
|
|
152
179
|
try {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
));
|
|
180
|
+
const args = ['auth', 'add', email, '--remote', '--step', '2', '--auth-url', redirectUrl,
|
|
181
|
+
'--services', services, '--force-consent'];
|
|
182
|
+
if (extraScopes) args.push(`--extra-scopes=${extraScopes}`);
|
|
183
|
+
return rawTextResult(await run(args));
|
|
157
184
|
} catch (err) {
|
|
158
185
|
return errorResult(errorText(err));
|
|
159
186
|
}
|
package/src/tools/calendar.ts
CHANGED
|
@@ -5,15 +5,22 @@ import { annotateTruncatedList } from '../pagination.js';
|
|
|
5
5
|
|
|
6
6
|
export function registerCalendarTools(server: McpServer): void {
|
|
7
7
|
server.registerTool('gog_calendar_events', {
|
|
8
|
-
description: 'List calendar events.
|
|
8
|
+
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): '
|
|
9
|
+
+ '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. '
|
|
9
10
|
+ '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. '
|
|
10
11
|
+ 'A response carrying "truncated": true is an incomplete view — never conclude an event does not exist from one.',
|
|
11
12
|
annotations: { readOnlyHint: true },
|
|
12
13
|
inputSchema: {
|
|
13
14
|
calendarId: z.string().optional().describe('Calendar ID (default: primary calendar)'),
|
|
14
15
|
from: z.string().optional().describe('Start time filter (RFC3339, date, or natural language)'),
|
|
15
|
-
to: z.string().optional().describe('End time filter (RFC3339, date, or natural language)'),
|
|
16
|
-
|
|
16
|
+
to: z.string().optional().describe('End time filter (RFC3339, date, or natural language). Mutually exclusive with today and with days.'),
|
|
17
|
+
// gog >= 0.36.0 (openclaw/gogcli#981). Before that release --days sat in
|
|
18
|
+
// a switch arm evaluated ahead of --from, so `--from 2026-09-25 --days 5`
|
|
19
|
+
// silently threw --from away and answered for today instead — at exit 0,
|
|
20
|
+
// in a well-formed table. It is only exposed here now that it means what
|
|
21
|
+
// it says.
|
|
22
|
+
days: z.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.'),
|
|
23
|
+
today: z.boolean().optional().describe('Only show today\'s events. A complete window on its own — mutually exclusive with from, to and days.'),
|
|
17
24
|
query: z.string().optional().describe('Free text search within events'),
|
|
18
25
|
max: z.number().int().optional().describe('Max events to return. gog defaults to 10, which silently hides the rest — raise it, or page with pageToken.'),
|
|
19
26
|
pageToken: pageTokenParam,
|
|
@@ -23,11 +30,12 @@ export function registerCalendarTools(server: McpServer): void {
|
|
|
23
30
|
timezone: z.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.'),
|
|
24
31
|
account: accountParam,
|
|
25
32
|
},
|
|
26
|
-
}, async ({ calendarId, from, to, today, query, max, pageToken, page, all, eventTypes, timezone, account }) => {
|
|
33
|
+
}, async ({ calendarId, from, to, days, today, query, max, pageToken, page, all, eventTypes, timezone, account }) => {
|
|
27
34
|
const args = ['calendar', 'events'];
|
|
28
35
|
if (calendarId) args.push(calendarId);
|
|
29
36
|
if (from) args.push(`--from=${from}`);
|
|
30
37
|
if (to) args.push(`--to=${to}`);
|
|
38
|
+
if (days !== undefined) args.push(`--days=${days}`);
|
|
31
39
|
if (today) args.push('--today');
|
|
32
40
|
if (query) args.push(`--query=${query}`);
|
|
33
41
|
if (max !== undefined) args.push(`--max=${max}`);
|
package/src/tools/gmail.ts
CHANGED
|
@@ -46,16 +46,22 @@ export function registerGmailTools(server: McpServer): void {
|
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
server.registerTool('gog_gmail_get', {
|
|
49
|
-
description: 'Get a Gmail message by ID.',
|
|
49
|
+
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.',
|
|
50
50
|
annotations: { readOnlyHint: true },
|
|
51
51
|
inputSchema: {
|
|
52
52
|
messageId: z.string().describe('Message ID'),
|
|
53
53
|
format: z.enum(['full', 'metadata', 'raw']).optional().describe('Message format (default: full)'),
|
|
54
|
+
// Requires gog >= 0.37.0. Before that (openclaw/gogcli#992) the JSON
|
|
55
|
+
// carried the headers and body TWICE — once inside `message`, once
|
|
56
|
+
// copied to the top level — so the flag meant to shrink the payload
|
|
57
|
+
// enlarged it. MIN_GOG_VERSION is the guard; there is no runtime check.
|
|
58
|
+
sanitizeContent: z.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 — omit this when you need to follow a link out of the message.'),
|
|
54
59
|
account: accountParam,
|
|
55
60
|
},
|
|
56
|
-
}, async ({ messageId, format, account }) => {
|
|
61
|
+
}, async ({ messageId, format, sanitizeContent, account }) => {
|
|
57
62
|
const args = ['gmail', 'get', messageId];
|
|
58
63
|
if (format) args.push(`--format=${format}`);
|
|
64
|
+
if (sanitizeContent) args.push('--sanitize-content');
|
|
59
65
|
return runOrDiagnose(args, { account });
|
|
60
66
|
});
|
|
61
67
|
|
package/src/worker.ts
CHANGED
|
@@ -38,7 +38,7 @@ import { gogAuth, CONNECTOR_INSTRUCTIONS, type GogProps } from './connector-auth
|
|
|
38
38
|
// connector with all ~360 tools at once. Add whichever paths you want as separate
|
|
39
39
|
// connectors in claude.ai (each authorizes with the same connector key).
|
|
40
40
|
|
|
41
|
-
const VERSION = '2.
|
|
41
|
+
const VERSION = '2.24.0'; // x-release-please-version
|
|
42
42
|
|
|
43
43
|
// Build an McpAgent subclass whose init() registers `registrars` onto its server,
|
|
44
44
|
// each handler wrapped in the ALS scope carrying the per-session Fly executor.
|
package/tests/tools/auth.test.ts
CHANGED
|
@@ -124,6 +124,35 @@ describe('gog_auth_add', () => {
|
|
|
124
124
|
);
|
|
125
125
|
});
|
|
126
126
|
|
|
127
|
+
// gog 0.37.0's Connected Sheets reads need bigquery.readonly, which no
|
|
128
|
+
// `services` selection covers. --force-consent is not optional alongside it:
|
|
129
|
+
// Google re-prompts for a NEW scope only when consent is forced, so without
|
|
130
|
+
// it the grant can come back missing the scope AND reporting success.
|
|
131
|
+
it('passes --extra-scopes with --force-consent', async () => {
|
|
132
|
+
vi.mocked(runner.run).mockResolvedValue('Authorization successful');
|
|
133
|
+
const harness = await setupHandlers();
|
|
134
|
+
await harness.callTool('gog_auth_add', {
|
|
135
|
+
email: 'user@gmail.com',
|
|
136
|
+
services: 'sheets',
|
|
137
|
+
extraScopes: 'https://www.googleapis.com/auth/bigquery.readonly',
|
|
138
|
+
});
|
|
139
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
140
|
+
['auth', 'add', 'user@gmail.com', '--services', 'sheets',
|
|
141
|
+
'--extra-scopes=https://www.googleapis.com/auth/bigquery.readonly', '--force-consent'],
|
|
142
|
+
{ interactive: true, timeout: 300_000 },
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('does not force consent when no extra scopes are asked for', async () => {
|
|
147
|
+
vi.mocked(runner.run).mockResolvedValue('Authorization successful');
|
|
148
|
+
const harness = await setupHandlers();
|
|
149
|
+
await harness.callTool('gog_auth_add', { email: 'user@gmail.com' });
|
|
150
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
151
|
+
['auth', 'add', 'user@gmail.com', '--services', 'all'],
|
|
152
|
+
{ interactive: true, timeout: 300_000 },
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
127
156
|
it('returns error text on failure', async () => {
|
|
128
157
|
vi.mocked(runner.run).mockRejectedValue(new Error('Auth cancelled by user'));
|
|
129
158
|
const harness = await setupHandlers();
|
|
@@ -215,6 +244,21 @@ describe('gog_auth_add_url', () => {
|
|
|
215
244
|
);
|
|
216
245
|
});
|
|
217
246
|
|
|
247
|
+
it('appends --extra-scopes after the service scopes', async () => {
|
|
248
|
+
vi.mocked(runner.run).mockResolvedValue('{"auth_url":"https://x"}');
|
|
249
|
+
const harness = await setupHandlers();
|
|
250
|
+
await harness.callTool('gog_auth_add_url', {
|
|
251
|
+
email: 'user@gmail.com',
|
|
252
|
+
services: 'sheets',
|
|
253
|
+
extraScopes: 'https://www.googleapis.com/auth/bigquery.readonly',
|
|
254
|
+
});
|
|
255
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
256
|
+
['auth', 'add', 'user@gmail.com', '--remote', '--step', '1', '--services', 'sheets', '--force-consent',
|
|
257
|
+
'--extra-scopes=https://www.googleapis.com/auth/bigquery.readonly'],
|
|
258
|
+
{ redactMode: 'tokens' },
|
|
259
|
+
);
|
|
260
|
+
});
|
|
261
|
+
|
|
218
262
|
it('returns error text on failure', async () => {
|
|
219
263
|
vi.mocked(runner.run).mockRejectedValue(new Error('client not configured'));
|
|
220
264
|
const harness = await setupHandlers();
|
|
@@ -252,6 +296,22 @@ describe('gog_auth_add_complete', () => {
|
|
|
252
296
|
);
|
|
253
297
|
});
|
|
254
298
|
|
|
299
|
+
it('carries the same --extra-scopes as step 1', async () => {
|
|
300
|
+
vi.mocked(runner.run).mockResolvedValue('{"stored":true}');
|
|
301
|
+
const harness = await setupHandlers();
|
|
302
|
+
await harness.callTool('gog_auth_add_complete', {
|
|
303
|
+
email: 'user@gmail.com',
|
|
304
|
+
redirectUrl: 'http://127.0.0.1/cb?code=c&state=s',
|
|
305
|
+
services: 'sheets',
|
|
306
|
+
extraScopes: 'https://www.googleapis.com/auth/bigquery.readonly',
|
|
307
|
+
});
|
|
308
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
309
|
+
['auth', 'add', 'user@gmail.com', '--remote', '--step', '2', '--auth-url',
|
|
310
|
+
'http://127.0.0.1/cb?code=c&state=s', '--services', 'sheets', '--force-consent',
|
|
311
|
+
'--extra-scopes=https://www.googleapis.com/auth/bigquery.readonly'],
|
|
312
|
+
);
|
|
313
|
+
});
|
|
314
|
+
|
|
255
315
|
it('returns error text on failure (e.g. expired state)', async () => {
|
|
256
316
|
vi.mocked(runner.run).mockRejectedValue(new Error('no matching manual auth state'));
|
|
257
317
|
const harness = await setupHandlers();
|
|
@@ -17,23 +17,50 @@ describe('gog_calendar_events', () => {
|
|
|
17
17
|
expect(runner.run).toHaveBeenCalledWith(['calendar', 'events'], { account: undefined });
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
// The window flags are pinned as SEPARATE cases on purpose: gog >= 0.36.0
|
|
21
|
+
// (openclaw/gogcli#981) rejects a fixed preset combined with from/to/days,
|
|
22
|
+
// and days combined with to, so one test passing them all at once would
|
|
23
|
+
// assert an arg array gog refuses to run.
|
|
24
|
+
it('appends calendarId and an explicit from/to range', async () => {
|
|
21
25
|
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
22
26
|
const harness = await setupHandlers();
|
|
23
27
|
await harness.callTool('gog_calendar_events', {
|
|
24
28
|
calendarId: 'primary',
|
|
25
29
|
from: '2026-01-01',
|
|
26
30
|
to: '2026-01-31',
|
|
27
|
-
today: true,
|
|
28
31
|
query: 'standup',
|
|
29
32
|
all: true,
|
|
30
33
|
});
|
|
31
34
|
expect(runner.run).toHaveBeenCalledWith(
|
|
32
|
-
['calendar', 'events', 'primary', '--from=2026-01-01', '--to=2026-01-31', '--
|
|
35
|
+
['calendar', 'events', 'primary', '--from=2026-01-01', '--to=2026-01-31', '--query=standup', '--all'],
|
|
33
36
|
{ account: undefined },
|
|
34
37
|
);
|
|
35
38
|
});
|
|
36
39
|
|
|
40
|
+
it('appends --today on its own', async () => {
|
|
41
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
42
|
+
const harness = await setupHandlers();
|
|
43
|
+
await harness.callTool('gog_calendar_events', { today: true });
|
|
44
|
+
expect(runner.run).toHaveBeenCalledWith(['calendar', 'events', '--today'], { account: undefined });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('anchors --days at --from when both are given', async () => {
|
|
48
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
49
|
+
const harness = await setupHandlers();
|
|
50
|
+
await harness.callTool('gog_calendar_events', { from: '2026-09-25', days: 5 });
|
|
51
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
52
|
+
['calendar', 'events', '--from=2026-09-25', '--days=5'],
|
|
53
|
+
{ account: undefined },
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('passes --days alone as a today-anchored window', async () => {
|
|
58
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
59
|
+
const harness = await setupHandlers();
|
|
60
|
+
await harness.callTool('gog_calendar_events', { days: 7 });
|
|
61
|
+
expect(runner.run).toHaveBeenCalledWith(['calendar', 'events', '--days=7'], { account: undefined });
|
|
62
|
+
});
|
|
63
|
+
|
|
37
64
|
it('repeats --event-types for each requested type', async () => {
|
|
38
65
|
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
39
66
|
const harness = await setupHandlers();
|
|
@@ -201,6 +201,23 @@ describe('gog_gmail_get', () => {
|
|
|
201
201
|
expect(runner.run).toHaveBeenCalledWith(['gmail', 'get', 'msg1', '--format=metadata'], { account: undefined });
|
|
202
202
|
});
|
|
203
203
|
|
|
204
|
+
// gog >= 0.37.0 (openclaw/gogcli#992): before that release the sanitized
|
|
205
|
+
// JSON repeated the headers and body at the top level, so this flag grew the
|
|
206
|
+
// payload it exists to shrink. Pinned here as the flag spelling gog expects.
|
|
207
|
+
it('appends --sanitize-content when asked', async () => {
|
|
208
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
209
|
+
const harness = await setupHandlers();
|
|
210
|
+
await harness.callTool('gog_gmail_get', { messageId: 'msg1', sanitizeContent: true });
|
|
211
|
+
expect(runner.run).toHaveBeenCalledWith(['gmail', 'get', 'msg1', '--sanitize-content'], { account: undefined });
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('omits --sanitize-content when false', async () => {
|
|
215
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
216
|
+
const harness = await setupHandlers();
|
|
217
|
+
await harness.callTool('gog_gmail_get', { messageId: 'msg1', sanitizeContent: false });
|
|
218
|
+
expect(runner.run).toHaveBeenCalledWith(['gmail', 'get', 'msg1'], { account: undefined });
|
|
219
|
+
});
|
|
220
|
+
|
|
204
221
|
it('returns error text on failure', async () => {
|
|
205
222
|
vi.mocked(runner.run).mockRejectedValue(new Error('Not found'));
|
|
206
223
|
const harness = await setupHandlers();
|