From 5ff081d92f910904975fd65afdbe12bef7fc8e39 Mon Sep 17 00:00:00 2001 From: Allen Hutchison Date: Fri, 6 Mar 2026 11:55:40 -0800 Subject: [PATCH 1/4] feat: remove slides.find and sheets.find in favor of drive.search Closes #268 Remove the redundant find methods from SlidesService and SheetsService, consolidating all file-finding through drive.search. This also drops the Drive API dependency (drive_v3, getDriveClient, buildDriveSearchQuery, MIME_TYPES) from both services entirely. Changes: - Remove slides.find and sheets.find tool registrations from index.ts - Remove find methods and Drive API deps from SlidesService and SheetsService - Remove associated tests and Drive API mocks - Update WORKSPACE-Context.md with MIME type filter examples for all types - Fix stale docs.find/docs.move references in Docs skill - Create new Sheets and Slides skills with drive.search guidance --- skills/docs/SKILL.md | 12 +++- skills/sheets/SKILL.md | 60 +++++++++++++++++ skills/slides/SKILL.md | 57 ++++++++++++++++ workspace-server/WORKSPACE-Context.md | 8 ++- .../__tests__/services/SheetsService.test.ts | 61 +---------------- .../__tests__/services/SlidesService.test.ts | 63 +----------------- workspace-server/src/index.ts | 40 ------------ .../src/services/SheetsService.ts | 65 +------------------ .../src/services/SlidesService.ts | 65 +------------------ 9 files changed, 138 insertions(+), 293 deletions(-) create mode 100644 skills/sheets/SKILL.md create mode 100644 skills/slides/SKILL.md diff --git a/skills/docs/SKILL.md b/skills/docs/SKILL.md index f333e190..53f837b0 100644 --- a/skills/docs/SKILL.md +++ b/skills/docs/SKILL.md @@ -216,12 +216,18 @@ All write tools (`writeText`, `replaceText`, `formatText`) accept an optional ### Finding Documents -Use `docs.find` to search by title. Supports pagination with `pageToken`. +Use `drive.search` with a document MIME type filter to find Google Docs: + +``` +drive.search({ + query: "mimeType='application/vnd.google-apps.document' and name contains 'Weekly Report'" +}) +``` ### Moving Documents -Use `docs.move` to move a document to a named folder. If multiple folders share -the same name, the first match is used. +Use `drive.moveFile` to move a document to a different folder. You can specify +the destination by folder ID or folder name. ## Comments & Suggestions diff --git a/skills/sheets/SKILL.md b/skills/sheets/SKILL.md new file mode 100644 index 00000000..ed2a8d01 --- /dev/null +++ b/skills/sheets/SKILL.md @@ -0,0 +1,60 @@ +--- +name: sheets +description: > + Activate this skill when the user wants to find, read, or analyze Google + Sheets spreadsheets. Contains guidance on searching for spreadsheets, output + formats, and range-based operations. +--- + +# Google Sheets Expert + +You are an expert at working with Google Sheets spreadsheets through the +Workspace Extension tools. Follow these guidelines when helping users with +spreadsheet tasks. + +## Finding Spreadsheets + +Use `drive.search` with a Sheets MIME type filter to find spreadsheets: + +``` +drive.search({ + query: "mimeType='application/vnd.google-apps.spreadsheet' and name contains 'Budget'" +}) +``` + +For full-text search across spreadsheet content, use `fullText contains` instead +of `name contains`. + +## Reading Data + +### Full Spreadsheet + +Use `sheets.getText` to read all sheets in a spreadsheet. Choose the output +format based on the use case: + +- **text** (default): Human-readable with pipe-separated columns — good for + quick review +- **csv**: Standard CSV format — good for data export and analysis +- **json**: Structured JSON keyed by sheet name — good for programmatic + processing + +### Specific Range + +Use `sheets.getRange` with A1 notation to read a specific cell range: + +``` +sheets.getRange({ + spreadsheetId: "spreadsheet-id", + range: "Sheet1!A1:D10" +}) +``` + +### Metadata + +Use `sheets.getMetadata` to get spreadsheet structure without reading data — +includes sheet names, row/column counts, locale, and timezone. + +## ID Handling + +- All tools accept Google Drive URLs directly — no manual ID extraction needed +- IDs and URLs are interchangeable in all `spreadsheetId` parameters diff --git a/skills/slides/SKILL.md b/skills/slides/SKILL.md new file mode 100644 index 00000000..7b54c39a --- /dev/null +++ b/skills/slides/SKILL.md @@ -0,0 +1,57 @@ +--- +name: slides +description: > + Activate this skill when the user wants to find, read, or extract content + from Google Slides presentations. Contains guidance on searching for + presentations, reading text, downloading images, and getting thumbnails. +--- + +# Google Slides Expert + +You are an expert at working with Google Slides presentations through the +Workspace Extension tools. Follow these guidelines when helping users with +presentation tasks. + +## Finding Presentations + +Use `drive.search` with a Slides MIME type filter to find presentations: + +``` +drive.search({ + query: "mimeType='application/vnd.google-apps.presentation' and name contains 'Quarterly Review'" +}) +``` + +For full-text search across presentation content, use `fullText contains` +instead of `name contains`. + +## Reading Content + +### Text Extraction + +Use `slides.getText` to extract all text content from a presentation. Text is +organized by slide with clear separators. + +### Metadata + +Use `slides.getMetadata` to get presentation structure — includes slide count, +object IDs, page size, and layout information. Slide object IDs from metadata +can be used with `slides.getSlideThumbnail`. + +## Downloading Images + +### All Images + +Use `slides.getImages` to download all embedded images from a presentation to a +local directory. Requires an **absolute path** for the output directory. + +### Slide Thumbnails + +Use `slides.getSlideThumbnail` to download a thumbnail of a specific slide. +Requires the slide's `objectId` (from `slides.getMetadata` or `slides.getText`) +and an **absolute path** for the output file. + +## ID Handling + +- All tools accept Google Drive URLs directly — no manual ID extraction needed +- IDs and URLs are interchangeable in all `presentationId` parameters diff --git a/workspace-server/WORKSPACE-Context.md b/workspace-server/WORKSPACE-Context.md index 5ed4d280..03c0b972 100644 --- a/workspace-server/WORKSPACE-Context.md +++ b/workspace-server/WORKSPACE-Context.md @@ -82,8 +82,12 @@ When creating documents in specific folders: 2. Move it to the target folder with `drive.moveFile` 3. Confirm successful completion -To find Google Docs, use `drive.search` with a document MIME type filter rather -than searching by name alone. +To find Google Docs, Sheets, or Slides, use `drive.search` with a MIME type +filter rather than searching by name alone. Example MIME type queries: + +- Docs: `mimeType='application/vnd.google-apps.document' and name contains 'query'` +- Sheets: `mimeType='application/vnd.google-apps.spreadsheet' and name contains 'query'` +- Slides: `mimeType='application/vnd.google-apps.presentation' and name contains 'query'` ## 📄 Docs, Sheets, and Slides diff --git a/workspace-server/src/__tests__/services/SheetsService.test.ts b/workspace-server/src/__tests__/services/SheetsService.test.ts index ac5fc7fd..23bbc60e 100644 --- a/workspace-server/src/__tests__/services/SheetsService.test.ts +++ b/workspace-server/src/__tests__/services/SheetsService.test.ts @@ -24,7 +24,6 @@ describe('SheetsService', () => { let sheetsService: SheetsService; let mockAuthManager: jest.Mocked; let mockSheetsAPI: any; - let mockDriveAPI: any; beforeEach(() => { // Clear all mocks before each test @@ -45,15 +44,10 @@ describe('SheetsService', () => { }, }; - mockDriveAPI = { - files: { - list: jest.fn(), - }, - }; // Mock the google constructors (google.sheets as jest.Mock) = jest.fn().mockReturnValue(mockSheetsAPI); - (google.drive as jest.Mock) = jest.fn().mockReturnValue(mockDriveAPI); + // Create SheetsService instance sheetsService = new SheetsService(mockAuthManager); @@ -306,59 +300,6 @@ describe('SheetsService', () => { }); }); - describe('find', () => { - it('should find spreadsheets by query', async () => { - const mockResponse = { - data: { - files: [ - { id: 'sheet1', name: 'Spreadsheet 1' }, - { id: 'sheet2', name: 'Spreadsheet 2' }, - ], - nextPageToken: 'next-token', - }, - }; - - mockDriveAPI.files.list.mockResolvedValue(mockResponse); - - const result = await sheetsService.find({ query: 'budget' }); - const response = JSON.parse(result.content[0].text); - - expect(mockDriveAPI.files.list).toHaveBeenCalledWith({ - pageSize: 10, - fields: 'nextPageToken, files(id, name)', - q: "mimeType='application/vnd.google-apps.spreadsheet' and fullText contains 'budget'", - pageToken: undefined, - supportsAllDrives: true, - includeItemsFromAllDrives: true, - }); - - expect(response.files).toHaveLength(2); - expect(response.files[0].name).toBe('Spreadsheet 1'); - expect(response.nextPageToken).toBe('next-token'); - }); - - it('should handle title-specific searches', async () => { - const mockResponse = { - data: { - files: [{ id: 'sheet1', name: 'Q4 Budget' }], - }, - }; - - mockDriveAPI.files.list.mockResolvedValue(mockResponse); - - const result = await sheetsService.find({ query: 'title:"Q4 Budget"' }); - const response = JSON.parse(result.content[0].text); - - expect(mockDriveAPI.files.list).toHaveBeenCalledWith( - expect.objectContaining({ - q: "mimeType='application/vnd.google-apps.spreadsheet' and name contains 'Q4 Budget'", - }), - ); - - expect(response.files).toHaveLength(1); - expect(response.files[0].name).toBe('Q4 Budget'); - }); - }); describe('getMetadata', () => { it('should retrieve spreadsheet metadata', async () => { diff --git a/workspace-server/src/__tests__/services/SlidesService.test.ts b/workspace-server/src/__tests__/services/SlidesService.test.ts index b1f180ee..3bc112c8 100644 --- a/workspace-server/src/__tests__/services/SlidesService.test.ts +++ b/workspace-server/src/__tests__/services/SlidesService.test.ts @@ -44,7 +44,6 @@ describe('SlidesService', () => { let slidesService: SlidesService; let mockAuthManager: jest.Mocked; let mockSlidesAPI: any; - let mockDriveAPI: any; beforeEach(() => { // Clear all mocks before each test @@ -62,15 +61,10 @@ describe('SlidesService', () => { }, }; - mockDriveAPI = { - files: { - list: jest.fn(), - }, - }; // Mock the google constructors (google.slides as jest.Mock) = jest.fn().mockReturnValue(mockSlidesAPI); - (google.drive as jest.Mock) = jest.fn().mockReturnValue(mockDriveAPI); + // Create SlidesService instance slidesService = new SlidesService(mockAuthManager); @@ -195,61 +189,6 @@ describe('SlidesService', () => { }); }); - describe('find', () => { - it('should find presentations by query', async () => { - const mockResponse = { - data: { - files: [ - { id: 'pres1', name: 'Presentation 1' }, - { id: 'pres2', name: 'Presentation 2' }, - ], - nextPageToken: 'next-token', - }, - }; - - mockDriveAPI.files.list.mockResolvedValue(mockResponse); - - const result = await slidesService.find({ query: 'test query' }); - const response = JSON.parse(result.content[0].text); - - expect(mockDriveAPI.files.list).toHaveBeenCalledWith({ - pageSize: 10, - fields: 'nextPageToken, files(id, name)', - q: "mimeType='application/vnd.google-apps.presentation' and fullText contains 'test query'", - pageToken: undefined, - supportsAllDrives: true, - includeItemsFromAllDrives: true, - }); - - expect(response.files).toHaveLength(2); - expect(response.files[0].name).toBe('Presentation 1'); - expect(response.nextPageToken).toBe('next-token'); - }); - - it('should handle title-specific searches', async () => { - const mockResponse = { - data: { - files: [{ id: 'pres1', name: 'Specific Title' }], - }, - }; - - mockDriveAPI.files.list.mockResolvedValue(mockResponse); - - const result = await slidesService.find({ - query: 'title:"Specific Title"', - }); - const response = JSON.parse(result.content[0].text); - - expect(mockDriveAPI.files.list).toHaveBeenCalledWith( - expect.objectContaining({ - q: "mimeType='application/vnd.google-apps.presentation' and name contains 'Specific Title'", - }), - ); - - expect(response.files).toHaveLength(1); - expect(response.files[0].name).toBe('Specific Title'); - }); - }); describe('getMetadata', () => { it('should retrieve presentation metadata', async () => { diff --git a/workspace-server/src/index.ts b/workspace-server/src/index.ts index ed4f0635..852c976d 100644 --- a/workspace-server/src/index.ts +++ b/workspace-server/src/index.ts @@ -371,26 +371,6 @@ async function main() { slidesService.getText, ); - server.registerTool( - 'slides.find', - { - description: - 'Finds Google Slides presentations by searching for a query. Supports pagination.', - inputSchema: { - query: z.string().describe('The text to search for in presentations.'), - pageToken: z - .string() - .optional() - .describe('The token for the next page of results.'), - pageSize: z - .number() - .optional() - .describe('The maximum number of results to return.'), - }, - ...readOnlyToolProps, - }, - slidesService.find, - ); server.registerTool( 'slides.getMetadata', @@ -486,26 +466,6 @@ async function main() { sheetsService.getRange, ); - server.registerTool( - 'sheets.find', - { - description: - 'Finds Google Sheets spreadsheets by searching for a query. Supports pagination.', - inputSchema: { - query: z.string().describe('The text to search for in spreadsheets.'), - pageToken: z - .string() - .optional() - .describe('The token for the next page of results.'), - pageSize: z - .number() - .optional() - .describe('The maximum number of results to return.'), - }, - ...readOnlyToolProps, - }, - sheetsService.find, - ); server.registerTool( 'sheets.getMetadata', diff --git a/workspace-server/src/services/SheetsService.ts b/workspace-server/src/services/SheetsService.ts index fb8df36f..3785d98c 100644 --- a/workspace-server/src/services/SheetsService.ts +++ b/workspace-server/src/services/SheetsService.ts @@ -4,12 +4,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { google, sheets_v4, drive_v3 } from 'googleapis'; +import { google, sheets_v4 } from 'googleapis'; import { AuthManager } from '../auth/AuthManager'; import { logToFile } from '../utils/logger'; import { extractDocId } from '../utils/IdUtils'; import { gaxiosOptions } from '../utils/GaxiosConfig'; -import { buildDriveSearchQuery, MIME_TYPES } from '../utils/DriveQueryBuilder'; + export class SheetsService { constructor(private authManager: AuthManager) {} @@ -20,11 +20,6 @@ export class SheetsService { return google.sheets({ version: 'v4', ...options }); } - private async getDriveClient(): Promise { - const auth = await this.authManager.getAuthenticatedClient(); - const options = { ...gaxiosOptions, auth }; - return google.drive({ version: 'v3', ...options }); - } public getText = async ({ spreadsheetId, @@ -201,62 +196,6 @@ export class SheetsService { } }; - public find = async ({ - query, - pageToken, - pageSize = 10, - }: { - query: string; - pageToken?: string; - pageSize?: number; - }) => { - logToFile( - `[SheetsService] Searching for spreadsheets with query: ${query}`, - ); - try { - const q = buildDriveSearchQuery(MIME_TYPES.SPREADSHEET, query); - logToFile(`[SheetsService] Executing Drive API query: ${q}`); - - const drive = await this.getDriveClient(); - const res = await drive.files.list({ - pageSize: pageSize, - fields: 'nextPageToken, files(id, name)', - q: q, - pageToken: pageToken, - supportsAllDrives: true, - includeItemsFromAllDrives: true, - }); - - const files = res.data.files || []; - const nextPageToken = res.data.nextPageToken; - - logToFile(`[SheetsService] Found ${files.length} spreadsheets.`); - - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ - files: files, - nextPageToken: nextPageToken, - }), - }, - ], - }; - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error); - logToFile(`[SheetsService] Error during sheets.find: ${errorMessage}`); - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ error: errorMessage }), - }, - ], - }; - } - }; public getMetadata = async ({ spreadsheetId }: { spreadsheetId: string }) => { logToFile( diff --git a/workspace-server/src/services/SlidesService.ts b/workspace-server/src/services/SlidesService.ts index d9627d7f..720f6146 100644 --- a/workspace-server/src/services/SlidesService.ts +++ b/workspace-server/src/services/SlidesService.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { google, slides_v1, drive_v3 } from 'googleapis'; +import { google, slides_v1 } from 'googleapis'; import * as fs from 'node:fs/promises'; import * as path from 'node:path'; import { request } from 'gaxios'; @@ -12,7 +12,7 @@ import { AuthManager } from '../auth/AuthManager'; import { logToFile } from '../utils/logger'; import { extractDocId } from '../utils/IdUtils'; import { gaxiosOptions } from '../utils/GaxiosConfig'; -import { buildDriveSearchQuery, MIME_TYPES } from '../utils/DriveQueryBuilder'; + export class SlidesService { constructor(private authManager: AuthManager) {} @@ -23,11 +23,6 @@ export class SlidesService { return google.slides({ version: 'v1', ...options }); } - private async getDriveClient(): Promise { - const auth = await this.authManager.getAuthenticatedClient(); - const options = { ...gaxiosOptions, auth }; - return google.drive({ version: 'v3', ...options }); - } public getText = async ({ presentationId }: { presentationId: string }) => { logToFile( @@ -132,62 +127,6 @@ export class SlidesService { return text; } - public find = async ({ - query, - pageToken, - pageSize = 10, - }: { - query: string; - pageToken?: string; - pageSize?: number; - }) => { - logToFile( - `[SlidesService] Searching for presentations with query: ${query}`, - ); - try { - const q = buildDriveSearchQuery(MIME_TYPES.PRESENTATION, query); - logToFile(`[SlidesService] Executing Drive API query: ${q}`); - - const drive = await this.getDriveClient(); - const res = await drive.files.list({ - pageSize: pageSize, - fields: 'nextPageToken, files(id, name)', - q: q, - pageToken: pageToken, - supportsAllDrives: true, - includeItemsFromAllDrives: true, - }); - - const files = res.data.files || []; - const nextPageToken = res.data.nextPageToken; - - logToFile(`[SlidesService] Found ${files.length} presentations.`); - - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ - files: files, - nextPageToken: nextPageToken, - }), - }, - ], - }; - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error); - logToFile(`[SlidesService] Error during slides.find: ${errorMessage}`); - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ error: errorMessage }), - }, - ], - }; - } - }; public getMetadata = async ({ presentationId, From 214ccc027f029a4c6568698f401d2d5393c3cf96 Mon Sep 17 00:00:00 2001 From: Allen Hutchison Date: Fri, 6 Mar 2026 11:59:47 -0800 Subject: [PATCH 2/4] refactor: move Sheets/Slides details from WORKSPACE-Context to skills Remove the Docs/Sheets/Slides section (format selection, content handling) since these are now covered by individual skills. Replace inline Sheets nuances with a skill cross-reference, add Slides cross-reference. --- workspace-server/WORKSPACE-Context.md | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/workspace-server/WORKSPACE-Context.md b/workspace-server/WORKSPACE-Context.md index 03c0b972..a3e1978e 100644 --- a/workspace-server/WORKSPACE-Context.md +++ b/workspace-server/WORKSPACE-Context.md @@ -89,21 +89,6 @@ filter rather than searching by name alone. Example MIME type queries: - Sheets: `mimeType='application/vnd.google-apps.spreadsheet' and name contains 'query'` - Slides: `mimeType='application/vnd.google-apps.presentation' and name contains 'query'` -## 📄 Docs, Sheets, and Slides - -### Format Selection (Sheets) - -Choose output format based on use case: - -- **text**: Human-readable, good for quick review -- **csv**: Data export, analysis in other tools -- **json**: Programmatic processing, structured data - -### Content Handling - -- Docs/Sheets/Slides tools accept URLs directly - no ID extraction needed -- Use markdown for initial document creation when appropriate -- Preserve formatting when reading/modifying content ## 🚫 Common Pitfalls to Avoid @@ -190,9 +175,13 @@ Choose output format based on use case: ### Google Sheets -- Multiple output formats available -- Range-based operations with A1 notation -- Metadata includes sheet structure information +- See the **Sheets skill** for detailed guidance on finding spreadsheets, + output format selection, and range-based operations. + +### Google Slides + +- See the **Slides skill** for detailed guidance on finding presentations, + text extraction, image downloads, and slide thumbnails. ### Google Calendar From 9626cd68659f833f2eef311abf7e55bdd63bfc96 Mon Sep 17 00:00:00 2001 From: Allen Hutchison Date: Fri, 6 Mar 2026 14:29:11 -0800 Subject: [PATCH 3/4] refactor: remove unused buildDriveSearchQuery and MIME_TYPES Address PR review feedback: - Remove buildDriveSearchQuery and MIME_TYPES from DriveQueryBuilder.ts (no longer used after removing slides.find and sheets.find) - Keep only escapeQueryString (still used by DriveService) - Rewrite DriveQueryBuilder tests to cover escapeQueryString only - Add fullText contains note to Docs skill for consistency --- skills/docs/SKILL.md | 3 + .../__tests__/utils/DriveQueryBuilder.test.ts | 114 ++---------------- .../src/utils/DriveQueryBuilder.ts | 43 +------ 3 files changed, 16 insertions(+), 144 deletions(-) diff --git a/skills/docs/SKILL.md b/skills/docs/SKILL.md index 53f837b0..94935a7f 100644 --- a/skills/docs/SKILL.md +++ b/skills/docs/SKILL.md @@ -224,6 +224,9 @@ drive.search({ }) ``` +For full-text search across document content, use `fullText contains` instead +of `name contains`. + ### Moving Documents Use `drive.moveFile` to move a document to a different folder. You can specify diff --git a/workspace-server/src/__tests__/utils/DriveQueryBuilder.test.ts b/workspace-server/src/__tests__/utils/DriveQueryBuilder.test.ts index e90cc0c2..421131ff 100644 --- a/workspace-server/src/__tests__/utils/DriveQueryBuilder.test.ts +++ b/workspace-server/src/__tests__/utils/DriveQueryBuilder.test.ts @@ -5,120 +5,30 @@ */ import { describe, it, expect } from '@jest/globals'; -import { - buildDriveSearchQuery, - MIME_TYPES, -} from '../../utils/DriveQueryBuilder'; +import { escapeQueryString } from '../../utils/DriveQueryBuilder'; describe('DriveQueryBuilder', () => { - describe('buildDriveSearchQuery', () => { - it('should build fullText query for regular search', () => { - const query = buildDriveSearchQuery(MIME_TYPES.DOCUMENT, 'test query'); - expect(query).toBe( - "mimeType='application/vnd.google-apps.document' and fullText contains 'test query'", - ); + describe('escapeQueryString', () => { + it('should escape backslashes', () => { + expect(escapeQueryString('path\\to\\file')).toBe('path\\\\to\\\\file'); }); - it('should build name query for title-prefixed search', () => { - const query = buildDriveSearchQuery( - MIME_TYPES.PRESENTATION, - 'title:My Presentation', - ); - expect(query).toBe( - "mimeType='application/vnd.google-apps.presentation' and name contains 'My Presentation'", - ); + it('should escape single quotes', () => { + expect(escapeQueryString("it's a test")).toBe("it\\'s a test"); }); - it('should handle quoted title searches', () => { - const query = buildDriveSearchQuery( - MIME_TYPES.SPREADSHEET, - 'title:"Budget 2024"', - ); - expect(query).toBe( - "mimeType='application/vnd.google-apps.spreadsheet' and name contains 'Budget 2024'", + it('should escape both backslashes and single quotes', () => { + expect(escapeQueryString("John's Presentation\\2024")).toBe( + "John\\'s Presentation\\\\2024", ); }); - it('should handle single-quoted title searches', () => { - const query = buildDriveSearchQuery( - MIME_TYPES.DOCUMENT, - "title:'Q4 Report'", - ); - expect(query).toBe( - "mimeType='application/vnd.google-apps.document' and name contains 'Q4 Report'", - ); - }); - - it('should escape special characters in query', () => { - const query = buildDriveSearchQuery( - MIME_TYPES.DOCUMENT, - "test's query\\path", - ); - expect(query).toBe( - "mimeType='application/vnd.google-apps.document' and fullText contains 'test\\'s query\\\\path'", - ); - }); - - it('should escape special characters in title search', () => { - const query = buildDriveSearchQuery( - MIME_TYPES.PRESENTATION, - "title:John's Presentation\\2024", - ); - expect(query).toBe( - "mimeType='application/vnd.google-apps.presentation' and name contains 'John\\'s Presentation\\\\2024'", - ); + it('should handle strings without special characters', () => { + expect(escapeQueryString('hello world')).toBe('hello world'); }); it('should handle empty strings', () => { - const query = buildDriveSearchQuery(MIME_TYPES.SPREADSHEET, ''); - expect(query).toBe( - "mimeType='application/vnd.google-apps.spreadsheet' and fullText contains ''", - ); - }); - - it('should handle whitespace-only queries', () => { - const query = buildDriveSearchQuery(MIME_TYPES.DOCUMENT, ' '); - expect(query).toBe( - "mimeType='application/vnd.google-apps.document' and fullText contains ' '", - ); - }); - - it('should handle title prefix with whitespace', () => { - const query = buildDriveSearchQuery( - MIME_TYPES.PRESENTATION, - ' title: "My Doc" ', - ); - expect(query).toBe( - "mimeType='application/vnd.google-apps.presentation' and name contains 'My Doc'", - ); - }); - - it('should work with all MIME types', () => { - expect(buildDriveSearchQuery(MIME_TYPES.DOCUMENT, 'test')).toContain( - 'application/vnd.google-apps.document', - ); - expect(buildDriveSearchQuery(MIME_TYPES.PRESENTATION, 'test')).toContain( - 'application/vnd.google-apps.presentation', - ); - expect(buildDriveSearchQuery(MIME_TYPES.SPREADSHEET, 'test')).toContain( - 'application/vnd.google-apps.spreadsheet', - ); - expect(buildDriveSearchQuery(MIME_TYPES.FOLDER, 'test')).toContain( - 'application/vnd.google-apps.folder', - ); - }); - }); - - describe('MIME_TYPES constants', () => { - it('should have correct MIME type values', () => { - expect(MIME_TYPES.DOCUMENT).toBe('application/vnd.google-apps.document'); - expect(MIME_TYPES.PRESENTATION).toBe( - 'application/vnd.google-apps.presentation', - ); - expect(MIME_TYPES.SPREADSHEET).toBe( - 'application/vnd.google-apps.spreadsheet', - ); - expect(MIME_TYPES.FOLDER).toBe('application/vnd.google-apps.folder'); + expect(escapeQueryString('')).toBe(''); }); }); }); diff --git a/workspace-server/src/utils/DriveQueryBuilder.ts b/workspace-server/src/utils/DriveQueryBuilder.ts index 02cf6fd6..0f568f78 100644 --- a/workspace-server/src/utils/DriveQueryBuilder.ts +++ b/workspace-server/src/utils/DriveQueryBuilder.ts @@ -5,42 +5,9 @@ */ /** - * Utility for building Google Drive API search queries + * Utility for escaping Google Drive API search query strings */ -/** - * Builds a Drive API search query for a specific MIME type with optional title filtering - * @param mimeType The MIME type to search for (e.g., 'application/vnd.google-apps.document') - * @param query The search query, may include 'title:' prefix for title-only searches - * @returns The formatted Drive API query string - */ -export function buildDriveSearchQuery(mimeType: string, query: string): string { - let searchTerm = query; - const titlePrefix = 'title:'; - let q: string; - - if (searchTerm.trim().startsWith(titlePrefix)) { - // Extract search term after 'title:' prefix - searchTerm = searchTerm.trim().substring(titlePrefix.length).trim(); - - // Remove surrounding quotes if present - if ( - (searchTerm.startsWith("'") && searchTerm.endsWith("'")) || - (searchTerm.startsWith('"') && searchTerm.endsWith('"')) - ) { - searchTerm = searchTerm.substring(1, searchTerm.length - 1); - } - - // Search by name (title) only - q = `mimeType='${mimeType}' and name contains '${escapeQueryString(searchTerm)}'`; - } else { - // Search full text content - q = `mimeType='${mimeType}' and fullText contains '${escapeQueryString(searchTerm)}'`; - } - - return q; -} - /** * Escapes special characters in a query string for Drive API * @param str The string to escape @@ -49,11 +16,3 @@ export function buildDriveSearchQuery(mimeType: string, query: string): string { export function escapeQueryString(str: string): string { return str.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); } - -// Export MIME type constants for convenience -export const MIME_TYPES = { - DOCUMENT: 'application/vnd.google-apps.document', - PRESENTATION: 'application/vnd.google-apps.presentation', - SPREADSHEET: 'application/vnd.google-apps.spreadsheet', - FOLDER: 'application/vnd.google-apps.folder', -} as const; From 6cd7ca8dbc2f9f12a897f75e7bc453ff803d1a17 Mon Sep 17 00:00:00 2001 From: Allen Hutchison Date: Mon, 9 Mar 2026 14:54:17 -0700 Subject: [PATCH 4/4] style: run prettier formatting --- skills/docs/SKILL.md | 4 ++-- skills/slides/SKILL.md | 6 +++--- workspace-server/WORKSPACE-Context.md | 18 ++++++++++-------- .../__tests__/services/SheetsService.test.ts | 3 --- .../__tests__/services/SlidesService.test.ts | 3 --- workspace-server/src/index.ts | 2 -- workspace-server/src/services/SheetsService.ts | 3 --- workspace-server/src/services/SlidesService.ts | 3 --- 8 files changed, 15 insertions(+), 27 deletions(-) diff --git a/skills/docs/SKILL.md b/skills/docs/SKILL.md index 94935a7f..a53d7828 100644 --- a/skills/docs/SKILL.md +++ b/skills/docs/SKILL.md @@ -224,8 +224,8 @@ drive.search({ }) ``` -For full-text search across document content, use `fullText contains` instead -of `name contains`. +For full-text search across document content, use `fullText contains` instead of +`name contains`. ### Moving Documents diff --git a/skills/slides/SKILL.md b/skills/slides/SKILL.md index 7b54c39a..7a8d06ef 100644 --- a/skills/slides/SKILL.md +++ b/skills/slides/SKILL.md @@ -1,9 +1,9 @@ --- name: slides description: > - Activate this skill when the user wants to find, read, or extract content - from Google Slides presentations. Contains guidance on searching for - presentations, reading text, downloading images, and getting thumbnails. + Activate this skill when the user wants to find, read, or extract content from + Google Slides presentations. Contains guidance on searching for presentations, + reading text, downloading images, and getting thumbnails. --- # Google Slides Expert diff --git a/workspace-server/WORKSPACE-Context.md b/workspace-server/WORKSPACE-Context.md index a3e1978e..218dd650 100644 --- a/workspace-server/WORKSPACE-Context.md +++ b/workspace-server/WORKSPACE-Context.md @@ -85,10 +85,12 @@ When creating documents in specific folders: To find Google Docs, Sheets, or Slides, use `drive.search` with a MIME type filter rather than searching by name alone. Example MIME type queries: -- Docs: `mimeType='application/vnd.google-apps.document' and name contains 'query'` -- Sheets: `mimeType='application/vnd.google-apps.spreadsheet' and name contains 'query'` -- Slides: `mimeType='application/vnd.google-apps.presentation' and name contains 'query'` - +- Docs: + `mimeType='application/vnd.google-apps.document' and name contains 'query'` +- Sheets: + `mimeType='application/vnd.google-apps.spreadsheet' and name contains 'query'` +- Slides: + `mimeType='application/vnd.google-apps.presentation' and name contains 'query'` ## 🚫 Common Pitfalls to Avoid @@ -175,13 +177,13 @@ filter rather than searching by name alone. Example MIME type queries: ### Google Sheets -- See the **Sheets skill** for detailed guidance on finding spreadsheets, - output format selection, and range-based operations. +- See the **Sheets skill** for detailed guidance on finding spreadsheets, output + format selection, and range-based operations. ### Google Slides -- See the **Slides skill** for detailed guidance on finding presentations, - text extraction, image downloads, and slide thumbnails. +- See the **Slides skill** for detailed guidance on finding presentations, text + extraction, image downloads, and slide thumbnails. ### Google Calendar diff --git a/workspace-server/src/__tests__/services/SheetsService.test.ts b/workspace-server/src/__tests__/services/SheetsService.test.ts index 23bbc60e..50c1ff4d 100644 --- a/workspace-server/src/__tests__/services/SheetsService.test.ts +++ b/workspace-server/src/__tests__/services/SheetsService.test.ts @@ -44,11 +44,9 @@ describe('SheetsService', () => { }, }; - // Mock the google constructors (google.sheets as jest.Mock) = jest.fn().mockReturnValue(mockSheetsAPI); - // Create SheetsService instance sheetsService = new SheetsService(mockAuthManager); @@ -300,7 +298,6 @@ describe('SheetsService', () => { }); }); - describe('getMetadata', () => { it('should retrieve spreadsheet metadata', async () => { const mockSpreadsheet = { diff --git a/workspace-server/src/__tests__/services/SlidesService.test.ts b/workspace-server/src/__tests__/services/SlidesService.test.ts index 3bc112c8..a0e0d678 100644 --- a/workspace-server/src/__tests__/services/SlidesService.test.ts +++ b/workspace-server/src/__tests__/services/SlidesService.test.ts @@ -61,11 +61,9 @@ describe('SlidesService', () => { }, }; - // Mock the google constructors (google.slides as jest.Mock) = jest.fn().mockReturnValue(mockSlidesAPI); - // Create SlidesService instance slidesService = new SlidesService(mockAuthManager); @@ -189,7 +187,6 @@ describe('SlidesService', () => { }); }); - describe('getMetadata', () => { it('should retrieve presentation metadata', async () => { const mockPresentation = { diff --git a/workspace-server/src/index.ts b/workspace-server/src/index.ts index 852c976d..674d6783 100644 --- a/workspace-server/src/index.ts +++ b/workspace-server/src/index.ts @@ -371,7 +371,6 @@ async function main() { slidesService.getText, ); - server.registerTool( 'slides.getMetadata', { @@ -466,7 +465,6 @@ async function main() { sheetsService.getRange, ); - server.registerTool( 'sheets.getMetadata', { diff --git a/workspace-server/src/services/SheetsService.ts b/workspace-server/src/services/SheetsService.ts index 3785d98c..636f03ca 100644 --- a/workspace-server/src/services/SheetsService.ts +++ b/workspace-server/src/services/SheetsService.ts @@ -10,7 +10,6 @@ import { logToFile } from '../utils/logger'; import { extractDocId } from '../utils/IdUtils'; import { gaxiosOptions } from '../utils/GaxiosConfig'; - export class SheetsService { constructor(private authManager: AuthManager) {} @@ -20,7 +19,6 @@ export class SheetsService { return google.sheets({ version: 'v4', ...options }); } - public getText = async ({ spreadsheetId, format = 'text', @@ -196,7 +194,6 @@ export class SheetsService { } }; - public getMetadata = async ({ spreadsheetId }: { spreadsheetId: string }) => { logToFile( `[SheetsService] Starting getMetadata for spreadsheet: ${spreadsheetId}`, diff --git a/workspace-server/src/services/SlidesService.ts b/workspace-server/src/services/SlidesService.ts index 720f6146..b98eff72 100644 --- a/workspace-server/src/services/SlidesService.ts +++ b/workspace-server/src/services/SlidesService.ts @@ -13,7 +13,6 @@ import { logToFile } from '../utils/logger'; import { extractDocId } from '../utils/IdUtils'; import { gaxiosOptions } from '../utils/GaxiosConfig'; - export class SlidesService { constructor(private authManager: AuthManager) {} @@ -23,7 +22,6 @@ export class SlidesService { return google.slides({ version: 'v1', ...options }); } - public getText = async ({ presentationId }: { presentationId: string }) => { logToFile( `[SlidesService] Starting getText for presentation: ${presentationId}`, @@ -127,7 +125,6 @@ export class SlidesService { return text; } - public getMetadata = async ({ presentationId, }: {