gogcli-mcp-sheets 2.0.8 → 2.0.10
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 +323 -78
- package/manifest.json +6 -2
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/tools/sheets-extra.ts +111 -28
- package/tests/tools/sheets-extra.test.ts +121 -8
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "Extended Google Sheets for Claude via gogcli — auth + full Sheets support",
|
|
10
|
-
"version": "2.0.
|
|
10
|
+
"version": "2.0.10"
|
|
11
11
|
},
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"displayName": "gogcli (Sheets)",
|
|
16
16
|
"source": "./",
|
|
17
17
|
"description": "Extended Google Sheets for Claude via gogcli — auth + full Sheets support",
|
|
18
|
-
"version": "2.0.
|
|
18
|
+
"version": "2.0.10",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
package/dist/index.js
CHANGED
|
@@ -3105,6 +3105,9 @@ var require_utils = __commonJS({
|
|
|
3105
3105
|
"use strict";
|
|
3106
3106
|
var isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu);
|
|
3107
3107
|
var isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u);
|
|
3108
|
+
var isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu);
|
|
3109
|
+
var isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu);
|
|
3110
|
+
var isPathCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu);
|
|
3108
3111
|
function stringArrayToHexStripped(input) {
|
|
3109
3112
|
let acc = "";
|
|
3110
3113
|
let code = 0;
|
|
@@ -3297,27 +3300,77 @@ var require_utils = __commonJS({
|
|
|
3297
3300
|
}
|
|
3298
3301
|
return output.join("");
|
|
3299
3302
|
}
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3303
|
+
var HOST_DELIMS = { "@": "%40", "/": "%2F", "?": "%3F", "#": "%23", ":": "%3A" };
|
|
3304
|
+
var HOST_DELIM_RE = /[@/?#:]/g;
|
|
3305
|
+
var HOST_DELIM_NO_COLON_RE = /[@/?#]/g;
|
|
3306
|
+
function reescapeHostDelimiters(host, isIP) {
|
|
3307
|
+
const re = isIP ? HOST_DELIM_NO_COLON_RE : HOST_DELIM_RE;
|
|
3308
|
+
re.lastIndex = 0;
|
|
3309
|
+
return host.replace(re, (ch) => HOST_DELIMS[ch]);
|
|
3310
|
+
}
|
|
3311
|
+
function normalizePercentEncoding(input, decodeUnreserved = false) {
|
|
3312
|
+
if (input.indexOf("%") === -1) {
|
|
3313
|
+
return input;
|
|
3310
3314
|
}
|
|
3311
|
-
|
|
3312
|
-
|
|
3315
|
+
let output = "";
|
|
3316
|
+
for (let i = 0; i < input.length; i++) {
|
|
3317
|
+
if (input[i] === "%" && i + 2 < input.length) {
|
|
3318
|
+
const hex3 = input.slice(i + 1, i + 3);
|
|
3319
|
+
if (isHexPair(hex3)) {
|
|
3320
|
+
const normalizedHex = hex3.toUpperCase();
|
|
3321
|
+
const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
|
|
3322
|
+
if (decodeUnreserved && isUnreserved(decoded)) {
|
|
3323
|
+
output += decoded;
|
|
3324
|
+
} else {
|
|
3325
|
+
output += "%" + normalizedHex;
|
|
3326
|
+
}
|
|
3327
|
+
i += 2;
|
|
3328
|
+
continue;
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
output += input[i];
|
|
3313
3332
|
}
|
|
3314
|
-
|
|
3315
|
-
|
|
3333
|
+
return output;
|
|
3334
|
+
}
|
|
3335
|
+
function normalizePathEncoding(input) {
|
|
3336
|
+
let output = "";
|
|
3337
|
+
for (let i = 0; i < input.length; i++) {
|
|
3338
|
+
if (input[i] === "%" && i + 2 < input.length) {
|
|
3339
|
+
const hex3 = input.slice(i + 1, i + 3);
|
|
3340
|
+
if (isHexPair(hex3)) {
|
|
3341
|
+
const normalizedHex = hex3.toUpperCase();
|
|
3342
|
+
const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
|
|
3343
|
+
if (decoded !== "." && isUnreserved(decoded)) {
|
|
3344
|
+
output += decoded;
|
|
3345
|
+
} else {
|
|
3346
|
+
output += "%" + normalizedHex;
|
|
3347
|
+
}
|
|
3348
|
+
i += 2;
|
|
3349
|
+
continue;
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
if (isPathCharacter(input[i])) {
|
|
3353
|
+
output += input[i];
|
|
3354
|
+
} else {
|
|
3355
|
+
output += escape(input[i]);
|
|
3356
|
+
}
|
|
3316
3357
|
}
|
|
3317
|
-
|
|
3318
|
-
|
|
3358
|
+
return output;
|
|
3359
|
+
}
|
|
3360
|
+
function escapePreservingEscapes(input) {
|
|
3361
|
+
let output = "";
|
|
3362
|
+
for (let i = 0; i < input.length; i++) {
|
|
3363
|
+
if (input[i] === "%" && i + 2 < input.length) {
|
|
3364
|
+
const hex3 = input.slice(i + 1, i + 3);
|
|
3365
|
+
if (isHexPair(hex3)) {
|
|
3366
|
+
output += "%" + hex3.toUpperCase();
|
|
3367
|
+
i += 2;
|
|
3368
|
+
continue;
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
3371
|
+
output += escape(input[i]);
|
|
3319
3372
|
}
|
|
3320
|
-
return
|
|
3373
|
+
return output;
|
|
3321
3374
|
}
|
|
3322
3375
|
function recomposeAuthority(component) {
|
|
3323
3376
|
const uriTokens = [];
|
|
@@ -3332,7 +3385,7 @@ var require_utils = __commonJS({
|
|
|
3332
3385
|
if (ipV6res.isIPV6 === true) {
|
|
3333
3386
|
host = `[${ipV6res.escapedHost}]`;
|
|
3334
3387
|
} else {
|
|
3335
|
-
host =
|
|
3388
|
+
host = reescapeHostDelimiters(host, false);
|
|
3336
3389
|
}
|
|
3337
3390
|
}
|
|
3338
3391
|
uriTokens.push(host);
|
|
@@ -3346,7 +3399,10 @@ var require_utils = __commonJS({
|
|
|
3346
3399
|
module.exports = {
|
|
3347
3400
|
nonSimpleDomain,
|
|
3348
3401
|
recomposeAuthority,
|
|
3349
|
-
|
|
3402
|
+
reescapeHostDelimiters,
|
|
3403
|
+
normalizePercentEncoding,
|
|
3404
|
+
normalizePathEncoding,
|
|
3405
|
+
escapePreservingEscapes,
|
|
3350
3406
|
removeDotSegments,
|
|
3351
3407
|
isIPv4,
|
|
3352
3408
|
isUUID,
|
|
@@ -3570,12 +3626,12 @@ var require_schemes = __commonJS({
|
|
|
3570
3626
|
var require_fast_uri = __commonJS({
|
|
3571
3627
|
"../../node_modules/fast-uri/index.js"(exports, module) {
|
|
3572
3628
|
"use strict";
|
|
3573
|
-
var { normalizeIPv6, removeDotSegments, recomposeAuthority,
|
|
3629
|
+
var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
|
|
3574
3630
|
var { SCHEMES, getSchemeHandler } = require_schemes();
|
|
3575
3631
|
function normalize(uri, options) {
|
|
3576
3632
|
if (typeof uri === "string") {
|
|
3577
3633
|
uri = /** @type {T} */
|
|
3578
|
-
|
|
3634
|
+
normalizeString(uri, options);
|
|
3579
3635
|
} else if (typeof uri === "object") {
|
|
3580
3636
|
uri = /** @type {T} */
|
|
3581
3637
|
parse3(serialize(uri, options), options);
|
|
@@ -3642,19 +3698,9 @@ var require_fast_uri = __commonJS({
|
|
|
3642
3698
|
return target;
|
|
3643
3699
|
}
|
|
3644
3700
|
function equal(uriA, uriB, options) {
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
} else if (typeof uriA === "object") {
|
|
3649
|
-
uriA = serialize(normalizeComponentEncoding(uriA, true), { ...options, skipEscape: true });
|
|
3650
|
-
}
|
|
3651
|
-
if (typeof uriB === "string") {
|
|
3652
|
-
uriB = unescape(uriB);
|
|
3653
|
-
uriB = serialize(normalizeComponentEncoding(parse3(uriB, options), true), { ...options, skipEscape: true });
|
|
3654
|
-
} else if (typeof uriB === "object") {
|
|
3655
|
-
uriB = serialize(normalizeComponentEncoding(uriB, true), { ...options, skipEscape: true });
|
|
3656
|
-
}
|
|
3657
|
-
return uriA.toLowerCase() === uriB.toLowerCase();
|
|
3701
|
+
const normalizedA = normalizeComparableURI(uriA, options);
|
|
3702
|
+
const normalizedB = normalizeComparableURI(uriB, options);
|
|
3703
|
+
return normalizedA !== void 0 && normalizedB !== void 0 && normalizedA.toLowerCase() === normalizedB.toLowerCase();
|
|
3658
3704
|
}
|
|
3659
3705
|
function serialize(cmpts, opts) {
|
|
3660
3706
|
const component = {
|
|
@@ -3679,12 +3725,12 @@ var require_fast_uri = __commonJS({
|
|
|
3679
3725
|
if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(component, options);
|
|
3680
3726
|
if (component.path !== void 0) {
|
|
3681
3727
|
if (!options.skipEscape) {
|
|
3682
|
-
component.path =
|
|
3728
|
+
component.path = escapePreservingEscapes(component.path);
|
|
3683
3729
|
if (component.scheme !== void 0) {
|
|
3684
3730
|
component.path = component.path.split("%3A").join(":");
|
|
3685
3731
|
}
|
|
3686
3732
|
} else {
|
|
3687
|
-
component.path =
|
|
3733
|
+
component.path = normalizePercentEncoding(component.path);
|
|
3688
3734
|
}
|
|
3689
3735
|
}
|
|
3690
3736
|
if (options.reference !== "suffix" && component.scheme) {
|
|
@@ -3719,7 +3765,16 @@ var require_fast_uri = __commonJS({
|
|
|
3719
3765
|
return uriTokens.join("");
|
|
3720
3766
|
}
|
|
3721
3767
|
var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
|
|
3722
|
-
function
|
|
3768
|
+
function getParseError(parsed, matches) {
|
|
3769
|
+
if (matches[2] !== void 0 && parsed.path && parsed.path[0] !== "/") {
|
|
3770
|
+
return 'URI path must start with "/" when authority is present.';
|
|
3771
|
+
}
|
|
3772
|
+
if (typeof parsed.port === "number" && (parsed.port < 0 || parsed.port > 65535)) {
|
|
3773
|
+
return "URI port is malformed.";
|
|
3774
|
+
}
|
|
3775
|
+
return void 0;
|
|
3776
|
+
}
|
|
3777
|
+
function parseWithStatus(uri, opts) {
|
|
3723
3778
|
const options = Object.assign({}, opts);
|
|
3724
3779
|
const parsed = {
|
|
3725
3780
|
scheme: void 0,
|
|
@@ -3730,6 +3785,7 @@ var require_fast_uri = __commonJS({
|
|
|
3730
3785
|
query: void 0,
|
|
3731
3786
|
fragment: void 0
|
|
3732
3787
|
};
|
|
3788
|
+
let malformedAuthorityOrPort = false;
|
|
3733
3789
|
let isIP = false;
|
|
3734
3790
|
if (options.reference === "suffix") {
|
|
3735
3791
|
if (options.scheme) {
|
|
@@ -3750,6 +3806,11 @@ var require_fast_uri = __commonJS({
|
|
|
3750
3806
|
if (isNaN(parsed.port)) {
|
|
3751
3807
|
parsed.port = matches[5];
|
|
3752
3808
|
}
|
|
3809
|
+
const parseError = getParseError(parsed, matches);
|
|
3810
|
+
if (parseError !== void 0) {
|
|
3811
|
+
parsed.error = parsed.error || parseError;
|
|
3812
|
+
malformedAuthorityOrPort = true;
|
|
3813
|
+
}
|
|
3753
3814
|
if (parsed.host) {
|
|
3754
3815
|
const ipv4result = isIPv4(parsed.host);
|
|
3755
3816
|
if (ipv4result === false) {
|
|
@@ -3788,14 +3849,18 @@ var require_fast_uri = __commonJS({
|
|
|
3788
3849
|
parsed.scheme = unescape(parsed.scheme);
|
|
3789
3850
|
}
|
|
3790
3851
|
if (parsed.host !== void 0) {
|
|
3791
|
-
parsed.host = unescape(parsed.host);
|
|
3852
|
+
parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP);
|
|
3792
3853
|
}
|
|
3793
3854
|
}
|
|
3794
3855
|
if (parsed.path) {
|
|
3795
|
-
parsed.path =
|
|
3856
|
+
parsed.path = normalizePathEncoding(parsed.path);
|
|
3796
3857
|
}
|
|
3797
3858
|
if (parsed.fragment) {
|
|
3798
|
-
|
|
3859
|
+
try {
|
|
3860
|
+
parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
|
|
3861
|
+
} catch {
|
|
3862
|
+
parsed.error = parsed.error || "URI malformed";
|
|
3863
|
+
}
|
|
3799
3864
|
}
|
|
3800
3865
|
}
|
|
3801
3866
|
if (schemeHandler && schemeHandler.parse) {
|
|
@@ -3804,7 +3869,29 @@ var require_fast_uri = __commonJS({
|
|
|
3804
3869
|
} else {
|
|
3805
3870
|
parsed.error = parsed.error || "URI can not be parsed.";
|
|
3806
3871
|
}
|
|
3807
|
-
return parsed;
|
|
3872
|
+
return { parsed, malformedAuthorityOrPort };
|
|
3873
|
+
}
|
|
3874
|
+
function parse3(uri, opts) {
|
|
3875
|
+
return parseWithStatus(uri, opts).parsed;
|
|
3876
|
+
}
|
|
3877
|
+
function normalizeString(uri, opts) {
|
|
3878
|
+
return normalizeStringWithStatus(uri, opts).normalized;
|
|
3879
|
+
}
|
|
3880
|
+
function normalizeStringWithStatus(uri, opts) {
|
|
3881
|
+
const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts);
|
|
3882
|
+
return {
|
|
3883
|
+
normalized: malformedAuthorityOrPort ? uri : serialize(parsed, opts),
|
|
3884
|
+
malformedAuthorityOrPort
|
|
3885
|
+
};
|
|
3886
|
+
}
|
|
3887
|
+
function normalizeComparableURI(uri, opts) {
|
|
3888
|
+
if (typeof uri === "string") {
|
|
3889
|
+
const { normalized, malformedAuthorityOrPort } = normalizeStringWithStatus(uri, opts);
|
|
3890
|
+
return malformedAuthorityOrPort ? void 0 : normalized;
|
|
3891
|
+
}
|
|
3892
|
+
if (typeof uri === "object") {
|
|
3893
|
+
return serialize(uri, opts);
|
|
3894
|
+
}
|
|
3808
3895
|
}
|
|
3809
3896
|
var fastUri = {
|
|
3810
3897
|
SCHEMES,
|
|
@@ -9886,14 +9973,14 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
|
|
|
9886
9973
|
return `shape[${k}]._zod.run({ value: input[${k}], issues: [] }, ctx)`;
|
|
9887
9974
|
};
|
|
9888
9975
|
doc.write(`const input = payload.value;`);
|
|
9889
|
-
const
|
|
9976
|
+
const ids2 = /* @__PURE__ */ Object.create(null);
|
|
9890
9977
|
let counter = 0;
|
|
9891
9978
|
for (const key of normalized.keys) {
|
|
9892
|
-
|
|
9979
|
+
ids2[key] = `key_${counter++}`;
|
|
9893
9980
|
}
|
|
9894
9981
|
doc.write(`const newResult = {};`);
|
|
9895
9982
|
for (const key of normalized.keys) {
|
|
9896
|
-
const id =
|
|
9983
|
+
const id = ids2[key];
|
|
9897
9984
|
const k = esc(key);
|
|
9898
9985
|
const schema = shape[key];
|
|
9899
9986
|
const isOptionalIn = schema?._zod?.optin === "optional";
|
|
@@ -30870,6 +30957,32 @@ function envOrUndefined(key) {
|
|
|
30870
30957
|
if (!value || value.startsWith("${")) return void 0;
|
|
30871
30958
|
return value;
|
|
30872
30959
|
}
|
|
30960
|
+
function sanitizedEnv() {
|
|
30961
|
+
const result = {};
|
|
30962
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
30963
|
+
if (key === "GOG_ACCESS_TOKEN") continue;
|
|
30964
|
+
if (key === "GOOGLE_APPLICATION_CREDENTIALS") continue;
|
|
30965
|
+
if (/(_TOKEN|_SECRET|_API_KEY|_PRIVATE_KEY)$/.test(key)) continue;
|
|
30966
|
+
result[key] = value;
|
|
30967
|
+
}
|
|
30968
|
+
return result;
|
|
30969
|
+
}
|
|
30970
|
+
var TOKEN_PATTERNS = [
|
|
30971
|
+
/Bearer\s+[A-Za-z0-9._\-+/=]+/gi,
|
|
30972
|
+
/ya29\.[A-Za-z0-9._\-]+/g,
|
|
30973
|
+
// OAuth2 access tokens
|
|
30974
|
+
/1\/\/[A-Za-z0-9._\-]+/g,
|
|
30975
|
+
// OAuth2 refresh tokens
|
|
30976
|
+
/AIza[A-Za-z0-9_\-]{35}/g
|
|
30977
|
+
// Google API keys
|
|
30978
|
+
];
|
|
30979
|
+
function redactSecrets(text) {
|
|
30980
|
+
let redacted = text;
|
|
30981
|
+
for (const re of TOKEN_PATTERNS) {
|
|
30982
|
+
redacted = redacted.replace(re, "[REDACTED]");
|
|
30983
|
+
}
|
|
30984
|
+
return redacted;
|
|
30985
|
+
}
|
|
30873
30986
|
function augmentedPath() {
|
|
30874
30987
|
const home = process.env.HOME;
|
|
30875
30988
|
const candidates = [
|
|
@@ -30912,8 +31025,7 @@ async function run(args, options = {}) {
|
|
|
30912
31025
|
fullArgs.push(...args);
|
|
30913
31026
|
const effectiveTimeout = timeout ?? TIMEOUT_MS;
|
|
30914
31027
|
return new Promise((resolve, reject) => {
|
|
30915
|
-
const
|
|
30916
|
-
const childEnv = { ...cleanEnv, PATH: augmentedPath() };
|
|
31028
|
+
const childEnv = { ...sanitizedEnv(), PATH: augmentedPath() };
|
|
30917
31029
|
const child = spawner(envOrUndefined("GOG_PATH") ?? "gog", fullArgs, { env: childEnv });
|
|
30918
31030
|
const stdoutChunks = [];
|
|
30919
31031
|
const stderrChunks = [];
|
|
@@ -30942,7 +31054,7 @@ async function run(args, options = {}) {
|
|
|
30942
31054
|
resolve(stdout);
|
|
30943
31055
|
}
|
|
30944
31056
|
} else {
|
|
30945
|
-
reject(new Error(stderr || `gog exited with code ${code}`));
|
|
31057
|
+
reject(new Error(redactSecrets(stderr || `gog exited with code ${code}`)));
|
|
30946
31058
|
}
|
|
30947
31059
|
});
|
|
30948
31060
|
child.on("error", (err) => {
|
|
@@ -30964,6 +31076,55 @@ async function run(args, options = {}) {
|
|
|
30964
31076
|
var accountParam = external_exports.string().optional().describe(
|
|
30965
31077
|
"Google account email to use (overrides GOG_ACCOUNT env var)"
|
|
30966
31078
|
);
|
|
31079
|
+
var ids = {
|
|
31080
|
+
course: external_exports.string().describe("Course ID"),
|
|
31081
|
+
coursework: external_exports.string().describe("Coursework ID"),
|
|
31082
|
+
submission: external_exports.string().describe("Submission ID"),
|
|
31083
|
+
announcement: external_exports.string().describe("Announcement ID"),
|
|
31084
|
+
topic: external_exports.string().describe("Topic ID"),
|
|
31085
|
+
invitation: external_exports.string().describe("Invitation ID"),
|
|
31086
|
+
spreadsheet: external_exports.string().describe("Spreadsheet ID (from the URL)"),
|
|
31087
|
+
doc: external_exports.string().describe("Doc ID (from the URL)"),
|
|
31088
|
+
presentation: external_exports.string().describe("Presentation ID"),
|
|
31089
|
+
slide: external_exports.string().describe("Slide ID"),
|
|
31090
|
+
file: external_exports.string().describe("File ID"),
|
|
31091
|
+
message: external_exports.string().describe("Message ID"),
|
|
31092
|
+
thread: external_exports.string().describe("Thread ID"),
|
|
31093
|
+
draft: external_exports.string().describe("Draft ID"),
|
|
31094
|
+
label: external_exports.string().describe("Label ID or name"),
|
|
31095
|
+
attachment: external_exports.string().describe("Attachment ID"),
|
|
31096
|
+
comment: external_exports.string().describe("Comment ID"),
|
|
31097
|
+
meetingCode: external_exports.string().describe("Meeting code (e.g. abc-defg-hij)"),
|
|
31098
|
+
permission: external_exports.string().describe("Permission ID"),
|
|
31099
|
+
user: external_exports.string().describe("User ID"),
|
|
31100
|
+
// People API uses fully-qualified resource names ("people/c123") not bare IDs.
|
|
31101
|
+
person: external_exports.string().describe("Person resource name (people/...) or email")
|
|
31102
|
+
};
|
|
31103
|
+
var paginationParams = {
|
|
31104
|
+
max: external_exports.number().int().optional().describe("Max results"),
|
|
31105
|
+
page: external_exports.string().optional().describe("Page token"),
|
|
31106
|
+
all: external_exports.boolean().optional().describe("Fetch all pages")
|
|
31107
|
+
};
|
|
31108
|
+
function registerRunTool(server2, options) {
|
|
31109
|
+
const { service, examples, omitAccount = false, note } = options;
|
|
31110
|
+
const baseDescription = `Run any gog ${service} subcommand not covered by the other tools. Run \`gog ${service} --help\` for the full list of subcommands, or \`gog ${service} <subcommand> --help\` for flags on a specific subcommand.`;
|
|
31111
|
+
const description = note ? `${baseDescription} ${note}` : baseDescription;
|
|
31112
|
+
const inputSchema = {
|
|
31113
|
+
subcommand: external_exports.string().describe(`The gog ${service} subcommand to run, e.g. ${examples}`),
|
|
31114
|
+
args: external_exports.array(external_exports.string()).describe("Additional positional args and flags")
|
|
31115
|
+
};
|
|
31116
|
+
if (!omitAccount) {
|
|
31117
|
+
inputSchema.account = accountParam;
|
|
31118
|
+
}
|
|
31119
|
+
server2.registerTool(`gog_${service}_run`, {
|
|
31120
|
+
description,
|
|
31121
|
+
annotations: { destructiveHint: true },
|
|
31122
|
+
inputSchema
|
|
31123
|
+
}, async (rawArgs) => {
|
|
31124
|
+
const { subcommand, args, account } = rawArgs;
|
|
31125
|
+
return runOrDiagnose([service, subcommand, ...args], { account });
|
|
31126
|
+
});
|
|
31127
|
+
}
|
|
30967
31128
|
function toText(output) {
|
|
30968
31129
|
return { content: [{ type: "text", text: output }] };
|
|
30969
31130
|
}
|
|
@@ -31049,19 +31210,16 @@ function registerAuthTools(server2) {
|
|
|
31049
31210
|
return toError(err);
|
|
31050
31211
|
}
|
|
31051
31212
|
});
|
|
31052
|
-
server2
|
|
31053
|
-
|
|
31054
|
-
|
|
31055
|
-
|
|
31056
|
-
|
|
31057
|
-
args: external_exports.array(external_exports.string()).describe("Additional positional args and flags")
|
|
31058
|
-
}
|
|
31059
|
-
}, async ({ subcommand, args }) => {
|
|
31060
|
-
return runOrDiagnose(["auth", subcommand, ...args], {});
|
|
31213
|
+
registerRunTool(server2, {
|
|
31214
|
+
service: "auth",
|
|
31215
|
+
examples: '"remove", "alias", "tokens"',
|
|
31216
|
+
omitAccount: true,
|
|
31217
|
+
note: "For browser-based authorization, use gog_auth_add instead."
|
|
31061
31218
|
});
|
|
31062
31219
|
}
|
|
31063
31220
|
|
|
31064
31221
|
// ../gogcli-mcp/src/tools/sheets.ts
|
|
31222
|
+
var cellValueParam = external_exports.union([external_exports.string(), external_exports.number(), external_exports.boolean(), external_exports.null()]);
|
|
31065
31223
|
function registerSheetsTools(server2) {
|
|
31066
31224
|
server2.registerTool("gog_sheets_get", {
|
|
31067
31225
|
description: 'Read values from a Google Sheets range. Returns a JSON object with a "values" array of rows.',
|
|
@@ -31075,12 +31233,12 @@ function registerSheetsTools(server2) {
|
|
|
31075
31233
|
return runOrDiagnose(["sheets", "get", spreadsheetId, range], { account });
|
|
31076
31234
|
});
|
|
31077
31235
|
server2.registerTool("gog_sheets_update", {
|
|
31078
|
-
description:
|
|
31236
|
+
description: 'Write values to a Google Sheets range, overwriting existing content. Values may be strings, numbers, booleans, or null. Strings starting with "=" are interpreted as formulas (e.g. "=SUM(A1:A10)").',
|
|
31079
31237
|
annotations: { destructiveHint: true },
|
|
31080
31238
|
inputSchema: {
|
|
31081
31239
|
spreadsheetId: external_exports.string().describe("Spreadsheet ID (from the URL)"),
|
|
31082
31240
|
range: external_exports.string().describe("Top-left cell or range in A1 notation, e.g. Sheet1!A1"),
|
|
31083
|
-
values: external_exports.array(external_exports.array(
|
|
31241
|
+
values: external_exports.array(external_exports.array(cellValueParam)).describe('2D array of values (rows of columns). Cells may be string/number/boolean/null; strings starting with "=" are formulas.'),
|
|
31084
31242
|
account: accountParam
|
|
31085
31243
|
}
|
|
31086
31244
|
}, async ({ spreadsheetId, range, values, account }) => {
|
|
@@ -31090,12 +31248,12 @@ function registerSheetsTools(server2) {
|
|
|
31090
31248
|
);
|
|
31091
31249
|
});
|
|
31092
31250
|
server2.registerTool("gog_sheets_append", {
|
|
31093
|
-
description:
|
|
31251
|
+
description: 'Append rows to a Google Sheet after the last row with data in the given range. Values may be strings, numbers, booleans, or null. Strings starting with "=" are interpreted as formulas.',
|
|
31094
31252
|
annotations: { destructiveHint: true },
|
|
31095
31253
|
inputSchema: {
|
|
31096
31254
|
spreadsheetId: external_exports.string().describe("Spreadsheet ID (from the URL)"),
|
|
31097
31255
|
range: external_exports.string().describe("Range indicating which sheet/columns to append to, e.g. Sheet1!A:C"),
|
|
31098
|
-
values: external_exports.array(external_exports.array(
|
|
31256
|
+
values: external_exports.array(external_exports.array(cellValueParam)).describe('2D array of rows to append. Cells may be string/number/boolean/null; strings starting with "=" are formulas.'),
|
|
31099
31257
|
account: accountParam
|
|
31100
31258
|
}
|
|
31101
31259
|
}, async ({ spreadsheetId, range, values, account }) => {
|
|
@@ -31147,21 +31305,11 @@ function registerSheetsTools(server2) {
|
|
|
31147
31305
|
}, async ({ spreadsheetId, find, replace, account }) => {
|
|
31148
31306
|
return runOrDiagnose(["sheets", "find-replace", spreadsheetId, find, replace], { account });
|
|
31149
31307
|
});
|
|
31150
|
-
server2
|
|
31151
|
-
description: "Run any gog sheets subcommand not covered by the other tools. Run `gog sheets --help` for the full list of subcommands, or `gog sheets <subcommand> --help` for flags on a specific subcommand.",
|
|
31152
|
-
annotations: { destructiveHint: true },
|
|
31153
|
-
inputSchema: {
|
|
31154
|
-
subcommand: external_exports.string().describe('The gog sheets subcommand to run, e.g. "freeze", "add-tab", "rename-tab"'),
|
|
31155
|
-
args: external_exports.array(external_exports.string()).describe('Additional positional args and flags, e.g. ["<spreadsheetId>", "--rows=1"]'),
|
|
31156
|
-
account: accountParam
|
|
31157
|
-
}
|
|
31158
|
-
}, async ({ subcommand, args, account }) => {
|
|
31159
|
-
return runOrDiagnose(["sheets", subcommand, ...args], { account });
|
|
31160
|
-
});
|
|
31308
|
+
registerRunTool(server2, { service: "sheets", examples: '"freeze", "add-tab", "rename-tab"' });
|
|
31161
31309
|
}
|
|
31162
31310
|
|
|
31163
31311
|
// ../gogcli-mcp/src/server.ts
|
|
31164
|
-
var VERSION = true ? "2.0.
|
|
31312
|
+
var VERSION = true ? "2.0.10" : "0.0.0";
|
|
31165
31313
|
function createServer(options) {
|
|
31166
31314
|
return new McpServer({
|
|
31167
31315
|
name: options?.name ?? "gogcli",
|
|
@@ -31170,7 +31318,31 @@ function createServer(options) {
|
|
|
31170
31318
|
}
|
|
31171
31319
|
|
|
31172
31320
|
// src/tools/sheets-extra.ts
|
|
31321
|
+
function hexToRgb(hex3) {
|
|
31322
|
+
let h = hex3.trim().replace(/^#/, "");
|
|
31323
|
+
if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
|
|
31324
|
+
if (!/^[0-9a-fA-F]{6}$/.test(h)) return null;
|
|
31325
|
+
const n = parseInt(h, 16);
|
|
31326
|
+
return {
|
|
31327
|
+
red: (n >> 16 & 255) / 255,
|
|
31328
|
+
green: (n >> 8 & 255) / 255,
|
|
31329
|
+
blue: (n & 255) / 255
|
|
31330
|
+
};
|
|
31331
|
+
}
|
|
31173
31332
|
function registerExtraSheetsTools(server2) {
|
|
31333
|
+
server2.registerTool("gog_sheets_list_tabs", {
|
|
31334
|
+
description: "List tabs (sheets) in a spreadsheet with their titles, sheetIds, and indices. A friendlier view than gog_sheets_metadata when you only need the tab list \u2014 useful for restructuring a workbook over a long agent session without losing track of names.",
|
|
31335
|
+
annotations: { readOnlyHint: true },
|
|
31336
|
+
inputSchema: {
|
|
31337
|
+
spreadsheetId: external_exports.string().describe("Spreadsheet ID (from the URL)"),
|
|
31338
|
+
account: accountParam
|
|
31339
|
+
}
|
|
31340
|
+
}, async ({ spreadsheetId, account }) => {
|
|
31341
|
+
return runOrDiagnose(
|
|
31342
|
+
["sheets", "metadata", spreadsheetId, "--select=sheets.properties.sheetId,sheets.properties.title,sheets.properties.index,sheets.properties.gridProperties"],
|
|
31343
|
+
{ account }
|
|
31344
|
+
);
|
|
31345
|
+
});
|
|
31174
31346
|
server2.registerTool("gog_sheets_add_tab", {
|
|
31175
31347
|
description: "Add a new sheet tab to a spreadsheet.",
|
|
31176
31348
|
inputSchema: {
|
|
@@ -31293,19 +31465,92 @@ function registerExtraSheetsTools(server2) {
|
|
|
31293
31465
|
return runOrDiagnose(["sheets", "unmerge", spreadsheetId, range], { account });
|
|
31294
31466
|
});
|
|
31295
31467
|
server2.registerTool("gog_sheets_format", {
|
|
31296
|
-
description: "Apply cell formatting (bold,
|
|
31468
|
+
description: "Apply cell formatting to a range. The named flags (bold, italic, backgroundColor, etc.) compose into a Sheets API CellFormat \u2014 use them for the 90% case. For full API control, pass formatJson + formatFields (Sheets CellFormat + field mask) directly.",
|
|
31297
31469
|
annotations: { destructiveHint: true },
|
|
31298
31470
|
inputSchema: {
|
|
31299
31471
|
spreadsheetId: external_exports.string().describe("Spreadsheet ID"),
|
|
31300
31472
|
range: external_exports.string().describe("Range to format (e.g. Sheet1!A1:C3)"),
|
|
31301
|
-
|
|
31302
|
-
|
|
31473
|
+
bold: external_exports.boolean().optional().describe("Set bold"),
|
|
31474
|
+
italic: external_exports.boolean().optional().describe("Set italic"),
|
|
31475
|
+
underline: external_exports.boolean().optional().describe("Set underline"),
|
|
31476
|
+
strikethrough: external_exports.boolean().optional().describe("Set strikethrough"),
|
|
31477
|
+
fontSize: external_exports.number().optional().describe("Font size in points"),
|
|
31478
|
+
fontFamily: external_exports.string().optional().describe("Font family (e.g. Arial)"),
|
|
31479
|
+
textColor: external_exports.string().optional().describe("Text color as #RRGGBB or #RGB"),
|
|
31480
|
+
backgroundColor: external_exports.string().optional().describe("Cell background color as #RRGGBB or #RGB"),
|
|
31481
|
+
horizontalAlignment: external_exports.enum(["LEFT", "CENTER", "RIGHT"]).optional().describe("Horizontal alignment"),
|
|
31482
|
+
verticalAlignment: external_exports.enum(["TOP", "MIDDLE", "BOTTOM"]).optional().describe("Vertical alignment"),
|
|
31483
|
+
wrapStrategy: external_exports.enum(["OVERFLOW_CELL", "LEGACY_WRAP", "CLIP", "WRAP"]).optional().describe("Text wrap strategy"),
|
|
31484
|
+
formatJson: external_exports.string().optional().describe("Escape hatch: raw CellFormat JSON. When provided, named flags above are ignored and formatJson is sent as-is."),
|
|
31485
|
+
formatFields: external_exports.string().optional().describe("Comma-separated field mask (e.g. textFormat.bold,backgroundColor). Required when using formatJson; auto-computed when using named flags."),
|
|
31303
31486
|
account: accountParam
|
|
31304
31487
|
}
|
|
31305
|
-
}, async (
|
|
31306
|
-
const
|
|
31488
|
+
}, async (rawArgs) => {
|
|
31489
|
+
const a = rawArgs;
|
|
31490
|
+
let formatJson = a.formatJson;
|
|
31491
|
+
let formatFields = a.formatFields;
|
|
31492
|
+
if (!formatJson) {
|
|
31493
|
+
const cellFormat = {};
|
|
31494
|
+
const textFormat = {};
|
|
31495
|
+
const fields = [];
|
|
31496
|
+
if (a.bold !== void 0) {
|
|
31497
|
+
textFormat.bold = a.bold;
|
|
31498
|
+
fields.push("textFormat.bold");
|
|
31499
|
+
}
|
|
31500
|
+
if (a.italic !== void 0) {
|
|
31501
|
+
textFormat.italic = a.italic;
|
|
31502
|
+
fields.push("textFormat.italic");
|
|
31503
|
+
}
|
|
31504
|
+
if (a.underline !== void 0) {
|
|
31505
|
+
textFormat.underline = a.underline;
|
|
31506
|
+
fields.push("textFormat.underline");
|
|
31507
|
+
}
|
|
31508
|
+
if (a.strikethrough !== void 0) {
|
|
31509
|
+
textFormat.strikethrough = a.strikethrough;
|
|
31510
|
+
fields.push("textFormat.strikethrough");
|
|
31511
|
+
}
|
|
31512
|
+
if (a.fontSize !== void 0) {
|
|
31513
|
+
textFormat.fontSize = a.fontSize;
|
|
31514
|
+
fields.push("textFormat.fontSize");
|
|
31515
|
+
}
|
|
31516
|
+
if (a.fontFamily !== void 0) {
|
|
31517
|
+
textFormat.fontFamily = a.fontFamily;
|
|
31518
|
+
fields.push("textFormat.fontFamily");
|
|
31519
|
+
}
|
|
31520
|
+
if (a.textColor !== void 0) {
|
|
31521
|
+
const rgb = hexToRgb(a.textColor);
|
|
31522
|
+
if (!rgb) throw new Error(`Invalid textColor: ${a.textColor} (expected #RRGGBB or #RGB)`);
|
|
31523
|
+
textFormat.foregroundColor = rgb;
|
|
31524
|
+
fields.push("textFormat.foregroundColor");
|
|
31525
|
+
}
|
|
31526
|
+
if (Object.keys(textFormat).length > 0) cellFormat.textFormat = textFormat;
|
|
31527
|
+
if (a.backgroundColor !== void 0) {
|
|
31528
|
+
const rgb = hexToRgb(a.backgroundColor);
|
|
31529
|
+
if (!rgb) throw new Error(`Invalid backgroundColor: ${a.backgroundColor} (expected #RRGGBB or #RGB)`);
|
|
31530
|
+
cellFormat.backgroundColor = rgb;
|
|
31531
|
+
fields.push("backgroundColor");
|
|
31532
|
+
}
|
|
31533
|
+
if (a.horizontalAlignment !== void 0) {
|
|
31534
|
+
cellFormat.horizontalAlignment = a.horizontalAlignment;
|
|
31535
|
+
fields.push("horizontalAlignment");
|
|
31536
|
+
}
|
|
31537
|
+
if (a.verticalAlignment !== void 0) {
|
|
31538
|
+
cellFormat.verticalAlignment = a.verticalAlignment;
|
|
31539
|
+
fields.push("verticalAlignment");
|
|
31540
|
+
}
|
|
31541
|
+
if (a.wrapStrategy !== void 0) {
|
|
31542
|
+
cellFormat.wrapStrategy = a.wrapStrategy;
|
|
31543
|
+
fields.push("wrapStrategy");
|
|
31544
|
+
}
|
|
31545
|
+
if (Object.keys(cellFormat).length === 0) {
|
|
31546
|
+
throw new Error("gog_sheets_format requires at least one named flag or a formatJson value");
|
|
31547
|
+
}
|
|
31548
|
+
formatJson = JSON.stringify(cellFormat);
|
|
31549
|
+
if (!formatFields) formatFields = fields.join(",");
|
|
31550
|
+
}
|
|
31551
|
+
const args = ["sheets", "format", a.spreadsheetId, a.range, `--format-json=${formatJson}`];
|
|
31307
31552
|
if (formatFields) args.push(`--format-fields=${formatFields}`);
|
|
31308
|
-
return runOrDiagnose(args, { account });
|
|
31553
|
+
return runOrDiagnose(args, { account: a.account });
|
|
31309
31554
|
});
|
|
31310
31555
|
server2.registerTool("gog_sheets_number_format", {
|
|
31311
31556
|
description: "Set number format on a range (currency, percentage, date, etc.).",
|
package/manifest.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"manifest_version": "0.3",
|
|
4
4
|
"name": "gogcli-mcp-sheets",
|
|
5
5
|
"display_name": "gogcli (Sheets)",
|
|
6
|
-
"version": "2.0.
|
|
6
|
+
"version": "2.0.10",
|
|
7
7
|
"description": "Extended Google Sheets for Claude via gogcli — auth + full Sheets support",
|
|
8
8
|
"author": {
|
|
9
9
|
"name": "Chris Hall",
|
|
@@ -146,7 +146,7 @@
|
|
|
146
146
|
},
|
|
147
147
|
{
|
|
148
148
|
"name": "gog_sheets_format",
|
|
149
|
-
"description": "Apply cell formatting (bold,
|
|
149
|
+
"description": "Apply cell formatting to a range. Named flags (bold, italic, backgroundColor, etc.) compose to a Sheets CellFormat; pass formatJson for full control."
|
|
150
150
|
},
|
|
151
151
|
{
|
|
152
152
|
"name": "gog_sheets_number_format",
|
|
@@ -195,6 +195,10 @@
|
|
|
195
195
|
{
|
|
196
196
|
"name": "gog_sheets_named_ranges_delete",
|
|
197
197
|
"description": "Delete a named range."
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"name": "gog_sheets_list_tabs",
|
|
201
|
+
"description": "List tabs in a spreadsheet (sheetId, title, index, gridProperties)"
|
|
198
202
|
}
|
|
199
203
|
],
|
|
200
204
|
"compatibility": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gogcli-mcp-sheets",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.10",
|
|
4
4
|
"mcpName": "io.github.chrischall/gogcli-mcp-sheets",
|
|
5
5
|
"description": "Extended Google Sheets MCP server via gogcli — all base tools plus full Sheets support",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
package/server.json
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"source": "github",
|
|
8
8
|
"subfolder": "packages/gogcli-mcp-sheets"
|
|
9
9
|
},
|
|
10
|
-
"version": "2.0.
|
|
10
|
+
"version": "2.0.10",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "gogcli-mcp-sheets",
|
|
15
|
-
"version": "2.0.
|
|
15
|
+
"version": "2.0.10",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
},
|
|
@@ -2,8 +2,39 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { accountParam, runOrDiagnose } from '../../../gogcli-mcp/src/lib.js';
|
|
4
4
|
|
|
5
|
+
// Convert a CSS-style hex color ("#FFF5D9", "#FD9", "FFF5D9") to the
|
|
6
|
+
// {red, green, blue} 0-1 float triple that Sheets API CellFormat expects.
|
|
7
|
+
// Returns null on unparseable input — caller decides whether to fall back
|
|
8
|
+
// or error.
|
|
9
|
+
export function hexToRgb(hex: string): { red: number; green: number; blue: number } | null {
|
|
10
|
+
let h = hex.trim().replace(/^#/, '');
|
|
11
|
+
if (h.length === 3) h = h[0]! + h[0]! + h[1]! + h[1]! + h[2]! + h[2]!;
|
|
12
|
+
if (!/^[0-9a-fA-F]{6}$/.test(h)) return null;
|
|
13
|
+
const n = parseInt(h, 16);
|
|
14
|
+
return {
|
|
15
|
+
red: ((n >> 16) & 0xff) / 255,
|
|
16
|
+
green: ((n >> 8) & 0xff) / 255,
|
|
17
|
+
blue: (n & 0xff) / 255,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
5
21
|
export function registerExtraSheetsTools(server: McpServer): void {
|
|
6
|
-
|
|
22
|
+
|
|
23
|
+
server.registerTool('gog_sheets_list_tabs', {
|
|
24
|
+
description: 'List tabs (sheets) in a spreadsheet with their titles, sheetIds, and indices. A friendlier view than gog_sheets_metadata when you only need the tab list — useful for restructuring a workbook over a long agent session without losing track of names.',
|
|
25
|
+
annotations: { readOnlyHint: true },
|
|
26
|
+
inputSchema: {
|
|
27
|
+
spreadsheetId: z.string().describe('Spreadsheet ID (from the URL)'),
|
|
28
|
+
account: accountParam,
|
|
29
|
+
},
|
|
30
|
+
}, async ({ spreadsheetId, account }) => {
|
|
31
|
+
// jq projection keeps the response compact: sheetId, title, index, gridProperties only.
|
|
32
|
+
return runOrDiagnose(
|
|
33
|
+
['sheets', 'metadata', spreadsheetId, '--select=sheets.properties.sheetId,sheets.properties.title,sheets.properties.index,sheets.properties.gridProperties'],
|
|
34
|
+
{ account },
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
7
38
|
server.registerTool('gog_sheets_add_tab', {
|
|
8
39
|
description: 'Add a new sheet tab to a spreadsheet.',
|
|
9
40
|
inputSchema: {
|
|
@@ -15,7 +46,6 @@ export function registerExtraSheetsTools(server: McpServer): void {
|
|
|
15
46
|
return runOrDiagnose(['sheets', 'add-tab', spreadsheetId, tabName], { account });
|
|
16
47
|
});
|
|
17
48
|
|
|
18
|
-
// 2. Delete tab
|
|
19
49
|
server.registerTool('gog_sheets_delete_tab', {
|
|
20
50
|
description: 'Delete a sheet tab from a spreadsheet.',
|
|
21
51
|
annotations: { destructiveHint: true },
|
|
@@ -28,7 +58,6 @@ export function registerExtraSheetsTools(server: McpServer): void {
|
|
|
28
58
|
return runOrDiagnose(['sheets', 'delete-tab', spreadsheetId, tabName], { account });
|
|
29
59
|
});
|
|
30
60
|
|
|
31
|
-
// 3. Rename tab
|
|
32
61
|
server.registerTool('gog_sheets_rename_tab', {
|
|
33
62
|
description: 'Rename a sheet tab.',
|
|
34
63
|
annotations: { destructiveHint: true },
|
|
@@ -42,7 +71,6 @@ export function registerExtraSheetsTools(server: McpServer): void {
|
|
|
42
71
|
return runOrDiagnose(['sheets', 'rename-tab', spreadsheetId, oldName, newName], { account });
|
|
43
72
|
});
|
|
44
73
|
|
|
45
|
-
// 4. Copy spreadsheet
|
|
46
74
|
server.registerTool('gog_sheets_copy', {
|
|
47
75
|
description: 'Copy a spreadsheet to a new spreadsheet with the given title.',
|
|
48
76
|
inputSchema: {
|
|
@@ -57,7 +85,6 @@ export function registerExtraSheetsTools(server: McpServer): void {
|
|
|
57
85
|
return runOrDiagnose(args, { account });
|
|
58
86
|
});
|
|
59
87
|
|
|
60
|
-
// 5. Export spreadsheet
|
|
61
88
|
server.registerTool('gog_sheets_export', {
|
|
62
89
|
description: 'Export a spreadsheet as CSV, TSV, or PDF.',
|
|
63
90
|
annotations: { readOnlyHint: true },
|
|
@@ -74,7 +101,6 @@ export function registerExtraSheetsTools(server: McpServer): void {
|
|
|
74
101
|
return runOrDiagnose(args, { account });
|
|
75
102
|
});
|
|
76
103
|
|
|
77
|
-
// 6. Freeze rows/columns
|
|
78
104
|
server.registerTool('gog_sheets_freeze', {
|
|
79
105
|
description: 'Freeze rows and/or columns in a sheet.',
|
|
80
106
|
annotations: { destructiveHint: true },
|
|
@@ -93,7 +119,6 @@ export function registerExtraSheetsTools(server: McpServer): void {
|
|
|
93
119
|
return runOrDiagnose(args, { account });
|
|
94
120
|
});
|
|
95
121
|
|
|
96
|
-
// 7. Insert rows/columns
|
|
97
122
|
server.registerTool('gog_sheets_insert', {
|
|
98
123
|
description: 'Insert rows or columns into a sheet.',
|
|
99
124
|
annotations: { destructiveHint: true },
|
|
@@ -113,7 +138,6 @@ export function registerExtraSheetsTools(server: McpServer): void {
|
|
|
113
138
|
return runOrDiagnose(args, { account });
|
|
114
139
|
});
|
|
115
140
|
|
|
116
|
-
// 8. Merge cells
|
|
117
141
|
server.registerTool('gog_sheets_merge', {
|
|
118
142
|
description: 'Merge cells in a range.',
|
|
119
143
|
annotations: { destructiveHint: true },
|
|
@@ -129,7 +153,6 @@ export function registerExtraSheetsTools(server: McpServer): void {
|
|
|
129
153
|
return runOrDiagnose(args, { account });
|
|
130
154
|
});
|
|
131
155
|
|
|
132
|
-
// 9. Unmerge cells
|
|
133
156
|
server.registerTool('gog_sheets_unmerge', {
|
|
134
157
|
description: 'Unmerge previously merged cells in a range.',
|
|
135
158
|
annotations: { destructiveHint: true },
|
|
@@ -142,24 +165,95 @@ export function registerExtraSheetsTools(server: McpServer): void {
|
|
|
142
165
|
return runOrDiagnose(['sheets', 'unmerge', spreadsheetId, range], { account });
|
|
143
166
|
});
|
|
144
167
|
|
|
145
|
-
// 10. Format cells
|
|
146
168
|
server.registerTool('gog_sheets_format', {
|
|
147
|
-
description: 'Apply cell formatting (bold,
|
|
169
|
+
description: 'Apply cell formatting to a range. The named flags (bold, italic, backgroundColor, etc.) compose into a Sheets API CellFormat — use them for the 90% case. For full API control, pass formatJson + formatFields (Sheets CellFormat + field mask) directly.',
|
|
148
170
|
annotations: { destructiveHint: true },
|
|
149
171
|
inputSchema: {
|
|
150
172
|
spreadsheetId: z.string().describe('Spreadsheet ID'),
|
|
151
173
|
range: z.string().describe('Range to format (e.g. Sheet1!A1:C3)'),
|
|
152
|
-
|
|
153
|
-
|
|
174
|
+
bold: z.boolean().optional().describe('Set bold'),
|
|
175
|
+
italic: z.boolean().optional().describe('Set italic'),
|
|
176
|
+
underline: z.boolean().optional().describe('Set underline'),
|
|
177
|
+
strikethrough: z.boolean().optional().describe('Set strikethrough'),
|
|
178
|
+
fontSize: z.number().optional().describe('Font size in points'),
|
|
179
|
+
fontFamily: z.string().optional().describe('Font family (e.g. Arial)'),
|
|
180
|
+
textColor: z.string().optional().describe('Text color as #RRGGBB or #RGB'),
|
|
181
|
+
backgroundColor: z.string().optional().describe('Cell background color as #RRGGBB or #RGB'),
|
|
182
|
+
horizontalAlignment: z.enum(['LEFT', 'CENTER', 'RIGHT']).optional().describe('Horizontal alignment'),
|
|
183
|
+
verticalAlignment: z.enum(['TOP', 'MIDDLE', 'BOTTOM']).optional().describe('Vertical alignment'),
|
|
184
|
+
wrapStrategy: z.enum(['OVERFLOW_CELL', 'LEGACY_WRAP', 'CLIP', 'WRAP']).optional().describe('Text wrap strategy'),
|
|
185
|
+
formatJson: z.string().optional().describe('Escape hatch: raw CellFormat JSON. When provided, named flags above are ignored and formatJson is sent as-is.'),
|
|
186
|
+
formatFields: z.string().optional().describe('Comma-separated field mask (e.g. textFormat.bold,backgroundColor). Required when using formatJson; auto-computed when using named flags.'),
|
|
154
187
|
account: accountParam,
|
|
155
188
|
},
|
|
156
|
-
}, async (
|
|
157
|
-
const
|
|
189
|
+
}, async (rawArgs) => {
|
|
190
|
+
const a = rawArgs as {
|
|
191
|
+
spreadsheetId: string;
|
|
192
|
+
range: string;
|
|
193
|
+
bold?: boolean;
|
|
194
|
+
italic?: boolean;
|
|
195
|
+
underline?: boolean;
|
|
196
|
+
strikethrough?: boolean;
|
|
197
|
+
fontSize?: number;
|
|
198
|
+
fontFamily?: string;
|
|
199
|
+
textColor?: string;
|
|
200
|
+
backgroundColor?: string;
|
|
201
|
+
horizontalAlignment?: string;
|
|
202
|
+
verticalAlignment?: string;
|
|
203
|
+
wrapStrategy?: string;
|
|
204
|
+
formatJson?: string;
|
|
205
|
+
formatFields?: string;
|
|
206
|
+
account?: string;
|
|
207
|
+
};
|
|
208
|
+
let formatJson = a.formatJson;
|
|
209
|
+
let formatFields = a.formatFields;
|
|
210
|
+
// If the caller didn't pass raw JSON, compose it from the named flags.
|
|
211
|
+
if (!formatJson) {
|
|
212
|
+
const cellFormat: Record<string, unknown> = {};
|
|
213
|
+
const textFormat: Record<string, unknown> = {};
|
|
214
|
+
const fields: string[] = [];
|
|
215
|
+
if (a.bold !== undefined) { textFormat.bold = a.bold; fields.push('textFormat.bold'); }
|
|
216
|
+
if (a.italic !== undefined) { textFormat.italic = a.italic; fields.push('textFormat.italic'); }
|
|
217
|
+
if (a.underline !== undefined) { textFormat.underline = a.underline; fields.push('textFormat.underline'); }
|
|
218
|
+
if (a.strikethrough !== undefined) { textFormat.strikethrough = a.strikethrough; fields.push('textFormat.strikethrough'); }
|
|
219
|
+
if (a.fontSize !== undefined) { textFormat.fontSize = a.fontSize; fields.push('textFormat.fontSize'); }
|
|
220
|
+
if (a.fontFamily !== undefined) { textFormat.fontFamily = a.fontFamily; fields.push('textFormat.fontFamily'); }
|
|
221
|
+
if (a.textColor !== undefined) {
|
|
222
|
+
const rgb = hexToRgb(a.textColor);
|
|
223
|
+
if (!rgb) throw new Error(`Invalid textColor: ${a.textColor} (expected #RRGGBB or #RGB)`);
|
|
224
|
+
textFormat.foregroundColor = rgb;
|
|
225
|
+
fields.push('textFormat.foregroundColor');
|
|
226
|
+
}
|
|
227
|
+
if (Object.keys(textFormat).length > 0) cellFormat.textFormat = textFormat;
|
|
228
|
+
if (a.backgroundColor !== undefined) {
|
|
229
|
+
const rgb = hexToRgb(a.backgroundColor);
|
|
230
|
+
if (!rgb) throw new Error(`Invalid backgroundColor: ${a.backgroundColor} (expected #RRGGBB or #RGB)`);
|
|
231
|
+
cellFormat.backgroundColor = rgb;
|
|
232
|
+
fields.push('backgroundColor');
|
|
233
|
+
}
|
|
234
|
+
if (a.horizontalAlignment !== undefined) {
|
|
235
|
+
cellFormat.horizontalAlignment = a.horizontalAlignment;
|
|
236
|
+
fields.push('horizontalAlignment');
|
|
237
|
+
}
|
|
238
|
+
if (a.verticalAlignment !== undefined) {
|
|
239
|
+
cellFormat.verticalAlignment = a.verticalAlignment;
|
|
240
|
+
fields.push('verticalAlignment');
|
|
241
|
+
}
|
|
242
|
+
if (a.wrapStrategy !== undefined) {
|
|
243
|
+
cellFormat.wrapStrategy = a.wrapStrategy;
|
|
244
|
+
fields.push('wrapStrategy');
|
|
245
|
+
}
|
|
246
|
+
if (Object.keys(cellFormat).length === 0) {
|
|
247
|
+
throw new Error('gog_sheets_format requires at least one named flag or a formatJson value');
|
|
248
|
+
}
|
|
249
|
+
formatJson = JSON.stringify(cellFormat);
|
|
250
|
+
if (!formatFields) formatFields = fields.join(',');
|
|
251
|
+
}
|
|
252
|
+
const args = ['sheets', 'format', a.spreadsheetId, a.range, `--format-json=${formatJson}`];
|
|
158
253
|
if (formatFields) args.push(`--format-fields=${formatFields}`);
|
|
159
|
-
return runOrDiagnose(args, { account });
|
|
254
|
+
return runOrDiagnose(args, { account: a.account });
|
|
160
255
|
});
|
|
161
256
|
|
|
162
|
-
// 11. Number format
|
|
163
257
|
server.registerTool('gog_sheets_number_format', {
|
|
164
258
|
description: 'Set number format on a range (currency, percentage, date, etc.).',
|
|
165
259
|
annotations: { destructiveHint: true },
|
|
@@ -177,7 +271,6 @@ export function registerExtraSheetsTools(server: McpServer): void {
|
|
|
177
271
|
return runOrDiagnose(args, { account });
|
|
178
272
|
});
|
|
179
273
|
|
|
180
|
-
// 12. Read format
|
|
181
274
|
server.registerTool('gog_sheets_read_format', {
|
|
182
275
|
description: 'Read cell formatting for a range.',
|
|
183
276
|
annotations: { readOnlyHint: true },
|
|
@@ -193,7 +286,6 @@ export function registerExtraSheetsTools(server: McpServer): void {
|
|
|
193
286
|
return runOrDiagnose(args, { account });
|
|
194
287
|
});
|
|
195
288
|
|
|
196
|
-
// 13. Resize columns
|
|
197
289
|
server.registerTool('gog_sheets_resize_columns', {
|
|
198
290
|
description: 'Resize column widths. Use --auto for auto-fit or --width for a specific pixel width.',
|
|
199
291
|
annotations: { destructiveHint: true },
|
|
@@ -211,7 +303,6 @@ export function registerExtraSheetsTools(server: McpServer): void {
|
|
|
211
303
|
return runOrDiagnose(args, { account });
|
|
212
304
|
});
|
|
213
305
|
|
|
214
|
-
// 14. Resize rows
|
|
215
306
|
server.registerTool('gog_sheets_resize_rows', {
|
|
216
307
|
description: 'Resize row heights. Use --auto for auto-fit or --height for a specific pixel height.',
|
|
217
308
|
annotations: { destructiveHint: true },
|
|
@@ -229,7 +320,6 @@ export function registerExtraSheetsTools(server: McpServer): void {
|
|
|
229
320
|
return runOrDiagnose(args, { account });
|
|
230
321
|
});
|
|
231
322
|
|
|
232
|
-
// 15. Notes (read)
|
|
233
323
|
server.registerTool('gog_sheets_notes', {
|
|
234
324
|
description: 'Read cell notes in a range.',
|
|
235
325
|
annotations: { readOnlyHint: true },
|
|
@@ -242,7 +332,6 @@ export function registerExtraSheetsTools(server: McpServer): void {
|
|
|
242
332
|
return runOrDiagnose(['sheets', 'notes', spreadsheetId, range], { account });
|
|
243
333
|
});
|
|
244
334
|
|
|
245
|
-
// 16. Update note
|
|
246
335
|
server.registerTool('gog_sheets_update_note', {
|
|
247
336
|
description: 'Add, update, or clear a cell note. Pass an empty string to clear the note.',
|
|
248
337
|
annotations: { destructiveHint: true },
|
|
@@ -256,7 +345,6 @@ export function registerExtraSheetsTools(server: McpServer): void {
|
|
|
256
345
|
return runOrDiagnose(['sheets', 'update-note', spreadsheetId, range, `--note=${note}`], { account });
|
|
257
346
|
});
|
|
258
347
|
|
|
259
|
-
// 17. Links
|
|
260
348
|
server.registerTool('gog_sheets_links', {
|
|
261
349
|
description: 'List hyperlinks in a range.',
|
|
262
350
|
annotations: { readOnlyHint: true },
|
|
@@ -269,7 +357,6 @@ export function registerExtraSheetsTools(server: McpServer): void {
|
|
|
269
357
|
return runOrDiagnose(['sheets', 'links', spreadsheetId, range], { account });
|
|
270
358
|
});
|
|
271
359
|
|
|
272
|
-
// 18. Named ranges list
|
|
273
360
|
server.registerTool('gog_sheets_named_ranges_list', {
|
|
274
361
|
description: 'List all named ranges in a spreadsheet.',
|
|
275
362
|
annotations: { readOnlyHint: true },
|
|
@@ -281,7 +368,6 @@ export function registerExtraSheetsTools(server: McpServer): void {
|
|
|
281
368
|
return runOrDiagnose(['sheets', 'named-ranges', 'list', spreadsheetId], { account });
|
|
282
369
|
});
|
|
283
370
|
|
|
284
|
-
// 19. Named ranges get
|
|
285
371
|
server.registerTool('gog_sheets_named_ranges_get', {
|
|
286
372
|
description: 'Get a named range by name or ID.',
|
|
287
373
|
annotations: { readOnlyHint: true },
|
|
@@ -294,7 +380,6 @@ export function registerExtraSheetsTools(server: McpServer): void {
|
|
|
294
380
|
return runOrDiagnose(['sheets', 'named-ranges', 'get', spreadsheetId, nameOrId], { account });
|
|
295
381
|
});
|
|
296
382
|
|
|
297
|
-
// 20. Named ranges add
|
|
298
383
|
server.registerTool('gog_sheets_named_ranges_add', {
|
|
299
384
|
description: 'Create a new named range.',
|
|
300
385
|
inputSchema: {
|
|
@@ -307,7 +392,6 @@ export function registerExtraSheetsTools(server: McpServer): void {
|
|
|
307
392
|
return runOrDiagnose(['sheets', 'named-ranges', 'add', spreadsheetId, name, range], { account });
|
|
308
393
|
});
|
|
309
394
|
|
|
310
|
-
// 21. Named ranges update
|
|
311
395
|
server.registerTool('gog_sheets_named_ranges_update', {
|
|
312
396
|
description: 'Update a named range (change its name, range, or both).',
|
|
313
397
|
annotations: { destructiveHint: true },
|
|
@@ -325,7 +409,6 @@ export function registerExtraSheetsTools(server: McpServer): void {
|
|
|
325
409
|
return runOrDiagnose(args, { account });
|
|
326
410
|
});
|
|
327
411
|
|
|
328
|
-
// 22. Named ranges delete
|
|
329
412
|
server.registerTool('gog_sheets_named_ranges_delete', {
|
|
330
413
|
description: 'Delete a named range.',
|
|
331
414
|
annotations: { destructiveHint: true },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
import { registerExtraSheetsTools } from '../../src/tools/sheets-extra.js';
|
|
3
3
|
import * as lib from '../../../gogcli-mcp/src/lib.js';
|
|
4
|
-
import {
|
|
4
|
+
import { setupHandlers as setupHandlersBase, toText } from '../../../gogcli-mcp/tests/helpers/test-harness.js';
|
|
5
5
|
|
|
6
6
|
vi.mock('../../../gogcli-mcp/src/lib.js', async (importOriginal) => {
|
|
7
7
|
const actual = await importOriginal<typeof lib>();
|
|
@@ -11,7 +11,7 @@ vi.mock('../../../gogcli-mcp/src/lib.js', async (importOriginal) => {
|
|
|
11
11
|
};
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
const setupHandlers = () =>
|
|
14
|
+
const setupHandlers = () => setupHandlersBase(registerExtraSheetsTools);
|
|
15
15
|
|
|
16
16
|
beforeEach(() => vi.clearAllMocks());
|
|
17
17
|
|
|
@@ -170,19 +170,132 @@ describe('gog_sheets_unmerge', () => {
|
|
|
170
170
|
|
|
171
171
|
// 10. format
|
|
172
172
|
describe('gog_sheets_format', () => {
|
|
173
|
-
it('
|
|
173
|
+
it('passes raw formatJson + formatFields through unchanged (escape hatch)', async () => {
|
|
174
174
|
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
175
175
|
const handlers = setupHandlers();
|
|
176
176
|
const fj = '{"textFormat":{"bold":true}}';
|
|
177
|
-
await handlers.get('gog_sheets_format')!({ spreadsheetId: 'sid', range: 'A1:B2', formatJson: fj });
|
|
178
|
-
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
177
|
+
await handlers.get('gog_sheets_format')!({ spreadsheetId: 'sid', range: 'A1:B2', formatJson: fj, formatFields: 'textFormat.bold' });
|
|
178
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
179
|
+
['sheets', 'format', 'sid', 'A1:B2', `--format-json=${fj}`, '--format-fields=textFormat.bold'],
|
|
180
|
+
{ account: undefined },
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('passes raw formatJson without formatFields', async () => {
|
|
185
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
186
|
+
const handlers = setupHandlers();
|
|
187
|
+
await handlers.get('gog_sheets_format')!({ spreadsheetId: 'sid', range: 'A1', formatJson: '{}' });
|
|
188
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
189
|
+
['sheets', 'format', 'sid', 'A1', '--format-json={}'],
|
|
190
|
+
{ account: undefined },
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('composes named flags into CellFormat JSON + auto-computed fields', async () => {
|
|
195
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
196
|
+
const handlers = setupHandlers();
|
|
197
|
+
await handlers.get('gog_sheets_format')!({
|
|
198
|
+
spreadsheetId: 'sid', range: 'A1:C3',
|
|
199
|
+
bold: true,
|
|
200
|
+
backgroundColor: '#FFF5D9',
|
|
201
|
+
wrapStrategy: 'WRAP',
|
|
202
|
+
verticalAlignment: 'TOP',
|
|
203
|
+
});
|
|
204
|
+
const call = vi.mocked(lib.runOrDiagnose).mock.calls[0]!;
|
|
205
|
+
const argv = call[0];
|
|
206
|
+
expect(argv[0]).toBe('sheets');
|
|
207
|
+
expect(argv[1]).toBe('format');
|
|
208
|
+
expect(argv[2]).toBe('sid');
|
|
209
|
+
expect(argv[3]).toBe('A1:C3');
|
|
210
|
+
const fmtArg = (argv[4] as string).replace(/^--format-json=/, '');
|
|
211
|
+
const parsed = JSON.parse(fmtArg);
|
|
212
|
+
expect(parsed.textFormat.bold).toBe(true);
|
|
213
|
+
expect(parsed.backgroundColor.red).toBeCloseTo(1.0);
|
|
214
|
+
expect(parsed.backgroundColor.green).toBeCloseTo(245 / 255);
|
|
215
|
+
expect(parsed.backgroundColor.blue).toBeCloseTo(217 / 255);
|
|
216
|
+
expect(parsed.wrapStrategy).toBe('WRAP');
|
|
217
|
+
expect(parsed.verticalAlignment).toBe('TOP');
|
|
218
|
+
const fieldsArg = argv[5] as string;
|
|
219
|
+
expect(fieldsArg).toMatch(/^--format-fields=/);
|
|
220
|
+
expect(fieldsArg).toContain('textFormat.bold');
|
|
221
|
+
expect(fieldsArg).toContain('backgroundColor');
|
|
222
|
+
expect(fieldsArg).toContain('wrapStrategy');
|
|
223
|
+
expect(fieldsArg).toContain('verticalAlignment');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('composes all named flags including textColor + alignment', async () => {
|
|
227
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
228
|
+
const handlers = setupHandlers();
|
|
229
|
+
await handlers.get('gog_sheets_format')!({
|
|
230
|
+
spreadsheetId: 'sid', range: 'A1',
|
|
231
|
+
italic: true,
|
|
232
|
+
underline: true,
|
|
233
|
+
strikethrough: true,
|
|
234
|
+
fontSize: 14,
|
|
235
|
+
fontFamily: 'Inter',
|
|
236
|
+
textColor: '#FFF',
|
|
237
|
+
horizontalAlignment: 'CENTER',
|
|
238
|
+
});
|
|
239
|
+
const call = vi.mocked(lib.runOrDiagnose).mock.calls[0]!;
|
|
240
|
+
const fmtArg = (call[0][4] as string).replace(/^--format-json=/, '');
|
|
241
|
+
const parsed = JSON.parse(fmtArg);
|
|
242
|
+
expect(parsed.textFormat.italic).toBe(true);
|
|
243
|
+
expect(parsed.textFormat.underline).toBe(true);
|
|
244
|
+
expect(parsed.textFormat.strikethrough).toBe(true);
|
|
245
|
+
expect(parsed.textFormat.fontSize).toBe(14);
|
|
246
|
+
expect(parsed.textFormat.fontFamily).toBe('Inter');
|
|
247
|
+
expect(parsed.textFormat.foregroundColor.red).toBeCloseTo(1);
|
|
248
|
+
expect(parsed.textFormat.foregroundColor.green).toBeCloseTo(1);
|
|
249
|
+
expect(parsed.textFormat.foregroundColor.blue).toBeCloseTo(1);
|
|
250
|
+
expect(parsed.horizontalAlignment).toBe('CENTER');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('rejects bad hex in textColor', async () => {
|
|
254
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
255
|
+
const handlers = setupHandlers();
|
|
256
|
+
await expect(
|
|
257
|
+
handlers.get('gog_sheets_format')!({ spreadsheetId: 'sid', range: 'A1', textColor: 'not-a-hex' }),
|
|
258
|
+
).rejects.toThrow(/Invalid textColor/);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('rejects bad hex in backgroundColor', async () => {
|
|
262
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
263
|
+
const handlers = setupHandlers();
|
|
264
|
+
await expect(
|
|
265
|
+
handlers.get('gog_sheets_format')!({ spreadsheetId: 'sid', range: 'A1', backgroundColor: 'orange' }),
|
|
266
|
+
).rejects.toThrow(/Invalid backgroundColor/);
|
|
179
267
|
});
|
|
180
268
|
|
|
181
|
-
it('
|
|
269
|
+
it('rejects no-op calls with neither flags nor formatJson', async () => {
|
|
270
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
271
|
+
const handlers = setupHandlers();
|
|
272
|
+
await expect(
|
|
273
|
+
handlers.get('gog_sheets_format')!({ spreadsheetId: 'sid', range: 'A1' }),
|
|
274
|
+
).rejects.toThrow(/requires at least one named flag/);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('honors caller-provided formatFields when also using named flags', async () => {
|
|
278
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
279
|
+
const handlers = setupHandlers();
|
|
280
|
+
await handlers.get('gog_sheets_format')!({
|
|
281
|
+
spreadsheetId: 'sid', range: 'A1',
|
|
282
|
+
bold: true,
|
|
283
|
+
formatFields: 'textFormat.bold,textFormat.italic',
|
|
284
|
+
});
|
|
285
|
+
const call = vi.mocked(lib.runOrDiagnose).mock.calls[0]!;
|
|
286
|
+
expect(call[0][5]).toBe('--format-fields=textFormat.bold,textFormat.italic');
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe('gog_sheets_list_tabs', () => {
|
|
291
|
+
it('uses metadata with a --select projection', async () => {
|
|
182
292
|
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
183
293
|
const handlers = setupHandlers();
|
|
184
|
-
await handlers.get('
|
|
185
|
-
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
294
|
+
await handlers.get('gog_sheets_list_tabs')!({ spreadsheetId: 'sid' });
|
|
295
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
296
|
+
['sheets', 'metadata', 'sid', '--select=sheets.properties.sheetId,sheets.properties.title,sheets.properties.index,sheets.properties.gridProperties'],
|
|
297
|
+
{ account: undefined },
|
|
298
|
+
);
|
|
186
299
|
});
|
|
187
300
|
});
|
|
188
301
|
|