gogcli-mcp 2.23.2 → 2.25.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 +173 -44
- package/dist/lib.js +182 -45
- package/manifest.json +2 -2
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/attachments.ts +263 -0
- package/src/gmail-results.ts +15 -4
- package/src/lib.ts +14 -0
- package/src/runner.ts +170 -15
- package/src/tools/auth.ts +39 -12
- package/src/tools/calendar.ts +12 -4
- package/src/tools/gmail.ts +25 -5
- package/src/worker.ts +1 -1
- package/tests/attachments.test.ts +227 -0
- package/tests/runner-file-args-failure.test.ts +48 -3
- package/tests/runner.test.ts +126 -0
- package/tests/tools/auth.test.ts +60 -0
- package/tests/tools/calendar.test.ts +30 -3
- package/tests/tools/gmail.test.ts +106 -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.25.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.25.0",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
package/dist/index.js
CHANGED
|
@@ -31177,10 +31177,11 @@ function sanitizedEnv() {
|
|
|
31177
31177
|
}
|
|
31178
31178
|
return result;
|
|
31179
31179
|
}
|
|
31180
|
+
var TOKEN_LEFT_BOUNDARY = "(?<![A-Za-z0-9+/])";
|
|
31180
31181
|
var GOOGLE_TOKEN_PATTERNS = [
|
|
31181
|
-
|
|
31182
|
+
new RegExp(`${TOKEN_LEFT_BOUNDARY}ya29\\.[A-Za-z0-9._\\-]+`, "g"),
|
|
31182
31183
|
// OAuth2 access tokens
|
|
31183
|
-
|
|
31184
|
+
new RegExp(`${TOKEN_LEFT_BOUNDARY}1//[A-Za-z0-9._\\-]+`, "g")
|
|
31184
31185
|
// OAuth2 refresh tokens
|
|
31185
31186
|
];
|
|
31186
31187
|
function redactGoogleTokens(text) {
|
|
@@ -31193,6 +31194,26 @@ function redactGoogleTokens(text) {
|
|
|
31193
31194
|
function redactSecrets2(text) {
|
|
31194
31195
|
return redactGoogleTokens(redactSecrets(text));
|
|
31195
31196
|
}
|
|
31197
|
+
var OPAQUE_FIELD_VALUE = "[A-Za-z0-9+/_-]{16,}={0,2}";
|
|
31198
|
+
var opaquePlaceholder = (i) => `\0gogOpaque${i}\0`;
|
|
31199
|
+
function redactPreservingOpaqueFields(text, fields, redact) {
|
|
31200
|
+
const lifted = [];
|
|
31201
|
+
let staged = text;
|
|
31202
|
+
for (const field of fields) {
|
|
31203
|
+
const escaped = field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
31204
|
+
const re = new RegExp(`("${escaped}"\\s*:\\s*")(${OPAQUE_FIELD_VALUE})(")`, "g");
|
|
31205
|
+
staged = staged.replace(re, (_m, open, value, close) => {
|
|
31206
|
+
lifted.push(value);
|
|
31207
|
+
return `${open}${opaquePlaceholder(lifted.length - 1)}${close}`;
|
|
31208
|
+
});
|
|
31209
|
+
}
|
|
31210
|
+
if (lifted.length === 0) return redact(text);
|
|
31211
|
+
let redacted = redact(staged);
|
|
31212
|
+
lifted.forEach((value, i) => {
|
|
31213
|
+
redacted = redacted.split(opaquePlaceholder(i)).join(value);
|
|
31214
|
+
});
|
|
31215
|
+
return redacted;
|
|
31216
|
+
}
|
|
31196
31217
|
function augmentedPath() {
|
|
31197
31218
|
const home = process.env.HOME;
|
|
31198
31219
|
const candidates = [
|
|
@@ -31223,19 +31244,24 @@ function formatTimeout(ms) {
|
|
|
31223
31244
|
return `${ms}ms`;
|
|
31224
31245
|
}
|
|
31225
31246
|
async function spawnWithTempFiles(args, opts) {
|
|
31226
|
-
const { mkdtemp, writeFile, rm } = await import("node:fs/promises");
|
|
31247
|
+
const { mkdtemp, mkdir, writeFile, rm } = await import("node:fs/promises");
|
|
31227
31248
|
const { tmpdir } = await import("node:os");
|
|
31228
31249
|
const dir = await mkdtemp(join(tmpdir(), "gogcli-mcp-"));
|
|
31229
31250
|
try {
|
|
31230
31251
|
const argv = [];
|
|
31252
|
+
let seq = 0;
|
|
31231
31253
|
for (const arg of args) {
|
|
31232
31254
|
if (!isGogFileArg(arg)) {
|
|
31233
31255
|
argv.push(arg);
|
|
31234
31256
|
continue;
|
|
31235
31257
|
}
|
|
31236
|
-
const
|
|
31237
|
-
|
|
31238
|
-
|
|
31258
|
+
const sub = join(dir, String(seq));
|
|
31259
|
+
seq += 1;
|
|
31260
|
+
await mkdir(sub, { recursive: true, mode: 448 });
|
|
31261
|
+
const path = join(sub, arg.filename ?? `${arg.flag}.${arg.ext ?? "txt"}`);
|
|
31262
|
+
const data = arg.encoding === "base64" ? Buffer.from(arg.contents, "base64") : Buffer.from(arg.contents, "utf8");
|
|
31263
|
+
await writeFile(path, data, { mode: 384 });
|
|
31264
|
+
argv.push(arg.positional ? path : `--${arg.flag}=${path}`);
|
|
31239
31265
|
}
|
|
31240
31266
|
return await spawnGog(argv, opts);
|
|
31241
31267
|
} finally {
|
|
@@ -31320,8 +31346,9 @@ function assembleArgs(args, opts) {
|
|
|
31320
31346
|
return fullArgs;
|
|
31321
31347
|
}
|
|
31322
31348
|
async function run(args, options = {}) {
|
|
31323
|
-
const { account, spawner, interactive = false, timeout, readonly: readonly2 = false, redactMode = "full" } = options;
|
|
31324
|
-
const
|
|
31349
|
+
const { account, spawner, interactive = false, timeout, readonly: readonly2 = false, redactMode = "full", opaqueFields } = options;
|
|
31350
|
+
const base = redactMode === "tokens" ? redactGoogleTokens : redactSecrets2;
|
|
31351
|
+
const redact = opaqueFields?.length ? (text) => redactPreservingOpaqueFields(text, opaqueFields, base) : base;
|
|
31325
31352
|
const fullArgs = assembleArgs(args, { account, interactive, readonly: readonly2 });
|
|
31326
31353
|
const store = activeExecutor();
|
|
31327
31354
|
try {
|
|
@@ -31335,7 +31362,7 @@ async function run(args, options = {}) {
|
|
|
31335
31362
|
}
|
|
31336
31363
|
return redact(output);
|
|
31337
31364
|
} catch (err) {
|
|
31338
|
-
const message =
|
|
31365
|
+
const message = base(err instanceof Error ? err.message : String(err));
|
|
31339
31366
|
if (isRunnerTransportError(err)) {
|
|
31340
31367
|
throw new RunnerTransportError(message, err.kind, err.status);
|
|
31341
31368
|
}
|
|
@@ -31827,6 +31854,7 @@ function registerApiTools(server) {
|
|
|
31827
31854
|
// src/tools/auth.ts
|
|
31828
31855
|
function registerAuthToolsWith(server, defaultServices) {
|
|
31829
31856
|
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.`;
|
|
31857
|
+
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
31858
|
server.registerTool("gog_auth_list", {
|
|
31831
31859
|
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
31860
|
annotations: { readOnlyHint: true },
|
|
@@ -31876,11 +31904,14 @@ function registerAuthToolsWith(server, defaultServices) {
|
|
|
31876
31904
|
annotations: { destructiveHint: true },
|
|
31877
31905
|
inputSchema: {
|
|
31878
31906
|
email: external_exports.string().describe("Google account email to authorize"),
|
|
31879
|
-
services: external_exports.string().optional().default(defaultServices).describe(servicesDescribe)
|
|
31907
|
+
services: external_exports.string().optional().default(defaultServices).describe(servicesDescribe),
|
|
31908
|
+
extraScopes: external_exports.string().optional().describe(extraScopesDescribe)
|
|
31880
31909
|
}
|
|
31881
|
-
}, async ({ email: email3, services = defaultServices }) => {
|
|
31910
|
+
}, async ({ email: email3, services = defaultServices, extraScopes }) => {
|
|
31882
31911
|
try {
|
|
31883
|
-
|
|
31912
|
+
const args = ["auth", "add", email3, "--services", services];
|
|
31913
|
+
if (extraScopes) args.push(`--extra-scopes=${extraScopes}`, "--force-consent");
|
|
31914
|
+
return rawTextResult(await run(args, {
|
|
31884
31915
|
interactive: true,
|
|
31885
31916
|
timeout: 3e5
|
|
31886
31917
|
}));
|
|
@@ -31892,14 +31923,14 @@ function registerAuthToolsWith(server, defaultServices) {
|
|
|
31892
31923
|
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
31924
|
inputSchema: {
|
|
31894
31925
|
email: external_exports.string().describe("Google account email to authorize"),
|
|
31895
|
-
services: external_exports.string().optional().default(defaultServices).describe(servicesDescribe)
|
|
31926
|
+
services: external_exports.string().optional().default(defaultServices).describe(servicesDescribe),
|
|
31927
|
+
extraScopes: external_exports.string().optional().describe(`${extraScopesDescribe} Pass the SAME value to gog_auth_add_complete.`)
|
|
31896
31928
|
}
|
|
31897
|
-
}, async ({ email: email3, services = defaultServices }) => {
|
|
31929
|
+
}, async ({ email: email3, services = defaultServices, extraScopes }) => {
|
|
31898
31930
|
try {
|
|
31899
|
-
|
|
31900
|
-
|
|
31901
|
-
|
|
31902
|
-
));
|
|
31931
|
+
const args = ["auth", "add", email3, "--remote", "--step", "1", "--services", services, "--force-consent"];
|
|
31932
|
+
if (extraScopes) args.push(`--extra-scopes=${extraScopes}`);
|
|
31933
|
+
return rawTextResult(await run(args, { redactMode: "tokens" }));
|
|
31903
31934
|
} catch (err) {
|
|
31904
31935
|
return errorResult(errorText(err));
|
|
31905
31936
|
}
|
|
@@ -31914,25 +31945,28 @@ function registerAuthToolsWith(server, defaultServices) {
|
|
|
31914
31945
|
),
|
|
31915
31946
|
services: external_exports.string().optional().default(defaultServices).describe(
|
|
31916
31947
|
`Services authorized \u2014 MUST match the value passed to gog_auth_add_url. Default: "${defaultServices}".`
|
|
31948
|
+
),
|
|
31949
|
+
extraScopes: external_exports.string().optional().describe(
|
|
31950
|
+
"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
31951
|
)
|
|
31918
31952
|
}
|
|
31919
|
-
}, async ({ email: email3, redirectUrl, services = defaultServices }) => {
|
|
31953
|
+
}, async ({ email: email3, redirectUrl, services = defaultServices, extraScopes }) => {
|
|
31920
31954
|
try {
|
|
31921
|
-
|
|
31922
|
-
|
|
31923
|
-
|
|
31924
|
-
|
|
31925
|
-
|
|
31926
|
-
|
|
31927
|
-
|
|
31928
|
-
|
|
31929
|
-
|
|
31930
|
-
|
|
31931
|
-
|
|
31932
|
-
|
|
31933
|
-
|
|
31934
|
-
|
|
31935
|
-
));
|
|
31955
|
+
const args = [
|
|
31956
|
+
"auth",
|
|
31957
|
+
"add",
|
|
31958
|
+
email3,
|
|
31959
|
+
"--remote",
|
|
31960
|
+
"--step",
|
|
31961
|
+
"2",
|
|
31962
|
+
"--auth-url",
|
|
31963
|
+
redirectUrl,
|
|
31964
|
+
"--services",
|
|
31965
|
+
services,
|
|
31966
|
+
"--force-consent"
|
|
31967
|
+
];
|
|
31968
|
+
if (extraScopes) args.push(`--extra-scopes=${extraScopes}`);
|
|
31969
|
+
return rawTextResult(await run(args));
|
|
31936
31970
|
} catch (err) {
|
|
31937
31971
|
return errorResult(errorText(err));
|
|
31938
31972
|
}
|
|
@@ -31951,13 +31985,19 @@ function registerAuthTools(server) {
|
|
|
31951
31985
|
// src/tools/calendar.ts
|
|
31952
31986
|
function registerCalendarTools(server) {
|
|
31953
31987
|
server.registerTool("gog_calendar_events", {
|
|
31954
|
-
description: 'List calendar events.
|
|
31988
|
+
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
31989
|
annotations: { readOnlyHint: true },
|
|
31956
31990
|
inputSchema: {
|
|
31957
31991
|
calendarId: external_exports.string().optional().describe("Calendar ID (default: primary calendar)"),
|
|
31958
31992
|
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
|
-
|
|
31993
|
+
to: external_exports.string().optional().describe("End time filter (RFC3339, date, or natural language). Mutually exclusive with today and with days."),
|
|
31994
|
+
// gog >= 0.36.0 (openclaw/gogcli#981). Before that release --days sat in
|
|
31995
|
+
// a switch arm evaluated ahead of --from, so `--from 2026-09-25 --days 5`
|
|
31996
|
+
// silently threw --from away and answered for today instead — at exit 0,
|
|
31997
|
+
// in a well-formed table. It is only exposed here now that it means what
|
|
31998
|
+
// it says.
|
|
31999
|
+
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.'),
|
|
32000
|
+
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
32001
|
query: external_exports.string().optional().describe("Free text search within events"),
|
|
31962
32002
|
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
32003
|
pageToken: pageTokenParam,
|
|
@@ -31967,11 +32007,12 @@ function registerCalendarTools(server) {
|
|
|
31967
32007
|
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
32008
|
account: accountParam
|
|
31969
32009
|
}
|
|
31970
|
-
}, async ({ calendarId, from, to, today, query, max, pageToken, page, all, eventTypes, timezone, account }) => {
|
|
32010
|
+
}, async ({ calendarId, from, to, days, today, query, max, pageToken, page, all, eventTypes, timezone, account }) => {
|
|
31971
32011
|
const args = ["calendar", "events"];
|
|
31972
32012
|
if (calendarId) args.push(calendarId);
|
|
31973
32013
|
if (from) args.push(`--from=${from}`);
|
|
31974
32014
|
if (to) args.push(`--to=${to}`);
|
|
32015
|
+
if (days !== void 0) args.push(`--days=${days}`);
|
|
31975
32016
|
if (today) args.push("--today");
|
|
31976
32017
|
if (query) args.push(`--query=${query}`);
|
|
31977
32018
|
if (max !== void 0) args.push(`--max=${max}`);
|
|
@@ -32936,6 +32977,85 @@ function finish(base, itemsKey, merged, token) {
|
|
|
32936
32977
|
return rawTextResult(JSON.stringify(out));
|
|
32937
32978
|
}
|
|
32938
32979
|
|
|
32980
|
+
// src/attachments.ts
|
|
32981
|
+
var MAX_INLINE_ATTACHMENT_BYTES = 8 * 1024 * 1024;
|
|
32982
|
+
var RUNNER_MAX_BODY_BYTES = 32 * 1024 * 1024;
|
|
32983
|
+
var RUNNER_BODY_JSON_RESERVE_BYTES = 256 * 1024;
|
|
32984
|
+
var MAX_REQUEST_PAYLOAD_WIRE_BYTES = RUNNER_MAX_BODY_BYTES - RUNNER_BODY_JSON_RESERVE_BYTES;
|
|
32985
|
+
var MAX_INLINE_ATTACHMENT_TOTAL_BYTES = Math.floor(MAX_REQUEST_PAYLOAD_WIRE_BYTES * 3 / 4);
|
|
32986
|
+
function wireBytesOf(arg) {
|
|
32987
|
+
if (typeof arg === "string") return Buffer.byteLength(arg, "utf8");
|
|
32988
|
+
return arg.encoding === "base64" ? arg.contents.length : Buffer.byteLength(arg.contents, "utf8");
|
|
32989
|
+
}
|
|
32990
|
+
var formatMiB = (bytes) => `${Math.floor(bytes / (1024 * 1024))} MiB`;
|
|
32991
|
+
var INLINE_ATTACHMENT_LIMITS_TEXT = `up to ${formatMiB(MAX_INLINE_ATTACHMENT_BYTES)} per file and ${formatMiB(MAX_INLINE_ATTACHMENT_TOTAL_BYTES)} in total`;
|
|
32992
|
+
var inlineAttachmentSchema = external_exports.object({
|
|
32993
|
+
filename: external_exports.string().min(1).describe(
|
|
32994
|
+
`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.`
|
|
32995
|
+
),
|
|
32996
|
+
contentBase64: external_exports.string().min(1).describe(
|
|
32997
|
+
"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."
|
|
32998
|
+
)
|
|
32999
|
+
});
|
|
33000
|
+
var attachInlineParam = external_exports.array(inlineAttachmentSchema).optional().describe(
|
|
33001
|
+
`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.`
|
|
33002
|
+
);
|
|
33003
|
+
function validateFilename(filename, where) {
|
|
33004
|
+
if (/[/\\]/.test(filename)) {
|
|
33005
|
+
throw new Error(
|
|
33006
|
+
`${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".`
|
|
33007
|
+
);
|
|
33008
|
+
}
|
|
33009
|
+
if (/[\x00-\x1f]/.test(filename) || /^\.+$/.test(filename) || filename.length > 200) {
|
|
33010
|
+
throw new Error(
|
|
33011
|
+
`${where}: filename ${JSON.stringify(filename)} is not a usable filename (no control characters, not "."/"..", 200 characters max).`
|
|
33012
|
+
);
|
|
33013
|
+
}
|
|
33014
|
+
}
|
|
33015
|
+
function decodedLength(contentBase64) {
|
|
33016
|
+
const buf = Buffer.from(contentBase64, "base64");
|
|
33017
|
+
return buf.toString("base64") === contentBase64 ? buf.length : null;
|
|
33018
|
+
}
|
|
33019
|
+
function inlineFileArg(flag, attachment, opts = {}) {
|
|
33020
|
+
const { filename, contentBase64 } = attachment;
|
|
33021
|
+
const where = opts.where ?? `attachInline entry ${JSON.stringify(filename)}`;
|
|
33022
|
+
validateFilename(filename, where);
|
|
33023
|
+
const bytes = decodedLength(contentBase64);
|
|
33024
|
+
if (bytes === null) {
|
|
33025
|
+
throw new Error(
|
|
33026
|
+
`${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.`
|
|
33027
|
+
);
|
|
33028
|
+
}
|
|
33029
|
+
if (bytes > MAX_INLINE_ATTACHMENT_BYTES) {
|
|
33030
|
+
throw new Error(
|
|
33031
|
+
`${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.`
|
|
33032
|
+
);
|
|
33033
|
+
}
|
|
33034
|
+
const arg = { kind: "file", flag, contents: contentBase64, encoding: "base64", filename };
|
|
33035
|
+
if (opts.positional) arg.positional = true;
|
|
33036
|
+
return { arg, bytes };
|
|
33037
|
+
}
|
|
33038
|
+
function inlineAttachmentArgs(flag, attachments, siblingArgs = []) {
|
|
33039
|
+
if (!attachments?.length) return [];
|
|
33040
|
+
const args = [];
|
|
33041
|
+
const siblingWire = siblingArgs.reduce((sum, arg) => sum + wireBytesOf(arg), 0);
|
|
33042
|
+
let attachmentWire = 0;
|
|
33043
|
+
let decodedTotal = 0;
|
|
33044
|
+
for (const attachment of attachments) {
|
|
33045
|
+
const { arg, bytes } = inlineFileArg(flag, attachment);
|
|
33046
|
+
attachmentWire += arg.contents.length;
|
|
33047
|
+
decodedTotal += bytes;
|
|
33048
|
+
if (siblingWire + attachmentWire > MAX_REQUEST_PAYLOAD_WIRE_BYTES) {
|
|
33049
|
+
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.` : "";
|
|
33050
|
+
throw new Error(
|
|
33051
|
+
`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.`
|
|
33052
|
+
);
|
|
33053
|
+
}
|
|
33054
|
+
args.push(arg);
|
|
33055
|
+
}
|
|
33056
|
+
return args;
|
|
33057
|
+
}
|
|
33058
|
+
|
|
32939
33059
|
// src/tools/gmail.ts
|
|
32940
33060
|
function registerGmailTools(server) {
|
|
32941
33061
|
server.registerTool("gog_gmail_search", {
|
|
@@ -32970,20 +33090,26 @@ function registerGmailTools(server) {
|
|
|
32970
33090
|
});
|
|
32971
33091
|
});
|
|
32972
33092
|
server.registerTool("gog_gmail_get", {
|
|
32973
|
-
description: "Get a Gmail message by ID.",
|
|
33093
|
+
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
33094
|
annotations: { readOnlyHint: true },
|
|
32975
33095
|
inputSchema: {
|
|
32976
33096
|
messageId: external_exports.string().describe("Message ID"),
|
|
32977
33097
|
format: external_exports.enum(["full", "metadata", "raw"]).optional().describe("Message format (default: full)"),
|
|
33098
|
+
// Requires gog >= 0.37.0. Before that (openclaw/gogcli#992) the JSON
|
|
33099
|
+
// carried the headers and body TWICE — once inside `message`, once
|
|
33100
|
+
// copied to the top level — so the flag meant to shrink the payload
|
|
33101
|
+
// enlarged it. MIN_GOG_VERSION is the guard; there is no runtime check.
|
|
33102
|
+
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
33103
|
account: accountParam
|
|
32979
33104
|
}
|
|
32980
|
-
}, async ({ messageId, format, account }) => {
|
|
33105
|
+
}, async ({ messageId, format, sanitizeContent, account }) => {
|
|
32981
33106
|
const args = ["gmail", "get", messageId];
|
|
32982
33107
|
if (format) args.push(`--format=${format}`);
|
|
33108
|
+
if (sanitizeContent) args.push("--sanitize-content");
|
|
32983
33109
|
return runOrDiagnose(args, { account });
|
|
32984
33110
|
});
|
|
32985
33111
|
server.registerTool("gog_gmail_send", {
|
|
32986
|
-
description:
|
|
33112
|
+
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.',
|
|
32987
33113
|
annotations: { destructiveHint: true },
|
|
32988
33114
|
inputSchema: {
|
|
32989
33115
|
to: external_exports.string().describe("Recipient(s), comma-separated"),
|
|
@@ -32993,16 +33119,19 @@ function registerGmailTools(server) {
|
|
|
32993
33119
|
bcc: external_exports.string().optional().describe("BCC recipients, comma-separated"),
|
|
32994
33120
|
replyToMessageId: external_exports.string().optional().describe("Message ID to reply to"),
|
|
32995
33121
|
threadId: external_exports.string().optional().describe("Thread ID to reply within"),
|
|
32996
|
-
attach: external_exports.array(external_exports.string()).optional().describe(
|
|
33122
|
+
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.`),
|
|
33123
|
+
attachInline: attachInlineParam,
|
|
32997
33124
|
account: accountParam
|
|
32998
33125
|
}
|
|
32999
|
-
}, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, attach, account }) => {
|
|
33126
|
+
}, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, attach, attachInline, account }) => {
|
|
33000
33127
|
const args = ["gmail", "send", `--to=${to}`, `--subject=${subject}`, payloadArg("body", "body-file", body)];
|
|
33001
33128
|
if (cc) args.push(`--cc=${cc}`);
|
|
33002
33129
|
if (bcc) args.push(`--bcc=${bcc}`);
|
|
33003
33130
|
if (replyToMessageId) args.push(`--reply-to-message-id=${replyToMessageId}`);
|
|
33004
33131
|
if (threadId) args.push(`--thread-id=${threadId}`);
|
|
33005
33132
|
if (attach) for (const path of attach) args.push(`--attach=${path}`);
|
|
33133
|
+
const inline = inlineAttachmentArgs("attach", attachInline, args);
|
|
33134
|
+
args.push(...inline);
|
|
33006
33135
|
return runOrDiagnose(args, { account });
|
|
33007
33136
|
});
|
|
33008
33137
|
registerRunTool(server, { service: "gmail", examples: '"archive", "mark-read", "labels"' });
|
|
@@ -33332,7 +33461,7 @@ function registerTasksTools(server) {
|
|
|
33332
33461
|
}
|
|
33333
33462
|
|
|
33334
33463
|
// src/server.ts
|
|
33335
|
-
var VERSION = true ? "2.
|
|
33464
|
+
var VERSION = true ? "2.25.0" : "0.0.0";
|
|
33336
33465
|
var BASE_TOOL_REGISTRARS = [
|
|
33337
33466
|
registerApiTools,
|
|
33338
33467
|
registerAuthTools,
|