diff --git a/docs/feature-configuration.md b/docs/feature-configuration.md index 3e9aed6..1a7906d 100644 --- a/docs/feature-configuration.md +++ b/docs/feature-configuration.md @@ -64,8 +64,8 @@ export WORKSPACE_FEATURE_OVERRIDES="gmail.write:off" # Disable all of Chat export WORKSPACE_FEATURE_OVERRIDES="chat.read:off,chat.write:off" -# Enable experimental features (Slides write, Tasks) -export WORKSPACE_FEATURE_OVERRIDES="slides.write:on,tasks.read:on,tasks.write:on" +# Enable experimental features (Slides write, Sheets write, Tasks) +export WORKSPACE_FEATURE_OVERRIDES="slides.write:on,sheets.write:on,tasks.read:on,tasks.write:on" ``` ### Tool-Level Overrides @@ -197,6 +197,15 @@ When a feature group is disabled: - `sheets.getRange` - `sheets.getMetadata` +### `sheets.write` + +- `sheets.updateRange` +- `sheets.appendRange` +- `sheets.clearRange` +- `sheets.createSpreadsheet` +- `sheets.addSheet` +- `sheets.deleteSheet` + ### `time.read` - `time.getCurrentDate` diff --git a/docs/index.md b/docs/index.md index 7d7b622..031236f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -40,6 +40,14 @@ The extension provides the following tools: - `sheets.getRange`: Gets values from a specific range in a Google Sheets spreadsheet. - `sheets.getMetadata`: Gets metadata about a Google Sheets spreadsheet. +- `sheets.updateRange`: Writes values to a specific range in a Google Sheets + spreadsheet. +- `sheets.appendRange`: Appends rows to a range in a Google Sheets spreadsheet. +- `sheets.clearRange`: Clears values from a specific range in a Google Sheets + spreadsheet. +- `sheets.createSpreadsheet`: Creates a new Google Sheets spreadsheet. +- `sheets.addSheet`: Adds a sheet tab to a Google Sheets spreadsheet. +- `sheets.deleteSheet`: Deletes a sheet tab from a Google Sheets spreadsheet. ### Google Drive diff --git a/workspace-server/src/__tests__/services/SheetsService.test.ts b/workspace-server/src/__tests__/services/SheetsService.test.ts index 50c1ff4..b7258fc 100644 --- a/workspace-server/src/__tests__/services/SheetsService.test.ts +++ b/workspace-server/src/__tests__/services/SheetsService.test.ts @@ -38,8 +38,13 @@ describe('SheetsService', () => { mockSheetsAPI = { spreadsheets: { get: jest.fn(), + create: jest.fn(), + batchUpdate: jest.fn(), values: { get: jest.fn(), + update: jest.fn(), + append: jest.fn(), + clear: jest.fn(), }, }, }; @@ -370,4 +375,409 @@ describe('SheetsService', () => { expect(response.error).toBe('Metadata Error'); }); }); + + describe('updateRange', () => { + it('should write values to a specific range', async () => { + const mockResponse = { + data: { + updatedRange: 'Sheet1!A1:B2', + updatedRows: 2, + updatedColumns: 2, + updatedCells: 4, + }, + }; + + mockSheetsAPI.spreadsheets.values.update.mockResolvedValue(mockResponse); + + const result = await sheetsService.updateRange({ + spreadsheetId: 'test-id', + range: 'Sheet1!A1:B2', + values: [ + ['A1', 'B1'], + ['A2', 'B2'], + ], + }); + + expect(mockSheetsAPI.spreadsheets.values.update).toHaveBeenCalledWith({ + spreadsheetId: 'test-id', + range: 'Sheet1!A1:B2', + valueInputOption: 'USER_ENTERED', + requestBody: { + values: [ + ['A1', 'B1'], + ['A2', 'B2'], + ], + }, + }); + + const response = JSON.parse(result.content[0].text); + expect(response.updatedRange).toBe('Sheet1!A1:B2'); + expect(response.updatedRows).toBe(2); + expect(response.updatedColumns).toBe(2); + expect(response.updatedCells).toBe(4); + }); + + it('should use RAW valueInputOption when specified', async () => { + const mockResponse = { + data: { + updatedRange: 'Sheet1!A1:A1', + updatedRows: 1, + updatedColumns: 1, + updatedCells: 1, + }, + }; + + mockSheetsAPI.spreadsheets.values.update.mockResolvedValue(mockResponse); + + await sheetsService.updateRange({ + spreadsheetId: 'test-id', + range: 'Sheet1!A1', + values: [['=SUM(B1:B10)']], + valueInputOption: 'RAW', + }); + + expect(mockSheetsAPI.spreadsheets.values.update).toHaveBeenCalledWith( + expect.objectContaining({ + valueInputOption: 'RAW', + }), + ); + }); + + it('should extract spreadsheet ID from a Sheets URL', async () => { + const mockResponse = { + data: { + updatedRange: 'Sheet1!A1:A1', + updatedRows: 1, + updatedColumns: 1, + updatedCells: 1, + }, + }; + + mockSheetsAPI.spreadsheets.values.update.mockResolvedValue(mockResponse); + + await sheetsService.updateRange({ + spreadsheetId: + 'https://docs.google.com/spreadsheets/d/url-spreadsheet-id/edit#gid=0', + range: 'Sheet1!A1', + values: [['test']], + }); + + expect(mockSheetsAPI.spreadsheets.values.update).toHaveBeenCalledWith( + expect.objectContaining({ + spreadsheetId: 'url-spreadsheet-id', + }), + ); + }); + + it('should handle errors gracefully', async () => { + mockSheetsAPI.spreadsheets.values.update.mockRejectedValue( + new Error('Update Error'), + ); + + const result = await sheetsService.updateRange({ + spreadsheetId: 'error-id', + range: 'Sheet1!A1', + values: [['test']], + }); + + const response = JSON.parse(result.content[0].text); + expect(result).toHaveProperty('isError', true); + expect(response.error).toBe('Update Error'); + }); + }); + + describe('appendRange', () => { + it('should append rows to a sheet', async () => { + const mockResponse = { + data: { + updates: { + updatedRange: 'Sheet1!A4:B5', + updatedRows: 2, + updatedColumns: 2, + updatedCells: 4, + }, + }, + }; + + mockSheetsAPI.spreadsheets.values.append.mockResolvedValue(mockResponse); + + const result = await sheetsService.appendRange({ + spreadsheetId: 'test-id', + range: 'Sheet1!A:B', + values: [ + ['NewRow1', 'Data1'], + ['NewRow2', 'Data2'], + ], + }); + + expect(mockSheetsAPI.spreadsheets.values.append).toHaveBeenCalledWith({ + spreadsheetId: 'test-id', + range: 'Sheet1!A:B', + valueInputOption: 'USER_ENTERED', + insertDataOption: 'INSERT_ROWS', + requestBody: { + values: [ + ['NewRow1', 'Data1'], + ['NewRow2', 'Data2'], + ], + }, + }); + + const response = JSON.parse(result.content[0].text); + expect(response.updates.updatedRange).toBe('Sheet1!A4:B5'); + expect(response.updates.updatedRows).toBe(2); + }); + + it('should handle an append response without updates', async () => { + mockSheetsAPI.spreadsheets.values.append.mockResolvedValue({ data: {} }); + + const result = await sheetsService.appendRange({ + spreadsheetId: 'test-id', + range: 'Sheet1!A:B', + values: [['NewRow']], + }); + + const response = JSON.parse(result.content[0].text); + expect(response.updates.updatedRange).toBeUndefined(); + expect(response.updates.updatedRows).toBeUndefined(); + expect(response.updates.updatedColumns).toBeUndefined(); + expect(response.updates.updatedCells).toBeUndefined(); + }); + + it('should handle errors gracefully', async () => { + mockSheetsAPI.spreadsheets.values.append.mockRejectedValue( + new Error('Append Error'), + ); + + const result = await sheetsService.appendRange({ + spreadsheetId: 'error-id', + range: 'Sheet1!A:B', + values: [['test']], + }); + + const response = JSON.parse(result.content[0].text); + expect(result).toHaveProperty('isError', true); + expect(response.error).toBe('Append Error'); + }); + }); + + describe('clearRange', () => { + it('should clear values from a range', async () => { + const mockResponse = { + data: { + clearedRange: 'Sheet1!A1:D10', + }, + }; + + mockSheetsAPI.spreadsheets.values.clear.mockResolvedValue(mockResponse); + + const result = await sheetsService.clearRange({ + spreadsheetId: 'test-id', + range: 'Sheet1!A1:D10', + }); + + expect(mockSheetsAPI.spreadsheets.values.clear).toHaveBeenCalledWith({ + spreadsheetId: 'test-id', + range: 'Sheet1!A1:D10', + }); + + const response = JSON.parse(result.content[0].text); + expect(response.clearedRange).toBe('Sheet1!A1:D10'); + }); + + it('should handle errors gracefully', async () => { + mockSheetsAPI.spreadsheets.values.clear.mockRejectedValue( + new Error('Clear Error'), + ); + + const result = await sheetsService.clearRange({ + spreadsheetId: 'error-id', + range: 'Sheet1!A1:A1', + }); + + const response = JSON.parse(result.content[0].text); + expect(result).toHaveProperty('isError', true); + expect(response.error).toBe('Clear Error'); + }); + }); + + describe('createSpreadsheet', () => { + it('should create a new spreadsheet', async () => { + const mockResponse = { + data: { + spreadsheetId: 'new-spreadsheet-id', + spreadsheetUrl: + 'https://docs.google.com/spreadsheets/d/new-spreadsheet-id', + properties: { title: 'My New Sheet' }, + sheets: [{ properties: { sheetId: 0, title: 'Sheet1' } }], + }, + }; + + mockSheetsAPI.spreadsheets.create.mockResolvedValue(mockResponse); + + const result = await sheetsService.createSpreadsheet({ + title: 'My New Sheet', + }); + + expect(mockSheetsAPI.spreadsheets.create).toHaveBeenCalledWith({ + requestBody: { + properties: { title: 'My New Sheet' }, + sheets: undefined, + }, + }); + + const response = JSON.parse(result.content[0].text); + expect(response.spreadsheetId).toBe('new-spreadsheet-id'); + expect(response.title).toBe('My New Sheet'); + }); + + it('should create a spreadsheet with custom sheet titles', async () => { + const mockResponse = { + data: { + spreadsheetId: 'new-id', + spreadsheetUrl: 'https://docs.google.com/spreadsheets/d/new-id', + properties: { title: 'Budget' }, + sheets: [ + { properties: { sheetId: 0, title: 'Summary' } }, + { properties: { sheetId: 1, title: 'Data' } }, + ], + }, + }; + + mockSheetsAPI.spreadsheets.create.mockResolvedValue(mockResponse); + + const result = await sheetsService.createSpreadsheet({ + title: 'Budget', + sheetTitles: ['Summary', 'Data'], + }); + + expect(mockSheetsAPI.spreadsheets.create).toHaveBeenCalledWith({ + requestBody: { + properties: { title: 'Budget' }, + sheets: [ + { properties: { title: 'Summary' } }, + { properties: { title: 'Data' } }, + ], + }, + }); + + const response = JSON.parse(result.content[0].text); + expect(response.sheets).toHaveLength(2); + expect(response.sheets[0].title).toBe('Summary'); + }); + + it('should handle errors gracefully', async () => { + mockSheetsAPI.spreadsheets.create.mockRejectedValue( + new Error('Create Error'), + ); + + const result = await sheetsService.createSpreadsheet({ + title: 'Error Sheet', + }); + + const response = JSON.parse(result.content[0].text); + expect(result).toHaveProperty('isError', true); + expect(response.error).toBe('Create Error'); + }); + }); + + describe('addSheet', () => { + it('should add a new sheet to a spreadsheet', async () => { + const mockResponse = { + data: { + replies: [ + { + addSheet: { + properties: { sheetId: 123, title: 'New Tab' }, + }, + }, + ], + }, + }; + + mockSheetsAPI.spreadsheets.batchUpdate.mockResolvedValue(mockResponse); + + const result = await sheetsService.addSheet({ + spreadsheetId: 'test-id', + title: 'New Tab', + }); + + expect(mockSheetsAPI.spreadsheets.batchUpdate).toHaveBeenCalledWith({ + spreadsheetId: 'test-id', + requestBody: { + requests: [{ addSheet: { properties: { title: 'New Tab' } } }], + }, + }); + + const response = JSON.parse(result.content[0].text); + expect(response.sheetId).toBe(123); + expect(response.title).toBe('New Tab'); + }); + + it('should handle an addSheet response without replies', async () => { + mockSheetsAPI.spreadsheets.batchUpdate.mockResolvedValue({ data: {} }); + + const result = await sheetsService.addSheet({ + spreadsheetId: 'test-id', + title: 'New Tab', + }); + + const response = JSON.parse(result.content[0].text); + expect(response.sheetId).toBeUndefined(); + expect(response.title).toBeUndefined(); + }); + + it('should handle errors gracefully', async () => { + mockSheetsAPI.spreadsheets.batchUpdate.mockRejectedValue( + new Error('AddSheet Error'), + ); + + const result = await sheetsService.addSheet({ + spreadsheetId: 'error-id', + title: 'Bad Tab', + }); + + const response = JSON.parse(result.content[0].text); + expect(result).toHaveProperty('isError', true); + expect(response.error).toBe('AddSheet Error'); + }); + }); + + describe('deleteSheet', () => { + it('should delete a sheet from a spreadsheet', async () => { + mockSheetsAPI.spreadsheets.batchUpdate.mockResolvedValue({ data: {} }); + + const result = await sheetsService.deleteSheet({ + spreadsheetId: 'test-id', + sheetId: 456, + }); + + expect(mockSheetsAPI.spreadsheets.batchUpdate).toHaveBeenCalledWith({ + spreadsheetId: 'test-id', + requestBody: { + requests: [{ deleteSheet: { sheetId: 456 } }], + }, + }); + + const response = JSON.parse(result.content[0].text); + expect(response.message).toBe( + 'Delete sheet request completed for sheet 456', + ); + }); + + it('should handle errors gracefully', async () => { + mockSheetsAPI.spreadsheets.batchUpdate.mockRejectedValue( + new Error('DeleteSheet Error'), + ); + + const result = await sheetsService.deleteSheet({ + spreadsheetId: 'error-id', + sheetId: 999, + }); + + const response = JSON.parse(result.content[0].text); + expect(result).toHaveProperty('isError', true); + expect(response.error).toBe('DeleteSheet Error'); + }); + }); }); diff --git a/workspace-server/src/features/feature-config.ts b/workspace-server/src/features/feature-config.ts index 5b4e810..90cce2a 100644 --- a/workspace-server/src/features/feature-config.ts +++ b/workspace-server/src/features/feature-config.ts @@ -226,7 +226,14 @@ export const FEATURE_GROUPS: readonly FeatureGroup[] = [ service: 'sheets', group: 'write', scopes: scopes('spreadsheets'), - tools: [], + tools: [ + 'sheets.updateRange', + 'sheets.appendRange', + 'sheets.clearRange', + 'sheets.createSpreadsheet', + 'sheets.addSheet', + 'sheets.deleteSheet', + ], defaultEnabled: false, }, diff --git a/workspace-server/src/index.ts b/workspace-server/src/index.ts index dd5f56c..4adcb3d 100644 --- a/workspace-server/src/index.ts +++ b/workspace-server/src/index.ts @@ -523,6 +523,128 @@ async function main() { sheetsService.getMetadata, ); + registerTool( + 'sheets.updateRange', + { + description: + 'Writes values to a specific range in a Google Sheets spreadsheet.', + inputSchema: { + spreadsheetId: z.string().describe('The ID or URL of the spreadsheet.'), + range: z + .string() + .describe( + 'The A1 notation range to write to (e.g., "Sheet1!A1:B2").', + ), + values: z + .array( + z.array(z.union([z.string(), z.number(), z.boolean(), z.null()])), + ) + .describe( + 'The values to write, as a 2D array (rows x columns). Supports strings, numbers, booleans, and null.', + ), + valueInputOption: z + .enum(['RAW', 'USER_ENTERED']) + .optional() + .describe( + 'How to interpret the input values. RAW: values are stored as-is. USER_ENTERED: values are parsed as if typed into the UI (default: USER_ENTERED).', + ), + }, + }, + sheetsService.updateRange, + ); + + registerTool( + 'sheets.appendRange', + { + description: + 'Appends rows of values after the last row with data in a Google Sheets spreadsheet.', + inputSchema: { + spreadsheetId: z.string().describe('The ID or URL of the spreadsheet.'), + range: z + .string() + .describe( + 'The A1 notation range to search for data to append after (e.g., "Sheet1!A:E").', + ), + values: z + .array( + z.array(z.union([z.string(), z.number(), z.boolean(), z.null()])), + ) + .describe( + 'The rows to append, as a 2D array. Supports strings, numbers, booleans, and null.', + ), + valueInputOption: z + .enum(['RAW', 'USER_ENTERED']) + .optional() + .describe( + 'How to interpret the input values. RAW: values are stored as-is. USER_ENTERED: values are parsed as if typed into the UI (default: USER_ENTERED).', + ), + }, + }, + sheetsService.appendRange, + ); + + registerTool( + 'sheets.clearRange', + { + description: + 'Clears all values from a specific range in a Google Sheets spreadsheet.', + inputSchema: { + spreadsheetId: z.string().describe('The ID or URL of the spreadsheet.'), + range: z + .string() + .describe('The A1 notation range to clear (e.g., "Sheet1!A1:B2").'), + }, + }, + sheetsService.clearRange, + ); + + registerTool( + 'sheets.createSpreadsheet', + { + description: 'Creates a new Google Sheets spreadsheet.', + inputSchema: { + title: z.string().describe('The title of the new spreadsheet.'), + sheetTitles: z + .array(z.string()) + .optional() + .describe( + 'Optional list of sheet/tab names to create. Defaults to a single "Sheet1" tab.', + ), + }, + }, + sheetsService.createSpreadsheet, + ); + + registerTool( + 'sheets.addSheet', + { + description: + 'Adds a new sheet (tab) to an existing Google Sheets spreadsheet.', + inputSchema: { + spreadsheetId: z.string().describe('The ID or URL of the spreadsheet.'), + title: z.string().describe('The title of the new sheet/tab.'), + }, + }, + sheetsService.addSheet, + ); + + registerTool( + 'sheets.deleteSheet', + { + description: + 'Deletes a sheet (tab) from a Google Sheets spreadsheet by its numeric sheet ID.', + inputSchema: { + spreadsheetId: z.string().describe('The ID or URL of the spreadsheet.'), + sheetId: z + .number() + .describe( + 'The numeric ID of the sheet to delete. Use sheets.getMetadata to find sheet IDs.', + ), + }, + }, + sheetsService.deleteSheet, + ); + registerTool( 'drive.search', { diff --git a/workspace-server/src/services/SheetsService.ts b/workspace-server/src/services/SheetsService.ts index 636f03c..4ba9e5a 100644 --- a/workspace-server/src/services/SheetsService.ts +++ b/workspace-server/src/services/SheetsService.ts @@ -13,6 +13,26 @@ import { gaxiosOptions } from '../utils/GaxiosConfig'; export class SheetsService { constructor(private authManager: AuthManager) {} + private handleError( + context: string, + error: unknown, + ): { + isError: true; + content: { type: 'text'; text: string }[]; + } { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`[SheetsService] Error during ${context}: ${errorMessage}`); + return { + isError: true, + content: [ + { + type: 'text' as const, + text: JSON.stringify({ error: errorMessage }), + }, + ], + }; + } + private async getSheetsClient(): Promise { const auth = await this.authManager.getAuthenticatedClient(); const options = { ...gaxiosOptions, auth }; @@ -194,6 +214,253 @@ export class SheetsService { } }; + public updateRange = async ({ + spreadsheetId, + range, + values, + valueInputOption = 'USER_ENTERED', + }: { + spreadsheetId: string; + range: string; + values: (string | number | boolean | null)[][]; + valueInputOption?: 'RAW' | 'USER_ENTERED'; + }) => { + logToFile( + `[SheetsService] Starting updateRange for spreadsheet: ${spreadsheetId}, range: ${range}`, + ); + try { + const id = extractDocId(spreadsheetId) || spreadsheetId; + + const sheets = await this.getSheetsClient(); + const response = await sheets.spreadsheets.values.update({ + spreadsheetId: id, + range, + valueInputOption, + requestBody: { values }, + }); + + logToFile(`[SheetsService] Finished updateRange for spreadsheet: ${id}`); + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ + updatedRange: response.data.updatedRange, + updatedRows: response.data.updatedRows, + updatedColumns: response.data.updatedColumns, + updatedCells: response.data.updatedCells, + }), + }, + ], + }; + } catch (error) { + return this.handleError('sheets.updateRange', error); + } + }; + + public appendRange = async ({ + spreadsheetId, + range, + values, + valueInputOption = 'USER_ENTERED', + }: { + spreadsheetId: string; + range: string; + values: (string | number | boolean | null)[][]; + valueInputOption?: 'RAW' | 'USER_ENTERED'; + }) => { + logToFile( + `[SheetsService] Starting appendRange for spreadsheet: ${spreadsheetId}, range: ${range}`, + ); + try { + const id = extractDocId(spreadsheetId) || spreadsheetId; + + const sheets = await this.getSheetsClient(); + const response = await sheets.spreadsheets.values.append({ + spreadsheetId: id, + range, + valueInputOption, + insertDataOption: 'INSERT_ROWS', + requestBody: { values }, + }); + + logToFile(`[SheetsService] Finished appendRange for spreadsheet: ${id}`); + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ + updates: { + updatedRange: response.data.updates?.updatedRange, + updatedRows: response.data.updates?.updatedRows, + updatedColumns: response.data.updates?.updatedColumns, + updatedCells: response.data.updates?.updatedCells, + }, + }), + }, + ], + }; + } catch (error) { + return this.handleError('sheets.appendRange', error); + } + }; + + public clearRange = async ({ + spreadsheetId, + range, + }: { + spreadsheetId: string; + range: string; + }) => { + logToFile( + `[SheetsService] Starting clearRange for spreadsheet: ${spreadsheetId}, range: ${range}`, + ); + try { + const id = extractDocId(spreadsheetId) || spreadsheetId; + + const sheets = await this.getSheetsClient(); + const response = await sheets.spreadsheets.values.clear({ + spreadsheetId: id, + range, + }); + + logToFile(`[SheetsService] Finished clearRange for spreadsheet: ${id}`); + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ + clearedRange: response.data.clearedRange, + }), + }, + ], + }; + } catch (error) { + return this.handleError('sheets.clearRange', error); + } + }; + + public createSpreadsheet = async ({ + title, + sheetTitles, + }: { + title: string; + sheetTitles?: string[]; + }) => { + logToFile( + `[SheetsService] Starting createSpreadsheet with title: ${title}`, + ); + try { + const sheets = await this.getSheetsClient(); + const response = await sheets.spreadsheets.create({ + requestBody: { + properties: { title }, + sheets: sheetTitles?.map((t) => ({ properties: { title: t } })), + }, + }); + + logToFile( + `[SheetsService] Created spreadsheet: ${response.data.spreadsheetId}`, + ); + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ + spreadsheetId: response.data.spreadsheetId, + spreadsheetUrl: response.data.spreadsheetUrl, + title: response.data.properties?.title, + sheets: response.data.sheets?.map((s) => ({ + sheetId: s.properties?.sheetId, + title: s.properties?.title, + })), + }), + }, + ], + }; + } catch (error) { + return this.handleError('sheets.createSpreadsheet', error); + } + }; + + public addSheet = async ({ + spreadsheetId, + title, + }: { + spreadsheetId: string; + title: string; + }) => { + logToFile( + `[SheetsService] Starting addSheet for spreadsheet: ${spreadsheetId}, title: ${title}`, + ); + try { + const id = extractDocId(spreadsheetId) || spreadsheetId; + + const sheets = await this.getSheetsClient(); + const response = await sheets.spreadsheets.batchUpdate({ + spreadsheetId: id, + requestBody: { + requests: [{ addSheet: { properties: { title } } }], + }, + }); + + const addedSheet = response.data.replies?.[0]?.addSheet; + logToFile(`[SheetsService] Added sheet to spreadsheet: ${id}`); + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ + sheetId: addedSheet?.properties?.sheetId, + title: addedSheet?.properties?.title, + }), + }, + ], + }; + } catch (error) { + return this.handleError('sheets.addSheet', error); + } + }; + + public deleteSheet = async ({ + spreadsheetId, + sheetId, + }: { + spreadsheetId: string; + sheetId: number; + }) => { + logToFile( + `[SheetsService] Starting deleteSheet for spreadsheet: ${spreadsheetId}, sheetId: ${sheetId}`, + ); + try { + const id = extractDocId(spreadsheetId) || spreadsheetId; + + const sheets = await this.getSheetsClient(); + await sheets.spreadsheets.batchUpdate({ + spreadsheetId: id, + requestBody: { + requests: [{ deleteSheet: { sheetId } }], + }, + }); + + logToFile( + `[SheetsService] Deleted sheet ${sheetId} from spreadsheet: ${id}`, + ); + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ + message: `Delete sheet request completed for sheet ${sheetId}`, + }), + }, + ], + }; + } catch (error) { + return this.handleError('sheets.deleteSheet', error); + } + }; + public getMetadata = async ({ spreadsheetId }: { spreadsheetId: string }) => { logToFile( `[SheetsService] Starting getMetadata for spreadsheet: ${spreadsheetId}`,