gogcli-mcp-sheets 2.0.9 → 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.
@@ -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.9"
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.9",
18
+ "version": "2.0.10",
19
19
  "author": {
20
20
  "name": "Chris Hall"
21
21
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gogcli-mcp-sheets",
3
3
  "displayName": "gogcli (Sheets)",
4
- "version": "2.0.9",
4
+ "version": "2.0.10",
5
5
  "description": "Extended Google Sheets for Claude via gogcli — auth + full Sheets support",
6
6
  "author": {
7
7
  "name": "Chris Hall",
package/dist/index.js CHANGED
@@ -31219,6 +31219,7 @@ function registerAuthTools(server2) {
31219
31219
  }
31220
31220
 
31221
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()]);
31222
31223
  function registerSheetsTools(server2) {
31223
31224
  server2.registerTool("gog_sheets_get", {
31224
31225
  description: 'Read values from a Google Sheets range. Returns a JSON object with a "values" array of rows.',
@@ -31232,12 +31233,12 @@ function registerSheetsTools(server2) {
31232
31233
  return runOrDiagnose(["sheets", "get", spreadsheetId, range], { account });
31233
31234
  });
31234
31235
  server2.registerTool("gog_sheets_update", {
31235
- description: "Write values to a Google Sheets range, overwriting existing content.",
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)").',
31236
31237
  annotations: { destructiveHint: true },
31237
31238
  inputSchema: {
31238
31239
  spreadsheetId: external_exports.string().describe("Spreadsheet ID (from the URL)"),
31239
31240
  range: external_exports.string().describe("Top-left cell or range in A1 notation, e.g. Sheet1!A1"),
31240
- values: external_exports.array(external_exports.array(external_exports.string())).describe("2D array of values: outer array is rows, inner is columns"),
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.'),
31241
31242
  account: accountParam
31242
31243
  }
31243
31244
  }, async ({ spreadsheetId, range, values, account }) => {
@@ -31247,12 +31248,12 @@ function registerSheetsTools(server2) {
31247
31248
  );
31248
31249
  });
31249
31250
  server2.registerTool("gog_sheets_append", {
31250
- description: "Append rows to a Google Sheet after the last row with data in the given range.",
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.',
31251
31252
  annotations: { destructiveHint: true },
31252
31253
  inputSchema: {
31253
31254
  spreadsheetId: external_exports.string().describe("Spreadsheet ID (from the URL)"),
31254
31255
  range: external_exports.string().describe("Range indicating which sheet/columns to append to, e.g. Sheet1!A:C"),
31255
- values: external_exports.array(external_exports.array(external_exports.string())).describe("2D array of rows to append"),
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.'),
31256
31257
  account: accountParam
31257
31258
  }
31258
31259
  }, async ({ spreadsheetId, range, values, account }) => {
@@ -31308,7 +31309,7 @@ function registerSheetsTools(server2) {
31308
31309
  }
31309
31310
 
31310
31311
  // ../gogcli-mcp/src/server.ts
31311
- var VERSION = true ? "2.0.9" : "0.0.0";
31312
+ var VERSION = true ? "2.0.10" : "0.0.0";
31312
31313
  function createServer(options) {
31313
31314
  return new McpServer({
31314
31315
  name: options?.name ?? "gogcli",
@@ -31317,7 +31318,31 @@ function createServer(options) {
31317
31318
  }
31318
31319
 
31319
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
+ }
31320
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
+ });
31321
31346
  server2.registerTool("gog_sheets_add_tab", {
31322
31347
  description: "Add a new sheet tab to a spreadsheet.",
31323
31348
  inputSchema: {
@@ -31440,19 +31465,92 @@ function registerExtraSheetsTools(server2) {
31440
31465
  return runOrDiagnose(["sheets", "unmerge", spreadsheetId, range], { account });
31441
31466
  });
31442
31467
  server2.registerTool("gog_sheets_format", {
31443
- description: "Apply cell formatting (bold, colors, alignment, etc.) to a range. Pass format as a JSON CellFormat object.",
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.",
31444
31469
  annotations: { destructiveHint: true },
31445
31470
  inputSchema: {
31446
31471
  spreadsheetId: external_exports.string().describe("Spreadsheet ID"),
31447
31472
  range: external_exports.string().describe("Range to format (e.g. Sheet1!A1:C3)"),
31448
- formatJson: external_exports.string().describe('JSON CellFormat object (e.g. {"textFormat":{"bold":true}})'),
31449
- formatFields: external_exports.string().optional().describe("Comma-separated field mask (e.g. textFormat.bold,backgroundColor)"),
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."),
31450
31486
  account: accountParam
31451
31487
  }
31452
- }, async ({ spreadsheetId, range, formatJson, formatFields, account }) => {
31453
- const args = ["sheets", "format", spreadsheetId, range, `--format-json=${formatJson}`];
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}`];
31454
31552
  if (formatFields) args.push(`--format-fields=${formatFields}`);
31455
- return runOrDiagnose(args, { account });
31553
+ return runOrDiagnose(args, { account: a.account });
31456
31554
  });
31457
31555
  server2.registerTool("gog_sheets_number_format", {
31458
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.9",
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, colors, alignment, etc.) to a range. Pass format as a JSON CellFormat object."
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.9",
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.9",
10
+ "version": "2.0.10",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "gogcli-mcp-sheets",
15
- "version": "2.0.9",
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: {
@@ -135,19 +166,92 @@ export function registerExtraSheetsTools(server: McpServer): void {
135
166
  });
136
167
 
137
168
  server.registerTool('gog_sheets_format', {
138
- description: 'Apply cell formatting (bold, colors, alignment, etc.) to a range. Pass format as a JSON CellFormat object.',
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.',
139
170
  annotations: { destructiveHint: true },
140
171
  inputSchema: {
141
172
  spreadsheetId: z.string().describe('Spreadsheet ID'),
142
173
  range: z.string().describe('Range to format (e.g. Sheet1!A1:C3)'),
143
- formatJson: z.string().describe('JSON CellFormat object (e.g. {"textFormat":{"bold":true}})'),
144
- formatFields: z.string().optional().describe('Comma-separated field mask (e.g. textFormat.bold,backgroundColor)'),
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.'),
145
187
  account: accountParam,
146
188
  },
147
- }, async ({ spreadsheetId, range, formatJson, formatFields, account }) => {
148
- const args = ['sheets', 'format', spreadsheetId, range, `--format-json=${formatJson}`];
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}`];
149
253
  if (formatFields) args.push(`--format-fields=${formatFields}`);
150
- return runOrDiagnose(args, { account });
254
+ return runOrDiagnose(args, { account: a.account });
151
255
  });
152
256
 
153
257
  server.registerTool('gog_sheets_number_format', {
@@ -170,19 +170,132 @@ describe('gog_sheets_unmerge', () => {
170
170
 
171
171
  // 10. format
172
172
  describe('gog_sheets_format', () => {
173
- it('calls runOrDiagnose with correct args', async () => {
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(['sheets', 'format', 'sid', 'A1:B2', `--format-json=${fj}`], { account: undefined });
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('includes --format-fields when provided', async () => {
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('gog_sheets_format')!({ spreadsheetId: 'sid', range: 'A1', formatJson: '{}', formatFields: 'textFormat.bold' });
185
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['sheets', 'format', 'sid', 'A1', '--format-json={}', '--format-fields=textFormat.bold'], { account: undefined });
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