Google Workspace Overview Interact with Google Drive, Gmail, Calendar, and Docs using OAuth authentication. Supports file uploads, folder management, email search, calendar search, and document operations. Quick Decision Tree Environment Setup OAuth credentials are stored locally after first authentication. Required Files - - From Google Cloud Console - - PyDrive2 configuration - - Auto-generated OAuth tokens First-Time Setup 1. Go to Google Cloud Console 2. Enable APIs: Drive, Gmail, Calendar, Docs 3. Create OAuth 2.0 credentials (Desktop app) 4. Download as 5. Run any script - browser opens…

)\nfor folder in all_folders:\n match = client_pattern.match(folder['title'])\n if match:\n number, name = match.groups()\n print(f\"Client #{number}: {name} - {folder['id']}\")\n```\n\n### Recursive Folder Navigation\n```python\ndef list_folder_recursive(drive, folder_id, indent=0):\n \"\"\"Recursively list folder contents.\"\"\"\n files = drive.ListFile({\n 'q': f\"'{folder_id}' in parents and trashed=false\"\n }).GetList()\n\n for file in files:\n prefix = \" \" * indent\n print(f\"{prefix}{file['title']}\")\n\n if file['mimeType'] == 'application/vnd.google-apps.folder':\n list_folder_recursive(drive, file['id'], indent + 1)\n\n# Usage\nlist_folder_recursive(drive, \"root_folder_id\")\n```\n\n### Query Syntax Reference\n```python\n# Common query patterns for ListFile\nqueries = {\n # By title\n \"exact_title\": \"title = 'Exact Name'\",\n \"contains\": \"title contains 'keyword'\",\n\n # By type\n \"folders_only\": \"mimeType = 'application/vnd.google-apps.folder'\",\n \"docs_only\": \"mimeType = 'application/vnd.google-apps.document'\",\n \"sheets_only\": \"mimeType = 'application/vnd.google-apps.spreadsheet'\",\n \"slides_only\": \"mimeType = 'application/vnd.google-apps.presentation'\",\n \"pdfs_only\": \"mimeType = 'application/pdf'\",\n\n # By location\n \"in_folder\": \"'folder_id' in parents\",\n \"in_root\": \"'root' in parents\",\n\n # By date\n \"modified_after\": \"modifiedDate > '2025-01-01T00:00:00'\",\n \"created_after\": \"createdDate > '2025-01-01T00:00:00'\",\n\n # Combined\n \"recent_docs\": \"mimeType = 'application/vnd.google-apps.document' and modifiedDate > '2025-01-01'\",\n \"folder_search\": \"title contains 'Client' and mimeType = 'application/vnd.google-apps.folder'\",\n}\n```\n\n### OAuth Troubleshooting\n\n1. **Token refresh fails**: Delete `mycreds.txt` and re-authenticate\n2. **settings.yaml missing**: Create from template with OAuth settings\n3. **Quota exceeded**: Wait 24h or use different project\n4. **\"Access denied\" error**: Ensure Google Drive API is enabled in Cloud Console\n5. **Credentials expired during long operation**: Wrap operations in try/except and refresh\n\n```python\n# Robust credential handling\ndef get_drive_client():\n gauth = GoogleAuth()\n gauth.LoadCredentialsFile(\"mycreds.txt\")\n\n if gauth.credentials is None:\n gauth.LocalWebserverAuth()\n elif gauth.access_token_expired:\n try:\n gauth.Refresh()\n except:\n gauth.LocalWebserverAuth()\n\n gauth.SaveCredentialsFile(\"mycreds.txt\")\n return GoogleDrive(gauth)\n```\n\n## API Used\nGoogle Drive API v2 via PyDrive2\n\n## Testing Checklist\n\n### Pre-flight\n- [ ] OAuth credentials file exists (`mycreds.txt` or `credentials.json`)\n- [ ] Google Drive API enabled in Google Cloud Console\n- [ ] Dependencies installed (`pip install pydrive2 python-dotenv`)\n- [ ] First-time OAuth flow completed (browser auth)\n\n### Smoke Test\n```bash\n# Search for a known client folder\npython scripts/gdrive_search.py folder \"Microsoft\"\n\n# Search for files in root\npython scripts/gdrive_search.py files \"report\" --modified-days 30\n\n# List contents of a known folder\npython scripts/gdrive_search.py list \"1abc123\" --recursive\n\n# Full client document search\npython scripts/gdrive_search.py client \"Kit\" --days 30\n```\n\n### Validation\n- [ ] Folder search returns valid `id` and `webViewLink`\n- [ ] Client folder pattern `[XX] Client Name` detected correctly\n- [ ] File search returns `title`, `mimeType`, `modifiedDate`\n- [ ] `--type` filter works (doc, sheet, slide, pdf)\n- [ ] `--modified-days` correctly filters recent files\n- [ ] `--recursive` lists nested folder contents\n- [ ] `webViewLink` URLs are accessible\n- [ ] OAuth token refreshes automatically when expired\n\n## Error Handling\n\n| Error | Cause | Resolution |\n|-------|-------|------------|\n| `Invalid credentials` | OAuth token expired or invalid | Delete `mycreds.txt`, re-authenticate |\n| `File not found` | File was deleted or moved | Verify file ID, search by name |\n| `Folder not found` | Folder doesn't exist or no access | Check folder ID and sharing permissions |\n| `403 Forbidden` | Insufficient permissions | Request access from file owner |\n| `404 Not Found` | Invalid file/folder ID | Verify ID format and existence |\n| `Quota exceeded` | API rate limit reached | Wait 1 minute, implement exponential backoff |\n| `Invalid query` | Malformed search query | Check query syntax, escape special characters |\n| `Shared drive access denied` | No access to shared drive | Request membership from drive admin |\n\n### Recovery Strategies\n\n1. **Automatic token refresh**: PyDrive2 handles token refresh automatically if `mycreds.txt` exists\n2. **Graceful degradation**: If specific file fails, continue with other search results\n3. **Retry with backoff**: Implement exponential backoff (1s, 2s, 4s) for quota errors\n4. **Cache folder IDs**: Cache frequently accessed folder IDs to reduce API calls\n5. **Pagination**: Use page tokens for large result sets to avoid timeouts\n\n## Performance Tips\n\n### Batch Operations\n- Use `batch_update` for multiple file operations\n- Group requests to reduce API calls\n- Max 100 operations per batch request\n\n### Caching\n- Cache folder IDs (they don't change)\n- Cache file metadata for repeated access\n- Use ETags for conditional requests\n\n### Query Optimization\n- Use specific fields in query (not '*')\n- Add `trashed=false` to exclude deleted items\n- Use `pageSize=100` for listing (max 1000)\n\n### Large File Handling\n- Use resumable uploads for files >5MB\n- Implement chunk upload with progress\n- Handle network interruptions gracefully\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":9038,"content_sha256":"4329028dcb267853722121912b05246cbe889ceeb9d569b4099a339c09dbe9d0"},{"filename":"references/drive-upload.md","content":"# Google Drive Upload\n\n## Overview\nUpload files to Google Drive with auto-folder creation and sharing.\n\n## Inputs\n\n| Parameter | Type | Default | Description |\n|-----------|------|---------|-------------|\n| `files` | list | required | Files to upload |\n| `folder` | string | required | Destination folder path |\n| `share` | string | - | \"anyone\" for public link |\n| `auto_create` | bool | true | Create folders if missing |\n\n## CLI Usage\n\n```bash\n# Upload single file\npython scripts/google_drive_upload.py --file report.pdf --folder \"Clients/Acme\"\n\n# Upload multiple files\npython scripts/google_drive_upload.py --files *.png --folder \"Clients/Acme/Assets\"\n\n# Upload with sharing\npython scripts/google_drive_upload.py --file doc.pdf --folder \"Reports\" --share anyone\n\n# Create folders only\npython scripts/google_drive_upload.py --create-folders \"Clients/NewClient/Assets\"\n```\n\n## Python Usage\n\n### Basic Setup\n```python\nfrom pydrive2.auth import GoogleAuth\nfrom pydrive2.drive import GoogleDrive\n\n# Initialize with saved credentials\ngauth = GoogleAuth()\ngauth.LoadCredentialsFile(\"mycreds.txt\")\nif gauth.credentials is None:\n gauth.LocalWebserverAuth()\nelif gauth.access_token_expired:\n gauth.Refresh()\ngauth.SaveCredentialsFile(\"mycreds.txt\")\n\ndrive = GoogleDrive(gauth)\n```\n\n### Upload a Single File\n```python\n# Create file in folder\nfolder_id = \"1abc123xyz\"\nfile = drive.CreateFile({\n 'title': 'thumbnail.png',\n 'parents': [{'id': folder_id}]\n})\nfile.SetContentFile('local_file.png')\nfile.Upload()\n\nprint(f\"Uploaded: {file['title']} - {file['id']}\")\nprint(f\"View: {file['alternateLink']}\")\n```\n\n### Upload with Auto-Detected MIME Type\n```python\nimport mimetypes\n\ndef upload_file(drive, local_path, folder_id, title=None):\n \"\"\"Upload file with automatic MIME type detection.\"\"\"\n mime_type, _ = mimetypes.guess_type(local_path)\n\n file = drive.CreateFile({\n 'title': title or local_path.split('/')[-1],\n 'parents': [{'id': folder_id}],\n 'mimeType': mime_type\n })\n file.SetContentFile(local_path)\n file.Upload()\n return file\n\n# Usage\nuploaded = upload_file(drive, 'report.pdf', folder_id)\n```\n\n### Upload Multiple Files\n```python\nfrom pathlib import Path\n\ndef upload_multiple(drive, file_paths, folder_id):\n \"\"\"Upload multiple files to a folder.\"\"\"\n uploaded_files = []\n for path in file_paths:\n file = drive.CreateFile({\n 'title': Path(path).name,\n 'parents': [{'id': folder_id}]\n })\n file.SetContentFile(str(path))\n file.Upload()\n uploaded_files.append({\n 'title': file['title'],\n 'id': file['id'],\n 'link': file['alternateLink']\n })\n return uploaded_files\n\n# Usage\nfiles = ['image1.png', 'image2.png', 'doc.pdf']\nresults = upload_multiple(drive, files, folder_id)\n```\n\n### Share File Publicly\n```python\n# Share file with anyone (view only)\nfile.InsertPermission({\n 'type': 'anyone',\n 'value': 'anyone',\n 'role': 'reader'\n})\npublic_link = file['alternateLink']\n\n# Share with edit access\nfile.InsertPermission({\n 'type': 'anyone',\n 'value': 'anyone',\n 'role': 'writer'\n})\n```\n\n### Share with Specific Users\n```python\n# Share with a specific email\nfile.InsertPermission({\n 'type': 'user',\n 'value': '[email protected]',\n 'role': 'writer' # or 'reader', 'commenter'\n})\n\n# Share with a domain\nfile.InsertPermission({\n 'type': 'domain',\n 'value': 'company.com',\n 'role': 'reader'\n})\n```\n\n### Upload and Convert to Google Format\n```python\n# Upload Word doc and convert to Google Doc\nfile = drive.CreateFile({\n 'title': 'Document',\n 'parents': [{'id': folder_id}],\n 'mimeType': 'application/vnd.google-apps.document' # Target format\n})\nfile.SetContentFile('document.docx')\nfile.Upload({'convert': True})\n\n# Conversion MIME types:\n# Word -> Google Doc: application/vnd.google-apps.document\n# Excel -> Google Sheet: application/vnd.google-apps.spreadsheet\n# PowerPoint -> Google Slides: application/vnd.google-apps.presentation\n```\n\n### Update Existing File\n```python\n# Update content of existing file\nexisting_file = drive.CreateFile({'id': 'existing_file_id'})\nexisting_file.SetContentFile('updated_content.pdf')\nexisting_file.Upload()\n```\n\n### Resumable Upload for Large Files\n```python\n# For files > 5MB, PyDrive2 automatically uses resumable upload\n# To explicitly control chunk size:\nfrom pydrive2.files import GoogleDriveFile\n\nfile = drive.CreateFile({\n 'title': 'large_video.mp4',\n 'parents': [{'id': folder_id}]\n})\nfile.SetContentFile('large_video.mp4')\nfile.Upload(param={'uploadType': 'resumable'})\n```\n\n### OAuth Troubleshooting\n\n1. **Token refresh fails**: Delete `mycreds.txt` and re-authenticate\n2. **settings.yaml missing**: Create from template with OAuth settings\n3. **Quota exceeded**: Wait 24h or use different project\n4. **Upload fails mid-way**: Use resumable upload for automatic retry\n5. **Permission denied on folder**: Verify you have edit access to target folder\n\n```python\n# Robust upload with error handling\ndef safe_upload(drive, local_path, folder_id, max_retries=3):\n \"\"\"Upload with retry logic.\"\"\"\n import time\n\n for attempt in range(max_retries):\n try:\n file = drive.CreateFile({\n 'title': Path(local_path).name,\n 'parents': [{'id': folder_id}]\n })\n file.SetContentFile(str(local_path))\n file.Upload()\n return file\n except Exception as e:\n if attempt \u003c max_retries - 1:\n time.sleep(2 ** attempt) # Exponential backoff\n else:\n raise e\n```\n\n## Output Structure\n\n```json\n{\n \"file_id\": \"1abc123\",\n \"web_view_link\": \"https://drive.google.com/file/d/...\",\n \"folder_ids\": [\"parent_id\", \"child_id\"]\n}\n```\n\n## Rate Limits\n- 1000 requests per 100 seconds per user\n- Resumable uploads for files > 5MB\n\n## Testing Checklist\n\n### Pre-flight\n- [ ] OAuth credentials file exists (`mycreds.txt` or `credentials.json`)\n- [ ] Google Drive API enabled in Google Cloud Console\n- [ ] Dependencies installed (`pip install pydrive2 python-dotenv`)\n- [ ] First-time OAuth flow completed (browser auth)\n- [ ] Test file exists locally for upload\n\n### Smoke Test\n```bash\n# Upload a test file\npython scripts/google_drive_upload.py --file test.txt --folder \"Test\"\n\n# Upload with sharing\npython scripts/google_drive_upload.py --file test.pdf --folder \"Test/Public\" --share anyone\n\n# Create folders only (no file upload)\npython scripts/google_drive_upload.py --create-folders \"Test/NewFolder/SubFolder\"\n```\n\n### Validation\n- [ ] Response contains `file_id` and `web_view_link`\n- [ ] File appears in correct Drive folder\n- [ ] `web_view_link` is accessible\n- [ ] `--share anyone` creates public link\n- [ ] `--auto-create` creates missing parent folders\n- [ ] Multiple file upload works (`--files *.png`)\n- [ ] Large files (>5MB) use resumable upload\n- [ ] File permissions match requested sharing settings\n- [ ] Duplicate uploads update existing files (or create new versions)\n\n## Error Handling\n\n| Error | Cause | Resolution |\n|-------|-------|------------|\n| `Invalid credentials` | OAuth token expired or invalid | Delete `mycreds.txt`, re-authenticate |\n| `File not found` (local) | Source file doesn't exist | Verify file path before upload |\n| `Folder not found` | Target folder doesn't exist | Use `--auto-create` or create folder first |\n| `403 Forbidden` | No write access to folder | Request edit access from folder owner |\n| `413 Request Entity Too Large` | File exceeds 5TB limit | Split file or use alternative storage |\n| `Quota exceeded` | Storage quota full or API rate limited | Free up space or wait for rate limit reset |\n| `Upload interrupted` | Network failure during upload | Resumable upload auto-retries, or restart |\n| `Invalid MIME type` | Unsupported file format | Check file extension, convert if needed |\n| `Sharing failed` | Cannot share to specified users | Verify email addresses and domain policies |\n\n### Recovery Strategies\n\n1. **Resumable uploads**: Use resumable upload for files >5MB to handle network interruptions\n2. **Retry with backoff**: Implement exponential backoff (1s, 2s, 4s) for transient failures\n3. **Pre-flight validation**: Verify local file exists and folder access before upload\n4. **Batch uploads**: Process multiple files in chunks to avoid timeouts\n5. **Progress tracking**: Log upload progress for large files to enable recovery\n\n## Performance Tips\n\n### Batch Operations\n- Use `batch_update` for multiple file operations\n- Group requests to reduce API calls\n- Max 100 operations per batch request\n\n### Caching\n- Cache folder IDs (they don't change)\n- Cache file metadata for repeated access\n- Use ETags for conditional requests\n\n### Query Optimization\n- Use specific fields in query (not '*')\n- Add `trashed=false` to exclude deleted items\n- Use `pageSize=100` for listing (max 1000)\n\n### Large File Handling\n- Use resumable uploads for files >5MB\n- Implement chunk upload with progress\n- Handle network interruptions gracefully\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":9066,"content_sha256":"ffd103b19c8eb42a3fbd9523ca60623604fee0e175ee7c6cec93324ec82ddb8f"},{"filename":"references/folder-structure.md","content":"# Google Drive Folder Structure\n\n## Overview\nAuto-generate nested folder structures in Google Drive.\n\n## Inputs\n\n| Parameter | Type | Required | Description |\n|-----------|------|----------|-------------|\n| `structure` | dict/JSON | Yes | Folder hierarchy |\n| `parent_id` | string | No | Parent folder ID |\n| `parent_path` | string | No | Parent folder path |\n\n## CLI Usage\n\n```bash\n# Using JSON structure\npython scripts/gdrive_folder_structure.py --structure '{\n \"Project\": {\n \"Assets\": {},\n \"Deliverables\": {}\n }\n}'\n\n# Using structure file\npython scripts/gdrive_folder_structure.py --file structure.json\n\n# Create in specific parent\npython scripts/gdrive_folder_structure.py --structure '{\"Project\": {}}' --parent-path \"Clients/Acme\"\n```\n\n## Structure Format\n\n```json\n{\n \"Project Alpha\": {\n \"Assets\": {\n \"Images\": {},\n \"Videos\": {}\n },\n \"Deliverables\": {\n \"Draft\": {},\n \"Final\": {}\n }\n }\n}\n```\n\n## Common Templates\n\n### Client Folder\n```json\n{\n \"Client Name\": {\n \"Assets\": {},\n \"Deliverables\": {},\n \"Reports\": {},\n \"Communications\": {}\n }\n}\n```\n\n### Project Folder\n```json\n{\n \"Project Name\": {\n \"01_Research\": {},\n \"02_Design\": {},\n \"03_Development\": {},\n \"04_Testing\": {},\n \"05_Delivery\": {}\n }\n}\n```\n\n## Output Structure\n\n```json\n{\n \"root_folder_id\": \"1abc123xyz\",\n \"root_folder_url\": \"https://drive.google.com/drive/folders/1abc123xyz\",\n \"created_folders\": [\n {\"path\": \"Project\", \"id\": \"1abc123\"},\n {\"path\": \"Project/Assets\", \"id\": \"2def456\"}\n ]\n}\n```\n\n## Behavior\n- Idempotent: skips folders that already exist\n- Recursive: creates full hierarchy\n\n## Python Usage\n\n### Basic Setup\n```python\nfrom pydrive2.auth import GoogleAuth\nfrom pydrive2.drive import GoogleDrive\n\n# Initialize with saved credentials\ngauth = GoogleAuth()\ngauth.LoadCredentialsFile(\"mycreds.txt\")\nif gauth.credentials is None:\n gauth.LocalWebserverAuth()\nelif gauth.access_token_expired:\n gauth.Refresh()\ngauth.SaveCredentialsFile(\"mycreds.txt\")\n\ndrive = GoogleDrive(gauth)\n```\n\n### List Folder Contents Recursively\n```python\ndef get_folder_tree(drive, folder_id, indent=0):\n \"\"\"Get folder tree as nested structure.\"\"\"\n files = drive.ListFile({\n 'q': f\"'{folder_id}' in parents and trashed=false\"\n }).GetList()\n\n tree = []\n for file in files:\n item = {\n 'title': file['title'],\n 'id': file['id'],\n 'mimeType': file['mimeType'],\n 'isFolder': file['mimeType'] == 'application/vnd.google-apps.folder'\n }\n\n if item['isFolder']:\n item['children'] = get_folder_tree(drive, file['id'], indent + 1)\n\n tree.append(item)\n\n return tree\n\n# Usage\ntree = get_folder_tree(drive, \"root_folder_id\")\n```\n\n### Print Folder Tree as Text\n```python\ndef print_folder_tree(drive, folder_id, prefix=\"\", is_last=True):\n \"\"\"Print folder tree with visual structure.\"\"\"\n files = drive.ListFile({\n 'q': f\"'{folder_id}' in parents and trashed=false\",\n 'orderBy': 'folder,title'\n }).GetList()\n\n for i, file in enumerate(files):\n is_final = (i == len(files) - 1)\n connector = \"└── \" if is_final else \"├── \"\n print(f\"{prefix}{connector}{file['title']}\")\n\n if file['mimeType'] == 'application/vnd.google-apps.folder':\n extension = \" \" if is_final else \"│ \"\n print_folder_tree(drive, file['id'], prefix + extension, is_final)\n\n# Usage\nprint(\"Root/\")\nprint_folder_tree(drive, \"folder_id\")\n```\n\n### Build Path-to-ID Mapping\n```python\ndef build_folder_map(drive, root_id, base_path=\"\"):\n \"\"\"Create mapping of folder paths to IDs.\"\"\"\n folder_map = {}\n\n files = drive.ListFile({\n 'q': f\"'{root_id}' in parents and mimeType='application/vnd.google-apps.folder' and trashed=false\"\n }).GetList()\n\n for folder in files:\n path = f\"{base_path}/{folder['title']}\" if base_path else folder['title']\n folder_map[path] = folder['id']\n\n # Recurse into subfolders\n subfolder_map = build_folder_map(drive, folder['id'], path)\n folder_map.update(subfolder_map)\n\n return folder_map\n\n# Usage\nfolder_map = build_folder_map(drive, \"root_folder_id\")\n# Result: {\"Assets\": \"id1\", \"Assets/Images\": \"id2\", \"Assets/Videos\": \"id3\"}\n```\n\n### Create Folder Structure from Dict\n```python\ndef create_folder_structure(drive, structure, parent_id=None):\n \"\"\"Recursively create folder structure from dict.\"\"\"\n created = []\n\n for folder_name, substructure in structure.items():\n # Check if folder exists\n query = f\"title='{folder_name}' and mimeType='application/vnd.google-apps.folder' and trashed=false\"\n if parent_id:\n query += f\" and '{parent_id}' in parents\"\n\n existing = drive.ListFile({'q': query}).GetList()\n\n if existing:\n folder = existing[0]\n else:\n # Create new folder\n folder_metadata = {\n 'title': folder_name,\n 'mimeType': 'application/vnd.google-apps.folder'\n }\n if parent_id:\n folder_metadata['parents'] = [{'id': parent_id}]\n\n folder = drive.CreateFile(folder_metadata)\n folder.Upload()\n\n created.append({\n 'title': folder_name,\n 'id': folder['id'],\n 'link': folder.get('alternateLink', '')\n })\n\n # Recurse into subfolders\n if substructure:\n sub_created = create_folder_structure(drive, substructure, folder['id'])\n created.extend(sub_created)\n\n return created\n\n# Usage\nstructure = {\n \"Project Alpha\": {\n \"Assets\": {\n \"Images\": {},\n \"Videos\": {}\n },\n \"Deliverables\": {\n \"Draft\": {},\n \"Final\": {}\n }\n }\n}\ncreated = create_folder_structure(drive, structure, parent_folder_id)\n```\n\n### Find or Create Folder Path\n```python\ndef ensure_folder_path(drive, path, root_id=None):\n \"\"\"Ensure folder path exists, creating if needed. Returns final folder ID.\"\"\"\n parts = path.strip('/').split('/')\n current_parent = root_id or 'root'\n\n for part in parts:\n # Search for existing folder\n query = f\"title='{part}' and mimeType='application/vnd.google-apps.folder' and trashed=false and '{current_parent}' in parents\"\n results = drive.ListFile({'q': query}).GetList()\n\n if results:\n current_parent = results[0]['id']\n else:\n # Create folder\n folder = drive.CreateFile({\n 'title': part,\n 'mimeType': 'application/vnd.google-apps.folder',\n 'parents': [{'id': current_parent}]\n })\n folder.Upload()\n current_parent = folder['id']\n\n return current_parent\n\n# Usage\nfolder_id = ensure_folder_path(drive, \"Clients/Acme/Assets/Images\")\n```\n\n### Get Folder Statistics\n```python\ndef get_folder_stats(drive, folder_id):\n \"\"\"Get statistics for a folder and its contents.\"\"\"\n stats = {\n 'total_files': 0,\n 'total_folders': 0,\n 'by_type': {},\n 'total_size': 0\n }\n\n def count_recursive(fid):\n files = drive.ListFile({\n 'q': f\"'{fid}' in parents and trashed=false\"\n }).GetList()\n\n for file in files:\n if file['mimeType'] == 'application/vnd.google-apps.folder':\n stats['total_folders'] += 1\n count_recursive(file['id'])\n else:\n stats['total_files'] += 1\n mime = file['mimeType']\n stats['by_type'][mime] = stats['by_type'].get(mime, 0) + 1\n if 'fileSize' in file:\n stats['total_size'] += int(file['fileSize'])\n\n count_recursive(folder_id)\n return stats\n\n# Usage\nstats = get_folder_stats(drive, \"folder_id\")\nprint(f\"Files: {stats['total_files']}, Folders: {stats['total_folders']}\")\n```\n\n### OAuth Troubleshooting\n\n1. **Token refresh fails**: Delete `mycreds.txt` and re-authenticate\n2. **settings.yaml missing**: Create from template with OAuth settings\n3. **Quota exceeded**: Wait 24h or use different project\n4. **Folder creation fails**: Check parent folder permissions\n5. **Duplicate folders**: Use idempotent creation (check before create)\n\n## Testing Checklist\n\n### Pre-flight\n- [ ] OAuth credentials file exists (`mycreds.txt` or `credentials.json`)\n- [ ] Google Drive API enabled in Google Cloud Console\n- [ ] Dependencies installed (`pip install pydrive2 python-dotenv`)\n- [ ] First-time OAuth flow completed (browser auth)\n\n### Smoke Test\n```bash\n# Create a simple test structure\npython scripts/gdrive_folder_structure.py --structure '{\"Test-$(date +%s)\": {\"SubA\": {}, \"SubB\": {}}}'\n\n# Create from JSON file\npython scripts/gdrive_folder_structure.py --file test_structure.json\n\n# Create in a specific parent folder\npython scripts/gdrive_folder_structure.py --structure '{\"NewProject\": {}}' --parent-path \"Clients/TestClient\"\n```\n\n### Validation\n- [ ] Response contains `root_folder_id` and `root_folder_url`\n- [ ] `created_folders` array lists all created folders with paths and IDs\n- [ ] Nested folders are created correctly\n- [ ] `root_folder_url` is accessible in browser\n- [ ] Idempotent: re-running skips existing folders\n- [ ] `--parent-path` correctly finds and uses parent folder\n- [ ] Empty `{}` creates folder without subfolders\n- [ ] Invalid JSON structure returns meaningful error\n\n## Error Handling\n\n| Error | Cause | Resolution |\n|-------|-------|------------|\n| `Invalid credentials` | OAuth token expired or invalid | Delete `mycreds.txt`, re-authenticate |\n| `Parent folder not found` | Parent path doesn't exist | Verify parent path or create parent first |\n| `403 Forbidden` | No write access to location | Request edit access from folder owner |\n| `Invalid JSON` | Malformed structure definition | Validate JSON syntax before running |\n| `Quota exceeded` | API rate limit reached | Wait 1 minute, reduce batch size |\n| `Folder name too long` | Name exceeds 255 characters | Shorten folder names |\n| `Invalid characters` | Folder name contains forbidden chars | Remove `/`, `\\`, or other special characters |\n| `Duplicate folder` | Folder already exists (not idempotent) | Script should skip - verify idempotent behavior |\n\n### Recovery Strategies\n\n1. **Idempotent design**: Check for existing folders before creating to support retry\n2. **Atomic operations**: Create folders in order so partial failures can resume\n3. **Validation first**: Validate JSON structure and folder names before API calls\n4. **Progress logging**: Log each folder creation to enable recovery from partial failure\n5. **Rollback support**: Track created folders to enable cleanup on critical failure\n\n## Performance Tips\n\n### Batch Operations\n- Use `batch_update` for multiple file operations\n- Group requests to reduce API calls\n- Max 100 operations per batch request\n\n### Caching\n- Cache folder IDs (they don't change)\n- Cache file metadata for repeated access\n- Use ETags for conditional requests\n\n### Query Optimization\n- Use specific fields in query (not '*')\n- Add `trashed=false` to exclude deleted items\n- Use `pageSize=100` for listing (max 1000)\n\n### Large File Handling\n- Use resumable uploads for files >5MB\n- Implement chunk upload with progress\n- Handle network interruptions gracefully\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":11359,"content_sha256":"cb6c959c81cb04576525c3f78007488e6419e8391394fb09ef5720d2daed91d3"},{"filename":"references/gmail-search.md","content":"# Gmail Search\n\n## Overview\nSearch Gmail for emails with/about clients. Read-only operations.\n\n## Inputs\n\n| Parameter | Type | Required | Description |\n|-----------|------|----------|-------------|\n| `query` | string | No | Free text search |\n| `domain` | string | No | Filter by email domain |\n| `days` | int | No | Days back (default: 14) |\n| `internal_only` | bool | No | Only internal emails |\n| `max_results` | int | No | Max messages (default: 50) |\n| `thread_id` | string | No | Get specific thread |\n\n## CLI Usage\n\n```bash\n# Search by domain\npython scripts/gmail_search.py --domain \"microsoft.com\" --days 14\n\n# Search by keyword\npython scripts/gmail_search.py --query \"proposal\" --days 30\n\n# Internal emails mentioning client\npython scripts/gmail_search.py --query \"Microsoft\" --internal-only --days 14\n\n# Get specific thread\npython scripts/gmail_search.py --thread \"thread123\"\n\n# Output as JSON\npython scripts/gmail_search.py --domain \"kit.com\" --json\n```\n\n## Gmail Query Syntax\n\n- `from:@domain.com` - Emails from domain\n- `to:@domain.com` - Emails to domain\n- `newer_than:14d` - Last 14 days\n- `subject:proposal` - Subject contains word\n- `has:attachment` - Has attachments\n\n## Output Structure\n\n```json\n{\n \"total\": 15,\n \"external_threads\": [\n {\n \"id\": \"msg123\",\n \"thread_id\": \"thread456\",\n \"subject\": \"Re: Proposal Review\",\n \"from\": \"John \[email protected]>\",\n \"to\": \"[email protected]\",\n \"date\": \"2025-12-25T10:30:00\",\n \"snippet\": \"Thanks for sending over...\"\n }\n ],\n \"internal_mentions\": [...],\n \"external_count\": 10,\n \"internal_count\": 5\n}\n```\n\n## First-Time Setup\n1. Enable Gmail API in Google Cloud Console\n2. Run script - browser opens for OAuth consent\n3. Credentials saved to `gmail_token.pickle`\n\n## Python Usage\n\n### Basic Setup\n```python\nimport os\nimport pickle\nfrom google.auth.transport.requests import Request\nfrom google_auth_oauthlib.flow import InstalledAppFlow\nfrom googleapiclient.discovery import build\n\nSCOPES = ['https://www.googleapis.com/auth/gmail.readonly']\n\ndef get_gmail_service():\n \"\"\"Initialize Gmail API service with OAuth.\"\"\"\n creds = None\n\n # Load saved credentials\n if os.path.exists('gmail_token.pickle'):\n with open('gmail_token.pickle', 'rb') as token:\n creds = pickle.load(token)\n\n # Refresh or create credentials\n if not creds or not creds.valid:\n if creds and creds.expired and creds.refresh_token:\n creds.refresh(Request())\n else:\n flow = InstalledAppFlow.from_client_secrets_file(\n 'credentials.json', SCOPES\n )\n creds = flow.run_local_server(port=0)\n\n # Save credentials\n with open('gmail_token.pickle', 'wb') as token:\n pickle.dump(creds, token)\n\n return build('gmail', 'v1', credentials=creds)\n\nservice = get_gmail_service()\n```\n\n### Search Messages by Query\n```python\ndef search_messages(service, query, max_results=50):\n \"\"\"Search Gmail messages with query string.\"\"\"\n results = service.users().messages().list(\n userId='me',\n q=query,\n maxResults=max_results\n ).execute()\n\n messages = results.get('messages', [])\n return messages\n\n# Usage\nmessages = search_messages(service, \"from:@microsoft.com newer_than:14d\")\nprint(f\"Found {len(messages)} messages\")\n```\n\n### Get Full Message Details\n```python\ndef get_message_details(service, message_id):\n \"\"\"Get full details of a message.\"\"\"\n message = service.users().messages().get(\n userId='me',\n id=message_id,\n format='full'\n ).execute()\n\n # Extract headers\n headers = {h['name']: h['value'] for h in message['payload']['headers']}\n\n return {\n 'id': message['id'],\n 'thread_id': message['threadId'],\n 'subject': headers.get('Subject', ''),\n 'from': headers.get('From', ''),\n 'to': headers.get('To', ''),\n 'date': headers.get('Date', ''),\n 'snippet': message.get('snippet', '')\n }\n\n# Usage\nfor msg in messages[:5]:\n details = get_message_details(service, msg['id'])\n print(f\"{details['date']}: {details['subject']}\")\n```\n\n### Search by Domain\n```python\ndef search_by_domain(service, domain, days=14):\n \"\"\"Search for emails from/to a specific domain.\"\"\"\n query = f\"(from:@{domain} OR to:@{domain}) newer_than:{days}d\"\n\n results = service.users().messages().list(\n userId='me',\n q=query,\n maxResults=100\n ).execute()\n\n messages = results.get('messages', [])\n\n detailed = []\n for msg in messages:\n details = get_message_details(service, msg['id'])\n detailed.append(details)\n\n return detailed\n\n# Usage\nemails = search_by_domain(service, \"microsoft.com\", days=30)\n```\n\n### Search Internal vs External\n```python\ndef search_client_mentions(service, client_name, internal_domain, days=14):\n \"\"\"Separate internal and external emails mentioning client.\"\"\"\n query = f\"{client_name} newer_than:{days}d\"\n\n results = service.users().messages().list(\n userId='me',\n q=query,\n maxResults=100\n ).execute()\n\n messages = results.get('messages', [])\n\n external_threads = []\n internal_mentions = []\n\n for msg in messages:\n details = get_message_details(service, msg['id'])\n\n # Check if external\n is_external = internal_domain not in details['from']\n\n if is_external:\n external_threads.append(details)\n else:\n internal_mentions.append(details)\n\n return {\n 'external_threads': external_threads,\n 'internal_mentions': internal_mentions,\n 'external_count': len(external_threads),\n 'internal_count': len(internal_mentions)\n }\n\n# Usage\nresults = search_client_mentions(service, \"Microsoft\", \"yourcompany.com\", days=14)\n```\n\n### Get Thread with All Messages\n```python\ndef get_thread(service, thread_id):\n \"\"\"Get all messages in a thread.\"\"\"\n thread = service.users().threads().get(\n userId='me',\n id=thread_id,\n format='full'\n ).execute()\n\n messages = []\n for msg in thread.get('messages', []):\n headers = {h['name']: h['value'] for h in msg['payload']['headers']}\n messages.append({\n 'id': msg['id'],\n 'from': headers.get('From', ''),\n 'date': headers.get('Date', ''),\n 'snippet': msg.get('snippet', '')\n })\n\n return {\n 'thread_id': thread_id,\n 'messages': messages,\n 'message_count': len(messages)\n }\n\n# Usage\nthread = get_thread(service, \"thread_id_here\")\nfor msg in thread['messages']:\n print(f\" {msg['from']}: {msg['snippet'][:50]}...\")\n```\n\n### Gmail Query Syntax Examples\n```python\n# Common query patterns\nqueries = {\n # By sender/recipient\n \"from_domain\": \"from:@example.com\",\n \"to_domain\": \"to:@example.com\",\n \"from_or_to\": \"(from:@example.com OR to:@example.com)\",\n\n # By date\n \"last_7_days\": \"newer_than:7d\",\n \"last_month\": \"newer_than:30d\",\n \"date_range\": \"after:2025/01/01 before:2025/02/01\",\n\n # By content\n \"subject\": \"subject:proposal\",\n \"has_attachment\": \"has:attachment\",\n \"has_pdf\": \"filename:pdf\",\n\n # Combined\n \"client_recent\": \"from:@client.com newer_than:14d has:attachment\",\n \"internal_mentions\": \"to:@ourcompany.com subject:ClientName newer_than:7d\",\n}\n```\n\n### Pagination for Large Result Sets\n```python\ndef search_all_messages(service, query):\n \"\"\"Search with pagination for all matching messages.\"\"\"\n all_messages = []\n page_token = None\n\n while True:\n results = service.users().messages().list(\n userId='me',\n q=query,\n maxResults=100,\n pageToken=page_token\n ).execute()\n\n messages = results.get('messages', [])\n all_messages.extend(messages)\n\n page_token = results.get('nextPageToken')\n if not page_token:\n break\n\n return all_messages\n\n# Usage\nall_emails = search_all_messages(service, \"from:@important.com\")\n```\n\n### OAuth Troubleshooting\n\n1. **Token refresh fails**: Delete `gmail_token.pickle` and re-authenticate\n2. **credentials.json missing**: Download from Google Cloud Console (OAuth 2.0 Client ID)\n3. **Quota exceeded**: Gmail API allows 250 quota units/second; implement backoff\n4. **\"Access denied\" error**: Ensure Gmail API is enabled in Cloud Console\n5. **Insufficient scopes**: Re-authenticate with correct scopes (`gmail.readonly`)\n\n```python\n# Robust error handling\nimport time\n\ndef safe_api_call(func, *args, max_retries=3, **kwargs):\n \"\"\"Execute API call with retry logic.\"\"\"\n for attempt in range(max_retries):\n try:\n return func(*args, **kwargs)\n except Exception as e:\n if \"quota\" in str(e).lower() or \"rate\" in str(e).lower():\n time.sleep(2 ** attempt)\n else:\n raise\n raise Exception(\"Max retries exceeded\")\n```\n\n## Testing Checklist\n\n### Pre-flight\n- [ ] Gmail API enabled in Google Cloud Console\n- [ ] OAuth credentials file exists (`credentials.json`)\n- [ ] `gmail_token.pickle` exists (or will be created on first run)\n- [ ] Dependencies installed (`pip install google-auth google-auth-oauthlib google-api-python-client`)\n\n### Smoke Test\n```bash\n# Search by keyword in last 7 days\npython scripts/gmail_search.py --query \"test\" --days 7\n\n# Search by domain\npython scripts/gmail_search.py --domain \"gmail.com\" --days 7\n\n# Internal emails only\npython scripts/gmail_search.py --query \"project\" --internal-only --days 14\n\n# Output as JSON\npython scripts/gmail_search.py --domain \"example.com\" --json\n```\n\n### Validation\n- [ ] Response contains `total`, `external_threads`, `internal_mentions`\n- [ ] Email objects have `id`, `thread_id`, `subject`, `from`, `to`, `date`, `snippet`\n- [ ] `--domain` filter correctly matches sender/recipient domains\n- [ ] `--internal-only` excludes external emails\n- [ ] `--days` correctly filters by date range\n- [ ] `--thread` retrieves specific thread with all messages\n- [ ] Gmail query syntax works (`from:`, `to:`, `subject:`, `has:attachment`)\n- [ ] OAuth token refreshes automatically when expired\n- [ ] Rate limits respected (avoid hammering API)\n\n## Error Handling\n\n| Error | Cause | Resolution |\n|-------|-------|------------|\n| `Invalid credentials` | OAuth token expired or invalid | Delete `gmail_token.pickle`, re-authenticate |\n| `403 Forbidden` | Gmail API not enabled or no access | Enable Gmail API in Google Cloud Console |\n| `404 Not Found` | Thread or message ID doesn't exist | Verify ID, message may have been deleted |\n| `Quota exceeded` | API rate limit (250 units/second) | Wait 1 second, implement exponential backoff |\n| `Invalid query` | Malformed Gmail search query | Check query syntax, escape special characters |\n| `User rate limit exceeded` | Too many requests per user | Reduce request frequency, batch requests |\n| `Backend error` | Gmail service temporarily unavailable | Retry after 30 seconds |\n| `Insufficient scopes` | Missing required OAuth scopes | Re-authenticate with `gmail.readonly` scope |\n\n### Recovery Strategies\n\n1. **Automatic token refresh**: Google client library handles token refresh automatically\n2. **Retry with backoff**: Implement exponential backoff (1s, 2s, 4s) for rate limits\n3. **Batch requests**: Use Gmail batch API for multiple message fetches\n4. **Pagination**: Use `nextPageToken` for large result sets\n5. **Query validation**: Validate query syntax before sending to API\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":11490,"content_sha256":"531148911b7a99c40740c406baf6ba87abfaf80521f6e8b172cfc2f49a6816f6"},{"filename":"references/transcript-search.md","content":"# Google Drive Transcript Search\n\n## Overview\nSearch Google Drive for meeting transcript files (uploaded from Fireflies or other sources).\n\n## Inputs\n\n| Parameter | Type | Required | Description |\n|-----------|------|----------|-------------|\n| `client_name` | string | Yes | Client name to search |\n| `days` | int | No | Days back to search (default: 90) |\n| `keywords` | list | No | Additional keywords |\n\n## CLI Usage\n\n```bash\n# Search client transcripts\npython scripts/gdrive_transcript_search.py \"Microsoft\" --days 30\n\n# With keywords\npython scripts/gdrive_transcript_search.py \"Acme\" --keywords \"discovery\" \"proposal\"\n```\n\n## Output Structure\n\n```json\n{\n \"client\": \"Microsoft\",\n \"transcripts\": [\n {\n \"id\": \"1abc123\",\n \"title\": \"Microsoft - Discovery Call - 2025-12-20\",\n \"webViewLink\": \"https://docs.google.com/document/d/...\",\n \"modifiedDate\": \"2025-12-20\",\n \"folder\": \"[2] Discovery/[3] Meeting Transcripts\"\n }\n ],\n \"count\": 3\n}\n```\n\n## Search Logic\n\n1. Find client folder by name pattern\n2. Navigate to Discovery > Meeting Transcripts\n3. List all documents in folder\n4. Filter by date and keywords\n5. Return sorted by date (newest first)\n\n## Related\n- `fireflies_transcript_search.md` - Search Fireflies API directly\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1261,"content_sha256":"3cdceceb0f7e18ab0a271e43f732401b4a545900e10825d0ca7d465332c4d61a"},{"filename":"scripts/create_client_folder.py","content":"#!/usr/bin/env python3\n\"\"\"\nCreate Client Folder Structure in Google Drive\n\nCreates a numbered client folder in the Casper Studios shared drive with\nthe standard folder structure. Replicates the n8n workflow.\n\nStructure created:\n [XX] {Client Name}/\n ├── [1] Admin/\n └── [2] Discovery/\n ├── [1] Reference/Data/\n ├── [2] Interviews/\n │ ├── [1] Leads/\n │ └── [2] Team/\n ├── [3] Meeting Transcripts/\n └── [4] Functional Read Out/\n\nDirective: directives/create_client_folder.md\n\nUsage:\n # Create folder structure for a new client\n python execution/create_client_folder.py \"Microsoft\"\n\n # Dry run to see what would be created\n python execution/create_client_folder.py \"Acme Corp\" --dry-run\n\"\"\"\n\nimport os\nimport re\nimport sys\nimport argparse\nimport json\nfrom pathlib import Path\nfrom pydrive2.auth import GoogleAuth\nfrom pydrive2.drive import GoogleDrive\n\n# Configuration\nSETTINGS_FILE = \"settings.yaml\"\nCREDENTIALS_FILE = \"mycreds.txt\"\nCLIENT_SECRETS_FILE = \"client_secrets.json\"\n\n# Shared drive configuration (set via env vars or .env)\nSHARED_DRIVE_ID = os.getenv(\"SHARED_DRIVE_ID\", \"\")\nSHARED_DRIVE_NAME = os.getenv(\"SHARED_DRIVE_NAME\", \"Client Projects\")\n\n# Standard client folder structure\nCLIENT_FOLDER_STRUCTURE = {\n \"[1] Admin\": {},\n \"[2] Discovery\": {\n \"[1] Reference/Data\": {},\n \"[2] Interviews\": {\n \"[1] Leads\": {},\n \"[2] Team\": {}\n },\n \"[3] Meeting Transcripts\": {},\n \"[4] Functional Read Out\": {}\n }\n}\n\n\nclass ClientFolderError(Exception):\n \"\"\"Custom exception for client folder operations.\"\"\"\n pass\n\n\ndef validate_setup():\n \"\"\"Validate required files exist.\"\"\"\n if not Path(CLIENT_SECRETS_FILE).exists():\n raise FileNotFoundError(\n f\"{CLIENT_SECRETS_FILE} not found!\\n\\n\"\n \"Setup instructions:\\n\"\n \"1. Go to https://console.cloud.google.com\\n\"\n \"2. Enable Google Drive API\\n\"\n \"3. Create OAuth 2.0 credentials (Desktop app)\\n\"\n \"4. Download JSON and save as 'client_secrets.json'\\n\"\n )\n\n if not Path(SETTINGS_FILE).exists():\n raise FileNotFoundError(f\"{SETTINGS_FILE} not found! This should be in project root.\")\n\n\ndef authenticate() -> GoogleDrive:\n \"\"\"\n Authenticate with Google Drive using OAuth 2.0.\n\n Returns:\n GoogleDrive: Authenticated drive instance\n \"\"\"\n print(\"Authenticating with Google Drive...\")\n\n gauth = GoogleAuth()\n gauth.LoadCredentialsFile(CREDENTIALS_FILE)\n\n if gauth.credentials is None:\n print(\" First time setup - opening browser for authentication...\")\n gauth.LocalWebserverAuth()\n elif gauth.access_token_expired:\n print(\" Refreshing expired credentials...\")\n gauth.Refresh()\n else:\n print(\" Using saved credentials...\")\n gauth.Authorize()\n\n gauth.SaveCredentialsFile(CREDENTIALS_FILE)\n print(\" Authentication successful\")\n\n return GoogleDrive(gauth)\n\n\ndef get_next_folder_number(drive: GoogleDrive) -> int:\n \"\"\"\n Find the highest numbered folder in the shared drive and return next number.\n\n Folders are named like \"[15] Client Name\". We find the highest number\n and increment by 1.\n\n Args:\n drive: Authenticated GoogleDrive instance\n\n Returns:\n int: Next folder number to use\n \"\"\"\n print(f\"Scanning {SHARED_DRIVE_NAME} for existing folders...\")\n\n # Query folders in the shared drive root\n # For shared drives, we use driveId and corpora='drive'\n query = (\n f\"'{SHARED_DRIVE_ID}' in parents and \"\n \"mimeType='application/vnd.google-apps.folder' and \"\n \"trashed=false\"\n )\n\n file_list = drive.ListFile({\n 'q': query,\n 'supportsAllDrives': True,\n 'includeItemsFromAllDrives': True,\n 'corpora': 'drive',\n 'driveId': SHARED_DRIVE_ID\n }).GetList()\n\n # Find folders matching pattern [XX]\n pattern = re.compile(r'^\\[(\\d+)\\]')\n max_number = 0\n\n for folder in file_list:\n match = pattern.match(folder['title'])\n if match:\n num = int(match.group(1))\n if num > max_number:\n max_number = num\n\n next_number = max_number + 1\n print(f\" Found highest number: [{max_number:02d}]\")\n print(f\" Next number will be: [{next_number:02d}]\")\n\n return next_number\n\n\ndef create_folder_in_shared_drive(\n drive: GoogleDrive,\n name: str,\n parent_id: str\n) -> dict:\n \"\"\"\n Create a folder in a shared drive.\n\n Args:\n drive: Authenticated GoogleDrive instance\n name: Folder name\n parent_id: Parent folder ID\n\n Returns:\n Dict with folder id and webViewLink\n \"\"\"\n # Check if folder already exists\n query = (\n f\"'{parent_id}' in parents and \"\n f\"title='{name}' and \"\n \"mimeType='application/vnd.google-apps.folder' and \"\n \"trashed=false\"\n )\n\n file_list = drive.ListFile({\n 'q': query,\n 'supportsAllDrives': True,\n 'includeItemsFromAllDrives': True,\n 'corpora': 'drive',\n 'driveId': SHARED_DRIVE_ID\n }).GetList()\n\n if file_list:\n existing = file_list[0]\n return {\n 'id': existing['id'],\n 'name': name,\n 'webViewLink': existing['alternateLink'],\n 'existed': True\n }\n\n # Create new folder in shared drive\n folder = drive.CreateFile({\n 'title': name,\n 'parents': [{'id': parent_id}],\n 'mimeType': 'application/vnd.google-apps.folder'\n })\n\n # Must set supportsAllDrives for shared drive operations\n folder.Upload(param={'supportsAllDrives': True})\n\n return {\n 'id': folder['id'],\n 'name': name,\n 'webViewLink': folder['alternateLink'],\n 'existed': False\n }\n\n\ndef create_structure_recursive(\n drive: GoogleDrive,\n structure: dict,\n parent_id: str,\n path_prefix: str = ''\n) -> list:\n \"\"\"\n Recursively create folder structure.\n\n Args:\n drive: GoogleDrive instance\n structure: Dict representing folder hierarchy\n parent_id: Current parent folder ID\n path_prefix: Current path for logging\n\n Returns:\n list: All created folders with paths and IDs\n \"\"\"\n created_folders = []\n\n for folder_name, children in structure.items():\n current_path = f\"{path_prefix}/{folder_name}\" if path_prefix else folder_name\n\n # Create this folder\n result = create_folder_in_shared_drive(drive, folder_name, parent_id)\n\n status = \"exists\" if result['existed'] else \"created\"\n print(f\" {'[exists]' if result['existed'] else '[created]'} {current_path}\")\n\n created_folders.append({\n 'path': current_path,\n 'id': result['id'],\n 'webViewLink': result['webViewLink'],\n 'existed': result['existed']\n })\n\n # Recursively create children\n if children and isinstance(children, dict):\n child_folders = create_structure_recursive(\n drive, children, result['id'], current_path\n )\n created_folders.extend(child_folders)\n\n return created_folders\n\n\ndef create_client_folder(\n client_name: str,\n dry_run: bool = False\n) -> dict:\n \"\"\"\n Create a complete client folder structure in the shared drive.\n\n Args:\n client_name: Name of the client (e.g., \"Microsoft\")\n dry_run: If True, don't actually create folders\n\n Returns:\n Dict with folder_url, folder_id, all created folders\n \"\"\"\n validate_setup()\n\n if dry_run:\n print(\"\\n[DRY RUN] Would create the following structure:\")\n print(f\" [XX] {client_name}/\")\n for folder, children in CLIENT_FOLDER_STRUCTURE.items():\n print(f\" {folder}/\")\n if children:\n for subfolder in children:\n print(f\" {subfolder}/\")\n return {\"dry_run\": True, \"client_name\": client_name}\n\n # Authenticate\n drive = authenticate()\n\n # Get next folder number\n next_num = get_next_folder_number(drive)\n\n # Create parent folder name\n parent_folder_name = f\"[{next_num:02d}] {client_name}\"\n print(f\"\\nCreating folder structure: {parent_folder_name}\")\n\n # Create parent folder\n parent_result = create_folder_in_shared_drive(\n drive,\n parent_folder_name,\n SHARED_DRIVE_ID\n )\n\n if parent_result['existed']:\n print(f\" [exists] {parent_folder_name}\")\n else:\n print(f\" [created] {parent_folder_name}\")\n\n # Create child structure\n created_folders = create_structure_recursive(\n drive,\n CLIENT_FOLDER_STRUCTURE,\n parent_result['id'],\n parent_folder_name\n )\n\n # Prepend parent folder to list\n all_folders = [{\n 'path': parent_folder_name,\n 'id': parent_result['id'],\n 'webViewLink': parent_result['webViewLink'],\n 'existed': parent_result['existed']\n }] + created_folders\n\n # Summary\n new_count = sum(1 for f in all_folders if not f['existed'])\n existing_count = sum(1 for f in all_folders if f['existed'])\n\n print(f\"\\nDone!\")\n print(f\" Created: {new_count} new folders\")\n print(f\" Existing: {existing_count} folders (skipped)\")\n print(f\"\\nClient folder: {parent_result['webViewLink']}\")\n\n result = {\n 'client_name': client_name,\n 'folder_number': next_num,\n 'folder_name': parent_folder_name,\n 'folder_id': parent_result['id'],\n 'folder_url': parent_result['webViewLink'],\n 'created_folders': all_folders,\n 'summary': {\n 'total': len(all_folders),\n 'new': new_count,\n 'existing': existing_count\n }\n }\n\n # Save result to .tmp\n output_dir = Path('.tmp/client_folders')\n output_dir.mkdir(parents=True, exist_ok=True)\n output_file = output_dir / f'{client_name.lower().replace(\" \", \"_\")}.json'\n with open(output_file, 'w') as f:\n json.dump(result, f, indent=2)\n print(f\" Result saved to: {output_file}\")\n\n return result\n\n\ndef main():\n \"\"\"CLI entry point.\"\"\"\n parser = argparse.ArgumentParser(\n description=\"Create client folder structure in Google Drive\",\n formatter_class=argparse.RawDescriptionHelpFormatter,\n epilog=\"\"\"\nExamples:\n # Create folder structure for a new client\n %(prog)s \"Microsoft\"\n\n # Dry run to see what would be created\n %(prog)s \"Acme Corp\" --dry-run\n \"\"\"\n )\n\n parser.add_argument(\"client_name\", help=\"Name of the client\")\n parser.add_argument(\"--dry-run\", action=\"store_true\", help=\"Preview without creating\")\n parser.add_argument(\"--json\", action=\"store_true\", help=\"Output as JSON\")\n\n args = parser.parse_args()\n\n try:\n result = create_client_folder(\n client_name=args.client_name,\n dry_run=args.dry_run\n )\n\n if args.json:\n print(json.dumps(result, indent=2))\n\n return 0\n\n except FileNotFoundError as e:\n print(f\"Setup Error: {e}\")\n return 1\n except ClientFolderError as e:\n print(f\"Error: {e}\")\n return 1\n except Exception as e:\n print(f\"Unexpected error: {e}\")\n import traceback\n traceback.print_exc()\n return 1\n\n\nif __name__ == \"__main__\":\n exit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":11420,"content_sha256":"1d7fd4de5f0f42da0d165b500ff9f1d1b4bcf93c878863cc38ca16f098efb688"},{"filename":"scripts/gdrive_folder_structure.py","content":"#!/usr/bin/env python3\n\"\"\"\nGoogle Drive Folder Structure Generator\n\nAuto-generate nested folder structures in Google Drive with a single command.\n\nUsage:\n # Create folder structure from JSON\n python execution/gdrive_folder_structure.py --structure '{\n \"Folder 1\": {\n \"subfolder a\": {},\n \"subfolder b\": {}\n }\n }'\n\n # Create from a JSON file\n python execution/gdrive_folder_structure.py --file structure.json\n\n # Create in specific parent folder\n python execution/gdrive_folder_structure.py --structure '{\"Project\": {}}' --parent-path \"Clients/Acme\"\n\"\"\"\n\nimport os\nimport sys\nimport json\nimport argparse\nfrom pathlib import Path\nfrom pydrive2.auth import GoogleAuth\nfrom pydrive2.drive import GoogleDrive\n\n# Configuration\nSETTINGS_FILE = \"settings.yaml\"\nCREDENTIALS_FILE = \"mycreds.txt\"\nCLIENT_SECRETS_FILE = \"client_secrets.json\"\n\n\ndef validate_setup():\n \"\"\"Validate required files exist.\"\"\"\n if not Path(CLIENT_SECRETS_FILE).exists():\n raise FileNotFoundError(\n f\"{CLIENT_SECRETS_FILE} not found!\\n\\n\"\n \"Setup instructions:\\n\"\n \"1. Go to https://console.cloud.google.com\\n\"\n \"2. Enable Google Drive API\\n\"\n \"3. Create OAuth 2.0 credentials (Desktop app)\\n\"\n \"4. Download JSON and save as 'client_secrets.json'\\n\"\n )\n\n if not Path(SETTINGS_FILE).exists():\n raise FileNotFoundError(f\"{SETTINGS_FILE} not found! This should be in project root.\")\n\n\ndef authenticate():\n \"\"\"\n Authenticate with Google Drive using OAuth 2.0.\n\n Returns:\n GoogleDrive: Authenticated drive instance\n \"\"\"\n print(\"🔐 Authenticating with Google Drive...\")\n\n gauth = GoogleAuth()\n gauth.LoadCredentialsFile(CREDENTIALS_FILE)\n\n if gauth.credentials is None:\n print(\" First time setup - opening browser for authentication...\")\n gauth.LocalWebserverAuth()\n elif gauth.access_token_expired:\n print(\" Refreshing expired credentials...\")\n gauth.Refresh()\n else:\n print(\" Using saved credentials...\")\n gauth.Authorize()\n\n gauth.SaveCredentialsFile(CREDENTIALS_FILE)\n print(\"✅ Authentication successful!\")\n\n return GoogleDrive(gauth)\n\n\ndef find_folder_by_path(drive, folder_path, parent_id='root'):\n \"\"\"\n Find folder ID by path, traversing nested structure.\n\n Args:\n drive: GoogleDrive instance\n folder_path: Path like \"Clients/Acme/Images\"\n parent_id: Parent folder ID (default: 'root' for My Drive)\n\n Returns:\n str: Folder ID or None if not found\n \"\"\"\n if not folder_path:\n return parent_id\n\n parts = folder_path.split('/')\n current_id = parent_id\n\n for folder_name in parts:\n query = f\"'{current_id}' in parents and title='{folder_name}' and mimeType='application/vnd.google-apps.folder' and trashed=false\"\n file_list = drive.ListFile({'q': query}).GetList()\n\n if file_list:\n current_id = file_list[0]['id']\n else:\n return None\n\n return current_id\n\n\ndef create_folder(drive, name, parent_id='root'):\n \"\"\"\n Create a single folder in Google Drive.\n\n Args:\n drive: GoogleDrive instance\n name: Folder name\n parent_id: Parent folder ID\n\n Returns:\n dict: Folder info with id and webViewLink\n \"\"\"\n # Check if folder already exists\n query = f\"'{parent_id}' in parents and title='{name}' and mimeType='application/vnd.google-apps.folder' and trashed=false\"\n file_list = drive.ListFile({'q': query}).GetList()\n\n if file_list:\n existing = file_list[0]\n return {\n 'id': existing['id'],\n 'name': name,\n 'webViewLink': existing['alternateLink'],\n 'existed': True\n }\n\n # Create new folder\n folder = drive.CreateFile({\n 'title': name,\n 'parents': [{'id': parent_id}],\n 'mimeType': 'application/vnd.google-apps.folder'\n })\n folder.Upload()\n\n return {\n 'id': folder['id'],\n 'name': name,\n 'webViewLink': folder['alternateLink'],\n 'existed': False\n }\n\n\ndef create_structure_recursive(drive, structure, parent_id='root', path_prefix=''):\n \"\"\"\n Recursively create folder structure.\n\n Args:\n drive: GoogleDrive instance\n structure: Dict representing folder hierarchy\n parent_id: Current parent folder ID\n path_prefix: Current path for logging\n\n Returns:\n list: All created folders with paths and IDs\n \"\"\"\n created_folders = []\n\n for folder_name, children in structure.items():\n current_path = f\"{path_prefix}/{folder_name}\" if path_prefix else folder_name\n\n # Create this folder\n result = create_folder(drive, folder_name, parent_id)\n\n status = \"exists\" if result['existed'] else \"created\"\n print(f\" {'✓' if result['existed'] else '✅'} {current_path} ({status})\")\n\n created_folders.append({\n 'path': current_path,\n 'id': result['id'],\n 'webViewLink': result['webViewLink'],\n 'existed': result['existed']\n })\n\n # Recursively create children\n if children and isinstance(children, dict):\n child_folders = create_structure_recursive(\n drive, children, result['id'], current_path\n )\n created_folders.extend(child_folders)\n\n return created_folders\n\n\ndef parse_structure(structure_str):\n \"\"\"\n Parse structure from JSON string.\n\n Args:\n structure_str: JSON string or file path\n\n Returns:\n dict: Parsed structure\n \"\"\"\n try:\n return json.loads(structure_str)\n except json.JSONDecodeError as e:\n raise ValueError(f\"Invalid JSON structure: {e}\")\n\n\ndef main():\n \"\"\"Main execution function.\"\"\"\n parser = argparse.ArgumentParser(\n description=\"Create folder structure in Google Drive\",\n formatter_class=argparse.RawDescriptionHelpFormatter,\n epilog=\"\"\"\nExamples:\n # Simple structure\n %(prog)s --structure '{\"Folder 1\": {\"subfolder a\": {}, \"subfolder b\": {}}}'\n\n # From file\n %(prog)s --file structure.json\n\n # In specific parent\n %(prog)s --structure '{\"Project\": {}}' --parent-path \"Clients/Acme\"\n \"\"\"\n )\n\n parser.add_argument(\"--structure\", help=\"JSON structure string\")\n parser.add_argument(\"--file\", help=\"Path to JSON structure file\")\n parser.add_argument(\"--parent-path\", help=\"Parent folder path (e.g., 'Clients/Acme')\")\n parser.add_argument(\"--parent-id\", default=\"root\", help=\"Parent folder ID (default: My Drive root)\")\n\n args = parser.parse_args()\n\n # Validate inputs\n if not args.structure and not args.file:\n parser.error(\"Must specify --structure or --file\")\n\n try:\n # Validate setup\n validate_setup()\n\n # Parse structure\n if args.file:\n with open(args.file, 'r') as f:\n structure = json.load(f)\n print(f\"📄 Loaded structure from: {args.file}\")\n else:\n structure = parse_structure(args.structure)\n\n print(f\"\\n📁 Folder structure to create:\")\n print(json.dumps(structure, indent=2))\n\n # Authenticate\n drive = authenticate()\n\n # Resolve parent folder\n parent_id = args.parent_id\n if args.parent_path:\n print(f\"\\n🔍 Finding parent folder: {args.parent_path}\")\n parent_id = find_folder_by_path(drive, args.parent_path)\n if parent_id is None:\n print(f\"❌ Parent folder not found: {args.parent_path}\")\n return 1\n print(f\" ✓ Found: {parent_id}\")\n\n # Create structure\n print(f\"\\n📁 Creating folder structure...\")\n created_folders = create_structure_recursive(drive, structure, parent_id)\n\n # Summary\n new_count = sum(1 for f in created_folders if not f['existed'])\n existing_count = sum(1 for f in created_folders if f['existed'])\n\n print(f\"\\n✅ Done!\")\n print(f\" Created: {new_count} new folders\")\n print(f\" Existing: {existing_count} folders (skipped)\")\n\n if created_folders:\n root_folder = created_folders[0]\n print(f\"\\n📂 Root folder: {root_folder['webViewLink']}\")\n\n # Output JSON result\n result = {\n 'root_folder_id': created_folders[0]['id'] if created_folders else None,\n 'root_folder_url': created_folders[0]['webViewLink'] if created_folders else None,\n 'created_folders': created_folders,\n 'summary': {\n 'total': len(created_folders),\n 'new': new_count,\n 'existing': existing_count\n }\n }\n\n # Save result to .tmp\n output_dir = Path('.tmp/gdrive_structure')\n output_dir.mkdir(parents=True, exist_ok=True)\n output_file = output_dir / 'last_result.json'\n with open(output_file, 'w') as f:\n json.dump(result, f, indent=2)\n print(f\"\\n📋 Result saved to: {output_file}\")\n\n return 0\n\n except Exception as e:\n print(f\"❌ Error: {str(e)}\")\n import traceback\n traceback.print_exc()\n return 1\n\n\nif __name__ == \"__main__\":\n exit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":9289,"content_sha256":"30859e12db086164963bbfbf3703eafd875dbee72ba4d4e36a4107de303cf26c"},{"filename":"scripts/gdrive_search.py","content":"#!/usr/bin/env python3\n\"\"\"\nGoogle Drive Search\n\nSearch for files and folders in Google Drive.\nUsed for client overview and document discovery.\n\nDirective: directives/gdrive_search.md\n\nUsage:\n # Find client folder\n python execution/gdrive_search.py folder \"Microsoft\"\n\n # Search for documents\n python execution/gdrive_search.py files \"proposal\" --modified-days 30\n\n # Search in specific folder\n python execution/gdrive_search.py files \"transcript\" --in-folder \"1abc123xyz\"\n\"\"\"\n\nimport os\nimport sys\nimport re\nimport json\nimport argparse\nfrom datetime import datetime, timedelta\nfrom pathlib import Path\nfrom typing import Optional, List\nfrom pydrive2.auth import GoogleAuth\nfrom pydrive2.drive import GoogleDrive\n\n# Configuration\nSETTINGS_FILE = \"settings.yaml\"\nCREDENTIALS_FILE = \"mycreds.txt\"\nCLIENT_SECRETS_FILE = \"client_secrets.json\"\n\n# Shared drive configuration (set via env vars or .env)\nSHARED_DRIVE_ID = os.getenv(\"SHARED_DRIVE_ID\", \"\")\n\n\nclass DriveSearchError(Exception):\n \"\"\"Custom exception for Drive search operations.\"\"\"\n pass\n\n\ndef validate_setup():\n \"\"\"Validate required files exist.\"\"\"\n if not Path(CLIENT_SECRETS_FILE).exists():\n raise FileNotFoundError(\n f\"{CLIENT_SECRETS_FILE} not found. \"\n \"Download OAuth credentials from Google Cloud Console.\"\n )\n if not Path(SETTINGS_FILE).exists():\n raise FileNotFoundError(f\"{SETTINGS_FILE} not found.\")\n\n\ndef authenticate() -> GoogleDrive:\n \"\"\"Authenticate with Google Drive.\"\"\"\n gauth = GoogleAuth()\n gauth.LoadCredentialsFile(CREDENTIALS_FILE)\n\n if gauth.credentials is None:\n print(\"First time setup - opening browser for authentication...\")\n gauth.LocalWebserverAuth()\n elif gauth.access_token_expired:\n print(\"Refreshing credentials...\")\n gauth.Refresh()\n else:\n gauth.Authorize()\n\n gauth.SaveCredentialsFile(CREDENTIALS_FILE)\n return GoogleDrive(gauth)\n\n\ndef find_client_folder(\n drive: GoogleDrive,\n client_name: str,\n shared_drive_id: str = SHARED_DRIVE_ID\n) -> Optional[dict]:\n \"\"\"\n Find a client folder by name.\n\n Client folders follow pattern: [XX] Client Name\n\n Args:\n drive: GoogleDrive instance\n client_name: Client name to search for\n shared_drive_id: Shared drive ID\n\n Returns:\n Folder info dict or None\n \"\"\"\n # Query folders in shared drive\n query = (\n f\"'{shared_drive_id}' in parents and \"\n \"mimeType='application/vnd.google-apps.folder' and \"\n \"trashed=false\"\n )\n\n file_list = drive.ListFile({\n 'q': query,\n 'supportsAllDrives': True,\n 'includeItemsFromAllDrives': True,\n 'corpora': 'drive',\n 'driveId': shared_drive_id\n }).GetList()\n\n # Search for matching folder\n client_lower = client_name.lower()\n pattern = re.compile(r'^\\[(\\d+)\\]\\s*(.+)

Google Workspace Overview Interact with Google Drive, Gmail, Calendar, and Docs using OAuth authentication. Supports file uploads, folder management, email search, calendar search, and document operations. Quick Decision Tree Environment Setup OAuth credentials are stored locally after first authentication. Required Files - - From Google Cloud Console - - PyDrive2 configuration - - Auto-generated OAuth tokens First-Time Setup 1. Go to Google Cloud Console 2. Enable APIs: Drive, Gmail, Calendar, Docs 3. Create OAuth 2.0 credentials (Desktop app) 4. Download as 5. Run any script - browser opens…

)\n\n for folder in file_list:\n title = folder['title']\n match = pattern.match(title)\n\n if match:\n folder_name = match.group(2).strip().lower()\n if client_lower in folder_name or folder_name in client_lower:\n return {\n 'id': folder['id'],\n 'title': folder['title'],\n 'number': int(match.group(1)),\n 'webViewLink': folder['alternateLink']\n }\n\n return None\n\n\ndef search_files(\n drive: GoogleDrive,\n query: str = None,\n folder_id: str = None,\n file_types: List[str] = None,\n modified_days: int = None,\n shared_drive_id: str = SHARED_DRIVE_ID,\n limit: int = 50\n) -> List[dict]:\n \"\"\"\n Search for files in Google Drive.\n\n Args:\n drive: GoogleDrive instance\n query: Text to search in file names\n folder_id: Limit search to specific folder\n file_types: Filter by MIME types (doc, sheet, pdf, etc.)\n modified_days: Only files modified in last N days\n shared_drive_id: Shared drive to search\n limit: Maximum results\n\n Returns:\n List of file info dicts\n \"\"\"\n # Build query\n conditions = [\"trashed=false\"]\n\n if folder_id:\n conditions.append(f\"'{folder_id}' in parents\")\n\n if query:\n conditions.append(f\"title contains '{query}'\")\n\n if modified_days:\n cutoff = datetime.utcnow() - timedelta(days=modified_days)\n cutoff_str = cutoff.strftime('%Y-%m-%dT%H:%M:%S')\n conditions.append(f\"modifiedDate > '{cutoff_str}'\")\n\n if file_types:\n type_map = {\n 'doc': 'application/vnd.google-apps.document',\n 'sheet': 'application/vnd.google-apps.spreadsheet',\n 'slide': 'application/vnd.google-apps.presentation',\n 'pdf': 'application/pdf',\n 'folder': 'application/vnd.google-apps.folder'\n }\n mime_conditions = []\n for ft in file_types:\n mime = type_map.get(ft, ft)\n mime_conditions.append(f\"mimeType='{mime}'\")\n if mime_conditions:\n conditions.append(f\"({' or '.join(mime_conditions)})\")\n\n q = \" and \".join(conditions)\n\n # Execute search\n params = {\n 'q': q,\n 'supportsAllDrives': True,\n 'includeItemsFromAllDrives': True,\n 'orderBy': 'modifiedDate desc',\n 'maxResults': limit\n }\n\n if shared_drive_id:\n params['corpora'] = 'drive'\n params['driveId'] = shared_drive_id\n\n file_list = drive.ListFile(params).GetList()\n\n # Process results\n results = []\n for f in file_list:\n # Parse modified time\n modified = f.get('modifiedDate', '')\n if modified:\n try:\n modified_dt = datetime.fromisoformat(modified.replace('Z', '+00:00'))\n modified_display = modified_dt.strftime('%Y-%m-%d')\n except Exception:\n modified_display = modified\n else:\n modified_display = 'Unknown'\n\n results.append({\n 'id': f['id'],\n 'title': f['title'],\n 'mimeType': f.get('mimeType', ''),\n 'webViewLink': f.get('alternateLink', ''),\n 'modifiedDate': modified_display,\n 'modifiedBy': f.get('lastModifyingUserName', ''),\n 'fileSize': f.get('fileSize', '')\n })\n\n return results\n\n\ndef list_folder_contents(\n drive: GoogleDrive,\n folder_id: str,\n recursive: bool = False,\n _depth: int = 0,\n _max_depth: int = 2\n) -> List[dict]:\n \"\"\"\n List contents of a folder.\n\n Args:\n drive: GoogleDrive instance\n folder_id: Folder ID\n recursive: Whether to recurse into subfolders\n _depth: Current recursion depth\n _max_depth: Maximum recursion depth\n\n Returns:\n List of file/folder info dicts\n \"\"\"\n query = f\"'{folder_id}' in parents and trashed=false\"\n\n file_list = drive.ListFile({\n 'q': query,\n 'supportsAllDrives': True,\n 'includeItemsFromAllDrives': True\n }).GetList()\n\n results = []\n for f in file_list:\n is_folder = f.get('mimeType') == 'application/vnd.google-apps.folder'\n\n item = {\n 'id': f['id'],\n 'title': f['title'],\n 'mimeType': f.get('mimeType', ''),\n 'webViewLink': f.get('alternateLink', ''),\n 'isFolder': is_folder,\n 'depth': _depth\n }\n\n results.append(item)\n\n # Recurse into subfolders\n if recursive and is_folder and _depth \u003c _max_depth:\n children = list_folder_contents(\n drive, f['id'],\n recursive=True,\n _depth=_depth + 1,\n _max_depth=_max_depth\n )\n results.extend(children)\n\n return results\n\n\ndef search_client_documents(\n client_name: str,\n keywords: List[str] = None,\n modified_days: int = 30\n) -> dict:\n \"\"\"\n Complete client document search.\n\n Args:\n client_name: Client name\n keywords: Additional keywords to search\n modified_days: Look back period for recent docs\n\n Returns:\n Dict with folder info and documents\n \"\"\"\n validate_setup()\n drive = authenticate()\n\n result = {\n 'client_name': client_name,\n 'folder': None,\n 'folder_contents': [],\n 'recent_documents': []\n }\n\n # Find client folder\n folder = find_client_folder(drive, client_name)\n if folder:\n result['folder'] = folder\n\n # List folder contents\n contents = list_folder_contents(\n drive, folder['id'],\n recursive=True,\n _max_depth=2\n )\n result['folder_contents'] = contents\n\n # Search for recent documents mentioning client\n search_terms = [client_name]\n if keywords:\n search_terms.extend(keywords)\n\n for term in search_terms[:3]: # Limit searches\n docs = search_files(\n drive,\n query=term,\n modified_days=modified_days,\n limit=10\n )\n result['recent_documents'].extend(docs)\n\n # Deduplicate\n seen = set()\n unique_docs = []\n for doc in result['recent_documents']:\n if doc['id'] not in seen:\n seen.add(doc['id'])\n unique_docs.append(doc)\n result['recent_documents'] = unique_docs[:20]\n\n return result\n\n\ndef main():\n \"\"\"CLI entry point.\"\"\"\n parser = argparse.ArgumentParser(\n description=\"Search Google Drive for files and folders\"\n )\n\n subparsers = parser.add_subparsers(dest=\"command\", help=\"Commands\")\n\n # Find client folder\n folder_parser = subparsers.add_parser(\"folder\", help=\"Find client folder\")\n folder_parser.add_argument(\"client_name\", help=\"Client name\")\n\n # Search files\n files_parser = subparsers.add_parser(\"files\", help=\"Search for files\")\n files_parser.add_argument(\"query\", help=\"Search query\")\n files_parser.add_argument(\"--in-folder\", help=\"Folder ID to search in\")\n files_parser.add_argument(\"--type\", nargs=\"+\", choices=['doc', 'sheet', 'slide', 'pdf'],\n help=\"File types to filter\")\n files_parser.add_argument(\"--modified-days\", type=int, help=\"Modified in last N days\")\n files_parser.add_argument(\"--limit\", type=int, default=20, help=\"Max results\")\n\n # List folder contents\n list_parser = subparsers.add_parser(\"list\", help=\"List folder contents\")\n list_parser.add_argument(\"folder_id\", help=\"Folder ID\")\n list_parser.add_argument(\"--recursive\", action=\"store_true\", help=\"Include subfolders\")\n\n # Client overview search\n client_parser = subparsers.add_parser(\"client\", help=\"Full client document search\")\n client_parser.add_argument(\"client_name\", help=\"Client name\")\n client_parser.add_argument(\"--keywords\", nargs=\"+\", help=\"Additional search keywords\")\n client_parser.add_argument(\"--days\", type=int, default=30, help=\"Look back period\")\n\n # JSON output\n parser.add_argument(\"--json\", action=\"store_true\", help=\"Output as JSON\")\n\n args = parser.parse_args()\n\n if not args.command:\n parser.print_help()\n return 1\n\n try:\n validate_setup()\n drive = authenticate()\n\n if args.command == \"folder\":\n print(f\"Searching for client folder: {args.client_name}\")\n folder = find_client_folder(drive, args.client_name)\n\n if folder:\n print(f\"\\nFound: {folder['title']}\")\n print(f\"ID: {folder['id']}\")\n print(f\"URL: {folder['webViewLink']}\")\n else:\n print(\"Client folder not found\")\n return 0\n\n elif args.command == \"files\":\n print(f\"Searching for: {args.query}\")\n files = search_files(\n drive,\n query=args.query,\n folder_id=args.in_folder,\n file_types=args.type,\n modified_days=args.modified_days,\n limit=args.limit\n )\n\n if hasattr(args, 'json') and args.json:\n print(json.dumps(files, indent=2))\n else:\n print(f\"\\nFound {len(files)} files:\")\n for f in files:\n print(f\"\\n {f['title']}\")\n print(f\" Modified: {f['modifiedDate']} by {f['modifiedBy']}\")\n print(f\" URL: {f['webViewLink']}\")\n return 0\n\n elif args.command == \"list\":\n print(f\"Listing folder: {args.folder_id}\")\n contents = list_folder_contents(\n drive,\n args.folder_id,\n recursive=args.recursive\n )\n\n if hasattr(args, 'json') and args.json:\n print(json.dumps(contents, indent=2))\n else:\n print(f\"\\nFolder contents ({len(contents)} items):\")\n for item in contents:\n indent = \" \" * (item['depth'] + 1)\n icon = \"[D]\" if item['isFolder'] else \"[F]\"\n print(f\"{indent}{icon} {item['title']}\")\n return 0\n\n elif args.command == \"client\":\n print(f\"Full client search: {args.client_name}\")\n result = search_client_documents(\n args.client_name,\n keywords=args.keywords,\n modified_days=args.days\n )\n\n if hasattr(args, 'json') and args.json:\n print(json.dumps(result, indent=2))\n else:\n print(f\"\\n=== Client Documents: {args.client_name} ===\")\n\n if result['folder']:\n print(f\"\\nClient Folder: {result['folder']['title']}\")\n print(f\"URL: {result['folder']['webViewLink']}\")\n print(f\"\\nFolder Contents ({len(result['folder_contents'])} items):\")\n for item in result['folder_contents'][:15]:\n indent = \" \" * (item['depth'] + 1)\n icon = \"[D]\" if item['isFolder'] else \"[F]\"\n print(f\"{indent}{icon} {item['title']}\")\n else:\n print(\"\\nNo client folder found\")\n\n if result['recent_documents']:\n print(f\"\\nRecent Documents ({len(result['recent_documents'])}):\")\n for doc in result['recent_documents'][:10]:\n print(f\" - {doc['title']} (modified {doc['modifiedDate']})\")\n return 0\n\n except FileNotFoundError as e:\n print(f\"Setup error: {e}\")\n return 1\n except DriveSearchError as e:\n print(f\"Error: {e}\")\n return 1\n except Exception as e:\n print(f\"Unexpected error: {e}\")\n import traceback\n traceback.print_exc()\n return 1\n\n\nif __name__ == \"__main__\":\n exit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":14781,"content_sha256":"dc5c5c7108f798e66d7b4b76293eb58c1ab1eb1df69b314a226e53e94e5f1eb6"},{"filename":"scripts/gdrive_transcript_search.py","content":"#!/usr/bin/env python3\n\"\"\"\nGoogle Drive Transcript Search\nSearches for transcript files in client folders based on company name.\n\nDirective: directives/google_workspace_integration.md\n\nUsage:\n # Find transcript for a client\n python execution/gdrive_transcript_search.py --client \"Acme Corp\"\n\n # Find transcript and return folder info\n python execution/gdrive_transcript_search.py --client \"Acme Corp\" --verbose\n\n # Search with specific keywords\n python execution/gdrive_transcript_search.py --client \"Acme\" --keywords \"transcript,notes,meeting\"\n\"\"\"\n\nimport os\nimport sys\nimport argparse\nimport json\nfrom pathlib import Path\nfrom datetime import datetime\nfrom typing import Optional, Tuple\nfrom pydrive2.auth import GoogleAuth\nfrom pydrive2.drive import GoogleDrive\n\n# Configuration\nSETTINGS_FILE = \"settings.yaml\"\nCREDENTIALS_FILE = \"mycreds.txt\"\n\n# Default search keywords for transcript files\nDEFAULT_TRANSCRIPT_KEYWORDS = [\n \"transcript\",\n \"transcription\",\n \"meeting notes\",\n \"call notes\",\n \"discovery\",\n \"kick-off\",\n \"kickoff\"\n]\n\n\ndef authenticate() -> GoogleDrive:\n \"\"\"\n Authenticate with Google Drive using OAuth 2.0.\n\n Returns:\n GoogleDrive: Authenticated drive instance\n \"\"\"\n print(\"Authenticating with Google Drive...\")\n\n gauth = GoogleAuth()\n gauth.LoadCredentialsFile(CREDENTIALS_FILE)\n\n if gauth.credentials is None:\n print(\" First time setup - opening browser for authentication...\")\n gauth.LocalWebserverAuth()\n elif gauth.access_token_expired:\n print(\" Refreshing expired credentials...\")\n gauth.Refresh()\n else:\n gauth.Authorize()\n\n gauth.SaveCredentialsFile(CREDENTIALS_FILE)\n print(\"Authentication successful!\")\n\n return GoogleDrive(gauth)\n\n\ndef search_client_folders(drive: GoogleDrive, client_name: str) -> list:\n \"\"\"\n Search for folders containing the client name.\n\n Args:\n drive: GoogleDrive instance\n client_name: Client/company name to search for\n\n Returns:\n List of folder metadata dicts\n \"\"\"\n print(f\" Searching for folders matching: {client_name}\")\n\n # Search for folders containing client name (case-insensitive via contains)\n query = (\n f\"mimeType='application/vnd.google-apps.folder' \"\n f\"and title contains '{client_name}' \"\n f\"and trashed=false\"\n )\n\n file_list = drive.ListFile({'q': query}).GetList()\n\n # Sort by most recently modified\n file_list.sort(key=lambda x: x.get('modifiedDate', ''), reverse=True)\n\n print(f\" Found {len(file_list)} matching folders\")\n return file_list\n\n\ndef search_transcripts_in_folder(\n drive: GoogleDrive,\n folder_id: str,\n keywords: list = None\n) -> list:\n \"\"\"\n Search for transcript files within a specific folder.\n\n Args:\n drive: GoogleDrive instance\n folder_id: Google Drive folder ID\n keywords: List of keywords to search for in file names\n\n Returns:\n List of file metadata dicts\n \"\"\"\n keywords = keywords or DEFAULT_TRANSCRIPT_KEYWORDS\n all_files = []\n\n # Search for each keyword\n for keyword in keywords:\n query = (\n f\"'{folder_id}' in parents \"\n f\"and title contains '{keyword}' \"\n f\"and trashed=false\"\n )\n files = drive.ListFile({'q': query}).GetList()\n all_files.extend(files)\n\n # Deduplicate by file ID\n seen_ids = set()\n unique_files = []\n for f in all_files:\n if f['id'] not in seen_ids:\n seen_ids.add(f['id'])\n unique_files.append(f)\n\n # Sort by most recently modified\n unique_files.sort(key=lambda x: x.get('modifiedDate', ''), reverse=True)\n\n return unique_files\n\n\ndef search_transcripts_recursive(\n drive: GoogleDrive,\n folder_id: str,\n keywords: list = None,\n max_depth: int = 3,\n current_depth: int = 0\n) -> list:\n \"\"\"\n Recursively search for transcripts in folder and subfolders.\n\n Args:\n drive: GoogleDrive instance\n folder_id: Starting folder ID\n keywords: Keywords to search for\n max_depth: Maximum recursion depth\n current_depth: Current depth (for recursion)\n\n Returns:\n List of (file_metadata, folder_path) tuples\n \"\"\"\n if current_depth > max_depth:\n return []\n\n results = []\n\n # Search current folder\n transcripts = search_transcripts_in_folder(drive, folder_id, keywords)\n for t in transcripts:\n results.append((t, folder_id))\n\n # Search subfolders\n subfolders_query = (\n f\"'{folder_id}' in parents \"\n f\"and mimeType='application/vnd.google-apps.folder' \"\n f\"and trashed=false\"\n )\n subfolders = drive.ListFile({'q': subfolders_query}).GetList()\n\n for subfolder in subfolders:\n sub_results = search_transcripts_recursive(\n drive,\n subfolder['id'],\n keywords,\n max_depth,\n current_depth + 1\n )\n results.extend(sub_results)\n\n return results\n\n\ndef get_file_content(drive: GoogleDrive, file_id: str) -> Optional[str]:\n \"\"\"\n Download and return content of a text-based file.\n\n Supports: Google Docs, plain text, markdown\n\n Args:\n drive: GoogleDrive instance\n file_id: File ID to download\n\n Returns:\n File content as string, or None if not readable\n \"\"\"\n try:\n file_obj = drive.CreateFile({'id': file_id})\n file_obj.FetchMetadata()\n\n mime_type = file_obj.get('mimeType', '')\n\n # Google Docs - export as plain text\n if mime_type == 'application/vnd.google-apps.document':\n content = file_obj.GetContentString(mimetype='text/plain')\n return content\n\n # Plain text files\n elif mime_type in ['text/plain', 'text/markdown', 'text/x-markdown']:\n content = file_obj.GetContentString()\n return content\n\n # Try to get content anyway for other text types\n elif 'text' in mime_type:\n content = file_obj.GetContentString()\n return content\n\n else:\n print(f\" Unsupported file type: {mime_type}\")\n return None\n\n except Exception as e:\n print(f\" Error reading file: {e}\")\n return None\n\n\ndef find_client_transcript(\n drive: GoogleDrive,\n client_name: str,\n keywords: list = None,\n verbose: bool = False\n) -> Tuple[Optional[str], Optional[str], Optional[str]]:\n \"\"\"\n Find transcript file for a client.\n\n Args:\n drive: GoogleDrive instance\n client_name: Client/company name\n keywords: Optional custom keywords to search\n verbose: Print detailed progress\n\n Returns:\n Tuple of (file_content, file_id, folder_id) or (None, None, None)\n \"\"\"\n print(f\"\\nSearching for transcript for: {client_name}\")\n\n # Step 1: Find client folders\n folders = search_client_folders(drive, client_name)\n\n if not folders:\n print(f\" No folders found matching '{client_name}'\")\n return None, None, None\n\n if verbose:\n print(f\" Found folders:\")\n for f in folders[:5]:\n print(f\" - {f['title']} (modified: {f.get('modifiedDate', 'N/A')[:10]})\")\n\n # Step 2: Search for transcripts in each folder\n all_transcripts = []\n\n for folder in folders:\n if verbose:\n print(f\" Searching in: {folder['title']}\")\n\n results = search_transcripts_recursive(\n drive,\n folder['id'],\n keywords,\n max_depth=2\n )\n\n for file_meta, parent_folder_id in results:\n all_transcripts.append({\n 'file': file_meta,\n 'folder_id': parent_folder_id,\n 'client_folder_id': folder['id'],\n 'client_folder_name': folder['title']\n })\n\n if not all_transcripts:\n print(f\" No transcript files found in client folders\")\n return None, None, None\n\n # Step 3: Select best transcript (most recent)\n all_transcripts.sort(\n key=lambda x: x['file'].get('modifiedDate', ''),\n reverse=True\n )\n\n best_match = all_transcripts[0]\n transcript_file = best_match['file']\n\n print(f\"\\n Found transcript: {transcript_file['title']}\")\n print(f\" In folder: {best_match['client_folder_name']}\")\n print(f\" Modified: {transcript_file.get('modifiedDate', 'N/A')[:10]}\")\n\n # Step 4: Download content\n print(f\" Downloading content...\")\n content = get_file_content(drive, transcript_file['id'])\n\n if content:\n print(f\" Downloaded {len(content)} characters\")\n return content, transcript_file['id'], best_match['client_folder_id']\n else:\n print(f\" Could not read file content\")\n return None, transcript_file['id'], best_match['client_folder_id']\n\n\ndef find_or_create_proposals_folder(\n drive: GoogleDrive,\n client_folder_id: str\n) -> str:\n \"\"\"\n Find or create a Proposals subfolder in the client folder.\n\n Args:\n drive: GoogleDrive instance\n client_folder_id: Client folder ID\n\n Returns:\n Proposals folder ID\n \"\"\"\n # Check if Proposals folder exists\n query = (\n f\"'{client_folder_id}' in parents \"\n f\"and title='Proposals' \"\n f\"and mimeType='application/vnd.google-apps.folder' \"\n f\"and trashed=false\"\n )\n folders = drive.ListFile({'q': query}).GetList()\n\n if folders:\n return folders[0]['id']\n\n # Create Proposals folder\n print(\" Creating Proposals folder...\")\n folder = drive.CreateFile({\n 'title': 'Proposals',\n 'parents': [{'id': client_folder_id}],\n 'mimeType': 'application/vnd.google-apps.folder'\n })\n folder.Upload()\n print(f\" Created Proposals folder: {folder['id']}\")\n return folder['id']\n\n\ndef main():\n \"\"\"CLI entry point.\"\"\"\n parser = argparse.ArgumentParser(\n description=\"Search Google Drive for client transcripts\",\n formatter_class=argparse.RawDescriptionHelpFormatter\n )\n\n parser.add_argument(\n \"--client\",\n required=True,\n help=\"Client/company name to search for\"\n )\n parser.add_argument(\n \"--keywords\",\n help=\"Comma-separated keywords to search for in file names\"\n )\n parser.add_argument(\n \"--verbose\",\n \"-v\",\n action=\"store_true\",\n help=\"Print detailed progress\"\n )\n parser.add_argument(\n \"--output\",\n help=\"Save transcript content to file\"\n )\n parser.add_argument(\n \"--json\",\n action=\"store_true\",\n help=\"Output results as JSON\"\n )\n\n args = parser.parse_args()\n\n try:\n # Authenticate\n drive = authenticate()\n\n # Parse keywords\n keywords = None\n if args.keywords:\n keywords = [k.strip() for k in args.keywords.split(',')]\n\n # Search for transcript\n content, file_id, folder_id = find_client_transcript(\n drive,\n args.client,\n keywords,\n args.verbose\n )\n\n if content is None:\n print(f\"\\nNo transcript found for '{args.client}'\")\n return 1\n\n # Output results\n if args.json:\n result = {\n \"client\": args.client,\n \"file_id\": file_id,\n \"folder_id\": folder_id,\n \"content_length\": len(content),\n \"content_preview\": content[:500] + \"...\" if len(content) > 500 else content\n }\n print(json.dumps(result, indent=2))\n else:\n print(f\"\\nTranscript found!\")\n print(f\" File ID: {file_id}\")\n print(f\" Folder ID: {folder_id}\")\n print(f\" Content length: {len(content)} characters\")\n\n if args.output:\n Path(args.output).write_text(content)\n print(f\" Saved to: {args.output}\")\n else:\n print(f\"\\n--- Content Preview (first 500 chars) ---\")\n print(content[:500])\n if len(content) > 500:\n print(\"...\")\n\n return 0\n\n except Exception as e:\n print(f\"Error: {e}\")\n import traceback\n traceback.print_exc()\n return 1\n\n\nif __name__ == \"__main__\":\n exit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":12319,"content_sha256":"95a84e3e30f94e10834409f002d4f5370f04003dfcac9d0819625d7047d31a3d"},{"filename":"scripts/gmail_search.py","content":"#!/usr/bin/env python3\n\"\"\"\nGmail Search and Read\n\nSearch Gmail for emails and read threads.\nUsed for client overview and communication history.\n\nDirective: directives/gmail_search.md\n\nUsage:\n # Search emails from/to a domain\n python execution/gmail_search.py --domain \"microsoft.com\" --days 14\n\n # Search by subject or content\n python execution/gmail_search.py --query \"proposal\" --days 30\n\n # Search internal emails mentioning a client\n python execution/gmail_search.py --query \"Microsoft\" --internal-only --days 14\n\"\"\"\n\nimport os\nimport sys\nimport json\nimport argparse\nimport pickle\nimport base64\nfrom datetime import datetime, timedelta\nfrom pathlib import Path\nfrom typing import Optional, List\nfrom dotenv import load_dotenv\nfrom email.utils import parsedate_to_datetime\n\nfrom google.auth.transport.requests import Request\nfrom google.oauth2.credentials import Credentials\nfrom google_auth_oauthlib.flow import InstalledAppFlow\nfrom googleapiclient.discovery import build\nfrom googleapiclient.errors import HttpError\n\n# Load environment variables\nload_dotenv()\n\n# OAuth scopes for Gmail (readonly)\nSCOPES = ['https://www.googleapis.com/auth/gmail.readonly']\n\n# Credentials files\nCLIENT_SECRETS_FILE = \"client_secrets.json\"\nGMAIL_TOKEN_FILE = \"gmail_token.pickle\"\n\n\nclass GmailSearchError(Exception):\n \"\"\"Custom exception for Gmail search operations.\"\"\"\n pass\n\n\ndef get_gmail_service():\n \"\"\"\n Authenticate and get Gmail service.\n\n Returns:\n Gmail API service\n \"\"\"\n creds = None\n\n # Load saved credentials\n if Path(GMAIL_TOKEN_FILE).exists():\n with open(GMAIL_TOKEN_FILE, 'rb') as token:\n creds = pickle.load(token)\n\n # Refresh or get new credentials\n if not creds or not creds.valid:\n if creds and creds.expired and creds.refresh_token:\n print(\"Refreshing Gmail credentials...\")\n creds.refresh(Request())\n else:\n if not Path(CLIENT_SECRETS_FILE).exists():\n raise GmailSearchError(\n f\"{CLIENT_SECRETS_FILE} not found. \"\n \"Download OAuth credentials from Google Cloud Console.\"\n )\n print(\"First time Gmail auth - opening browser...\")\n flow = InstalledAppFlow.from_client_secrets_file(\n CLIENT_SECRETS_FILE, SCOPES\n )\n creds = flow.run_local_server(port=0)\n\n # Save credentials\n with open(GMAIL_TOKEN_FILE, 'wb') as token:\n pickle.dump(creds, token)\n print(\"Gmail credentials saved.\")\n\n return build('gmail', 'v1', credentials=creds)\n\n\ndef build_search_query(\n query: str = None,\n domain: str = None,\n days: int = 14,\n internal_only: bool = False,\n exclude_domain: str = None\n) -> str:\n \"\"\"\n Build Gmail search query string.\n\n Args:\n query: Free text search\n domain: Email domain to filter (from/to)\n days: Limit to last N days\n internal_only: Only internal emails (exclude external domain)\n exclude_domain: Domain to exclude from results\n\n Returns:\n Gmail query string\n \"\"\"\n parts = []\n\n # Date filter\n if days > 0:\n parts.append(f\"newer_than:{days}d\")\n\n # Domain filter\n if domain:\n if internal_only:\n # Internal emails that mention the domain/query\n parts.append(f\"-from:@{domain}\")\n parts.append(f\"-to:@{domain}\")\n else:\n # Emails from or to the domain\n parts.append(f\"(from:@{domain} OR to:@{domain})\")\n\n # Exclude domain\n if exclude_domain:\n parts.append(f\"-from:@{exclude_domain}\")\n parts.append(f\"-to:@{exclude_domain}\")\n\n # Free text query\n if query:\n parts.append(query)\n\n return \" \".join(parts)\n\n\ndef search_messages(\n query: str = None,\n domain: str = None,\n days: int = 14,\n internal_only: bool = False,\n max_results: int = 50\n) -> List[dict]:\n \"\"\"\n Search Gmail messages.\n\n Args:\n query: Search query\n domain: Filter by domain\n days: Days back to search\n internal_only: Only internal emails\n max_results: Maximum results\n\n Returns:\n List of message summaries\n \"\"\"\n service = get_gmail_service()\n\n # Build query\n q = build_search_query(\n query=query,\n domain=domain,\n days=days,\n internal_only=internal_only\n )\n\n print(f\"Gmail query: {q}\")\n\n try:\n # Search messages\n result = service.users().messages().list(\n userId='me',\n q=q,\n maxResults=max_results\n ).execute()\n\n messages = result.get('messages', [])\n\n if not messages:\n return []\n\n # Get message details\n processed = []\n for msg in messages:\n try:\n detail = service.users().messages().get(\n userId='me',\n id=msg['id'],\n format='metadata',\n metadataHeaders=['From', 'To', 'Subject', 'Date']\n ).execute()\n\n # Extract headers\n headers = {h['name']: h['value'] for h in detail.get('payload', {}).get('headers', [])}\n\n # Parse date\n date_str = headers.get('Date', '')\n try:\n date_dt = parsedate_to_datetime(date_str)\n date_iso = date_dt.isoformat()\n date_display = date_dt.strftime('%Y-%m-%d %H:%M')\n except Exception:\n date_iso = date_str\n date_display = date_str\n\n processed.append({\n 'id': msg['id'],\n 'thread_id': msg.get('threadId'),\n 'subject': headers.get('Subject', 'No subject'),\n 'from': headers.get('From', ''),\n 'to': headers.get('To', ''),\n 'date': date_iso,\n 'date_display': date_display,\n 'snippet': detail.get('snippet', ''),\n 'labels': detail.get('labelIds', [])\n })\n except HttpError:\n continue\n\n return processed\n\n except HttpError as e:\n raise GmailSearchError(f\"Gmail API error: {e}\")\n\n\ndef get_thread(thread_id: str, include_body: bool = False) -> dict:\n \"\"\"\n Get full email thread.\n\n Args:\n thread_id: Thread ID\n include_body: Whether to include message bodies\n\n Returns:\n Thread data with all messages\n \"\"\"\n service = get_gmail_service()\n\n try:\n thread = service.users().threads().get(\n userId='me',\n id=thread_id,\n format='full' if include_body else 'metadata'\n ).execute()\n\n messages = []\n for msg in thread.get('messages', []):\n headers = {h['name']: h['value'] for h in msg.get('payload', {}).get('headers', [])}\n\n message_data = {\n 'id': msg['id'],\n 'from': headers.get('From', ''),\n 'to': headers.get('To', ''),\n 'subject': headers.get('Subject', ''),\n 'date': headers.get('Date', ''),\n 'snippet': msg.get('snippet', '')\n }\n\n # Extract body if requested\n if include_body:\n body = extract_body(msg.get('payload', {}))\n message_data['body'] = body[:2000] if body else ''\n\n messages.append(message_data)\n\n return {\n 'thread_id': thread_id,\n 'message_count': len(messages),\n 'messages': messages\n }\n\n except HttpError as e:\n raise GmailSearchError(f\"Failed to get thread: {e}\")\n\n\ndef extract_body(payload: dict) -> str:\n \"\"\"Extract plain text body from email payload.\"\"\"\n if 'body' in payload and payload['body'].get('data'):\n return base64.urlsafe_b64decode(payload['body']['data']).decode('utf-8', errors='ignore')\n\n if 'parts' in payload:\n for part in payload['parts']:\n if part.get('mimeType') == 'text/plain':\n if 'body' in part and part['body'].get('data'):\n return base64.urlsafe_b64decode(part['body']['data']).decode('utf-8', errors='ignore')\n elif 'parts' in part:\n # Nested multipart\n body = extract_body(part)\n if body:\n return body\n\n return ''\n\n\ndef summarize_emails(messages: List[dict], client_domain: str = None) -> dict:\n \"\"\"\n Summarize emails for client overview.\n\n Args:\n messages: List of processed messages\n client_domain: Client's email domain for categorization\n\n Returns:\n Summary dict\n \"\"\"\n external = [] # With client\n internal = [] # About client (internal)\n\n for msg in messages:\n from_addr = msg.get('from', '').lower()\n to_addr = msg.get('to', '').lower()\n\n if client_domain:\n if client_domain.lower() in from_addr or client_domain.lower() in to_addr:\n external.append(msg)\n else:\n internal.append(msg)\n else:\n external.append(msg)\n\n return {\n 'total': len(messages),\n 'external_threads': external[:10],\n 'internal_mentions': internal[:10],\n 'external_count': len(external),\n 'internal_count': len(internal)\n }\n\n\ndef main():\n \"\"\"CLI entry point.\"\"\"\n parser = argparse.ArgumentParser(\n description=\"Search Gmail for emails\"\n )\n\n parser.add_argument(\"--query\", \"-q\", help=\"Search query\")\n parser.add_argument(\"--domain\", \"-d\", help=\"Filter by email domain\")\n parser.add_argument(\"--days\", type=int, default=14, help=\"Days back to search\")\n parser.add_argument(\"--internal-only\", action=\"store_true\",\n help=\"Only internal emails (exclude external domain)\")\n parser.add_argument(\"--limit\", type=int, default=50, help=\"Max results\")\n parser.add_argument(\"--thread\", help=\"Get specific thread by ID\")\n parser.add_argument(\"--json\", action=\"store_true\", help=\"Output as JSON\")\n\n args = parser.parse_args()\n\n if not args.query and not args.domain and not args.thread:\n parser.print_help()\n print(\"\\nError: Specify --query, --domain, or --thread\")\n return 1\n\n try:\n if args.thread:\n print(f\"Getting thread: {args.thread}\")\n thread = get_thread(args.thread, include_body=True)\n\n if args.json:\n print(json.dumps(thread, indent=2))\n else:\n print(f\"\\nThread: {thread['message_count']} messages\")\n for msg in thread['messages']:\n print(f\"\\n[{msg['date']}] {msg['from']}\")\n print(f\"Subject: {msg['subject']}\")\n print(f\"Snippet: {msg['snippet'][:100]}...\")\n\n return 0\n\n print(f\"Searching Gmail (last {args.days} days)...\")\n\n messages = search_messages(\n query=args.query,\n domain=args.domain,\n days=args.days,\n internal_only=args.internal_only,\n max_results=args.limit\n )\n\n if args.json:\n summary = summarize_emails(messages, args.domain)\n print(json.dumps(summary, indent=2))\n return 0\n\n if not messages:\n print(\"No emails found\")\n return 0\n\n print(f\"\\n=== Gmail Search Results ===\")\n print(f\"Found: {len(messages)} emails\")\n print()\n\n for msg in messages[:20]:\n print(f\"[{msg['date_display']}] {msg['subject'][:50]}\")\n print(f\" From: {msg['from'][:40]}\")\n print(f\" Snippet: {msg['snippet'][:60]}...\")\n print()\n\n return 0\n\n except GmailSearchError as e:\n print(f\"Error: {e}\")\n return 1\n except Exception as e:\n print(f\"Unexpected error: {e}\")\n import traceback\n traceback.print_exc()\n return 1\n\n\nif __name__ == \"__main__\":\n exit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":12093,"content_sha256":"0413c4868171a5bba58164f62f5b21c5e4e29f407f9c059388d4698df6a6b682"},{"filename":"scripts/google_calendar_search.py","content":"#!/usr/bin/env python3\n\"\"\"\nGoogle Calendar Event Search\n\nSearch for calendar events by query, date range, and attendees.\nUsed for client overview and meeting history.\n\nDirective: directives/google_calendar_search.md\n\nUsage:\n # Search for events mentioning a client\n python execution/google_calendar_search.py \"Microsoft\" --days-back 14 --days-forward 14\n\n # Search by attendee domain\n python execution/google_calendar_search.py --domain \"microsoft.com\" --days-back 30\n\n # List upcoming events\n python execution/google_calendar_search.py --upcoming --days-forward 7\n\"\"\"\n\nimport os\nimport sys\nimport json\nimport argparse\nimport pickle\nfrom datetime import datetime, timedelta\nfrom pathlib import Path\nfrom typing import Optional, List\nfrom dotenv import load_dotenv\n\nfrom google.auth.transport.requests import Request\nfrom google.oauth2.credentials import Credentials\nfrom google_auth_oauthlib.flow import InstalledAppFlow\nfrom googleapiclient.discovery import build\nfrom googleapiclient.errors import HttpError\n\n# Load environment variables\nload_dotenv()\n\n# OAuth scopes for Calendar\nSCOPES = ['https://www.googleapis.com/auth/calendar.readonly']\n\n# Credentials files\nCLIENT_SECRETS_FILE = \"client_secrets.json\"\nCALENDAR_TOKEN_FILE = \"calendar_token.pickle\"\n\n\nclass CalendarSearchError(Exception):\n \"\"\"Custom exception for calendar search operations.\"\"\"\n pass\n\n\ndef get_calendar_service():\n \"\"\"\n Authenticate and get Google Calendar service.\n\n Returns:\n Google Calendar API service\n \"\"\"\n creds = None\n\n # Load saved credentials\n if Path(CALENDAR_TOKEN_FILE).exists():\n with open(CALENDAR_TOKEN_FILE, 'rb') as token:\n creds = pickle.load(token)\n\n # Refresh or get new credentials\n if not creds or not creds.valid:\n if creds and creds.expired and creds.refresh_token:\n print(\"Refreshing Calendar credentials...\")\n creds.refresh(Request())\n else:\n if not Path(CLIENT_SECRETS_FILE).exists():\n raise CalendarSearchError(\n f\"{CLIENT_SECRETS_FILE} not found. \"\n \"Download OAuth credentials from Google Cloud Console.\"\n )\n print(\"First time Calendar auth - opening browser...\")\n flow = InstalledAppFlow.from_client_secrets_file(\n CLIENT_SECRETS_FILE, SCOPES\n )\n creds = flow.run_local_server(port=0)\n\n # Save credentials\n with open(CALENDAR_TOKEN_FILE, 'wb') as token:\n pickle.dump(creds, token)\n print(\"Calendar credentials saved.\")\n\n return build('calendar', 'v3', credentials=creds)\n\n\ndef search_events(\n query: str = None,\n attendee_domain: str = None,\n days_back: int = 14,\n days_forward: int = 14,\n calendar_id: str = 'primary',\n max_results: int = 50\n) -> List[dict]:\n \"\"\"\n Search for calendar events.\n\n Args:\n query: Text to search for in event title/description\n attendee_domain: Filter by attendee email domain\n days_back: Days in the past to search\n days_forward: Days in the future to search\n calendar_id: Calendar ID (default: primary)\n max_results: Maximum events to return\n\n Returns:\n List of event dictionaries\n \"\"\"\n service = get_calendar_service()\n\n # Calculate time range\n now = datetime.utcnow()\n time_min = (now - timedelta(days=days_back)).isoformat() + 'Z'\n time_max = (now + timedelta(days=days_forward)).isoformat() + 'Z'\n\n try:\n # Build query parameters\n params = {\n 'calendarId': calendar_id,\n 'timeMin': time_min,\n 'timeMax': time_max,\n 'maxResults': max_results,\n 'singleEvents': True,\n 'orderBy': 'startTime'\n }\n\n if query:\n params['q'] = query\n\n # Execute search\n result = service.events().list(**params).execute()\n events = result.get('items', [])\n\n # Process events\n processed = []\n for event in events:\n # Get start/end times\n start = event.get('start', {})\n end = event.get('end', {})\n start_dt = start.get('dateTime', start.get('date', ''))\n end_dt = end.get('dateTime', end.get('date', ''))\n\n # Parse datetime for comparison\n if 'T' in start_dt:\n event_start = datetime.fromisoformat(start_dt.replace('Z', '+00:00'))\n else:\n event_start = datetime.strptime(start_dt, '%Y-%m-%d')\n\n # Get attendees\n attendees = []\n for att in event.get('attendees', []):\n attendees.append({\n 'email': att.get('email', ''),\n 'name': att.get('displayName', ''),\n 'response': att.get('responseStatus', 'unknown'),\n 'organizer': att.get('organizer', False)\n })\n\n # Filter by attendee domain if specified\n if attendee_domain:\n domain_match = any(\n attendee_domain.lower() in att['email'].lower()\n for att in attendees\n )\n if not domain_match:\n continue\n\n # Determine if past or upcoming\n is_past = event_start.replace(tzinfo=None) \u003c now\n\n processed.append({\n 'id': event.get('id'),\n 'summary': event.get('summary', 'No title'),\n 'description': event.get('description', '')[:500] if event.get('description') else '',\n 'start': start_dt,\n 'end': end_dt,\n 'is_past': is_past,\n 'location': event.get('location', ''),\n 'attendees': attendees,\n 'attendee_count': len(attendees),\n 'html_link': event.get('htmlLink', ''),\n 'organizer': event.get('organizer', {}).get('email', ''),\n 'status': event.get('status', 'confirmed')\n })\n\n return processed\n\n except HttpError as e:\n raise CalendarSearchError(f\"Calendar API error: {e}\")\n\n\ndef get_events_summary(events: List[dict]) -> dict:\n \"\"\"\n Summarize events into past and upcoming.\n\n Args:\n events: List of processed events\n\n Returns:\n Dict with past_meetings and upcoming_meetings\n \"\"\"\n past = [e for e in events if e['is_past']]\n upcoming = [e for e in events if not e['is_past']]\n\n return {\n 'past_meetings': sorted(past, key=lambda x: x['start'], reverse=True),\n 'upcoming_meetings': sorted(upcoming, key=lambda x: x['start']),\n 'total_past': len(past),\n 'total_upcoming': len(upcoming)\n }\n\n\ndef format_event_for_display(event: dict) -> str:\n \"\"\"Format a single event for display.\"\"\"\n # Parse date\n start = event['start']\n if 'T' in start:\n dt = datetime.fromisoformat(start.replace('Z', '+00:00'))\n date_str = dt.strftime('%Y-%m-%d %H:%M')\n else:\n date_str = start\n\n status = \"PAST\" if event['is_past'] else \"UPCOMING\"\n attendee_str = f\"{event['attendee_count']} attendees\" if event['attendee_count'] else \"no attendees\"\n\n return f\"[{status}] {date_str} - {event['summary']} ({attendee_str})\"\n\n\ndef main():\n \"\"\"CLI entry point.\"\"\"\n parser = argparse.ArgumentParser(\n description=\"Search Google Calendar events\"\n )\n\n parser.add_argument(\"query\", nargs=\"?\", help=\"Search query (event title/description)\")\n parser.add_argument(\"--domain\", help=\"Filter by attendee email domain\")\n parser.add_argument(\"--days-back\", type=int, default=14, help=\"Days in past to search\")\n parser.add_argument(\"--days-forward\", type=int, default=14, help=\"Days in future to search\")\n parser.add_argument(\"--calendar\", default=\"primary\", help=\"Calendar ID\")\n parser.add_argument(\"--limit\", type=int, default=50, help=\"Max results\")\n parser.add_argument(\"--upcoming\", action=\"store_true\", help=\"Only show upcoming events\")\n parser.add_argument(\"--past\", action=\"store_true\", help=\"Only show past events\")\n parser.add_argument(\"--json\", action=\"store_true\", help=\"Output as JSON\")\n\n args = parser.parse_args()\n\n try:\n print(\"Searching calendar...\")\n\n # Adjust time range based on flags\n days_back = 0 if args.upcoming else args.days_back\n days_forward = 0 if args.past else args.days_forward\n\n events = search_events(\n query=args.query,\n attendee_domain=args.domain,\n days_back=days_back,\n days_forward=days_forward,\n calendar_id=args.calendar,\n max_results=args.limit\n )\n\n if args.json:\n summary = get_events_summary(events)\n print(json.dumps(summary, indent=2))\n return 0\n\n if not events:\n print(\"No events found\")\n return 0\n\n summary = get_events_summary(events)\n\n print(f\"\\n=== Calendar Events ===\")\n if args.query:\n print(f\"Query: {args.query}\")\n if args.domain:\n print(f\"Domain filter: @{args.domain}\")\n print()\n\n if summary['past_meetings'] and not args.upcoming:\n print(f\"PAST MEETINGS ({summary['total_past']}):\")\n for event in summary['past_meetings'][:10]:\n print(f\" {format_event_for_display(event)}\")\n print()\n\n if summary['upcoming_meetings'] and not args.past:\n print(f\"UPCOMING MEETINGS ({summary['total_upcoming']}):\")\n for event in summary['upcoming_meetings'][:10]:\n print(f\" {format_event_for_display(event)}\")\n\n return 0\n\n except CalendarSearchError as e:\n print(f\"Error: {e}\")\n return 1\n except Exception as e:\n print(f\"Unexpected error: {e}\")\n import traceback\n traceback.print_exc()\n return 1\n\n\nif __name__ == \"__main__\":\n exit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":9992,"content_sha256":"5955ecb730a7b85b4a6e9b3009df5de29930e4249d7bafa1fd7f1dcaea5b09d1"},{"filename":"scripts/google_drive_upload.py","content":"#!/usr/bin/env python3\n\"\"\"\nGoogle Drive File Upload with OAuth\nSupports folder management, batch uploads, and sharing.\n\nUsage:\n # First time (opens browser for OAuth)\n python execution/google_drive_upload.py --file image.png --folder \"Clients/Acme/Images\"\n\n # Subsequent runs (uses saved credentials)\n python execution/google_drive_upload.py --files *.png --folder \"Test\" --share anyone\n\n # Create folder structure\n python execution/google_drive_upload.py --create-folder \"Clients/NewClient/Assets/2025\"\n\n # Upload with custom name\n python execution/google_drive_upload.py --file image.png --folder \"Test\" --name \"custom_name.png\"\n\"\"\"\n\nimport os\nimport sys\nimport argparse\nfrom pathlib import Path\nfrom datetime import datetime\nimport glob\nfrom pydrive2.auth import GoogleAuth\nfrom pydrive2.drive import GoogleDrive\n\n# Configuration\nSETTINGS_FILE = \"settings.yaml\"\nCREDENTIALS_FILE = \"mycreds.txt\"\nCLIENT_SECRETS_FILE = \"client_secrets.json\"\n\ndef validate_setup():\n \"\"\"Validate required files exist.\"\"\"\n if not Path(CLIENT_SECRETS_FILE).exists():\n raise FileNotFoundError(\n f\"{CLIENT_SECRETS_FILE} not found!\\n\\n\"\n \"Setup instructions:\\n\"\n \"1. Go to https://console.cloud.google.com\\n\"\n \"2. Enable Google Drive API\\n\"\n \"3. Create OAuth 2.0 credentials (Desktop app)\\n\"\n \"4. Download JSON and save as 'client_secrets.json'\\n\"\n )\n\n if not Path(SETTINGS_FILE).exists():\n raise FileNotFoundError(f\"{SETTINGS_FILE} not found! This should be in project root.\")\n\ndef authenticate():\n \"\"\"\n Authenticate with Google Drive using OAuth 2.0.\n\n Returns:\n GoogleDrive: Authenticated drive instance\n \"\"\"\n print(\"🔐 Authenticating with Google Drive...\")\n\n gauth = GoogleAuth()\n\n # Try to load saved credentials\n gauth.LoadCredentialsFile(CREDENTIALS_FILE)\n\n if gauth.credentials is None:\n # First time: open browser for OAuth\n print(\" First time setup - opening browser for authentication...\")\n print(\" Please log in and authorize the application.\")\n gauth.LocalWebserverAuth()\n elif gauth.access_token_expired:\n # Refresh expired token\n print(\" Refreshing expired credentials...\")\n gauth.Refresh()\n else:\n # Use saved credentials\n print(\" Using saved credentials...\")\n gauth.Authorize()\n\n # Save credentials for next run\n gauth.SaveCredentialsFile(CREDENTIALS_FILE)\n print(\"✅ Authentication successful!\")\n\n return GoogleDrive(gauth)\n\ndef find_folder_by_path(drive, folder_path, parent_id='root'):\n \"\"\"\n Find folder ID by path, traversing nested structure.\n\n Args:\n drive: GoogleDrive instance\n folder_path: Path like \"Clients/Acme/Images\"\n parent_id: Parent folder ID (default: 'root' for My Drive)\n\n Returns:\n str: Folder ID or None if not found\n \"\"\"\n if not folder_path:\n return parent_id\n\n parts = folder_path.split('/')\n current_id = parent_id\n\n for folder_name in parts:\n # Search for folder in current parent\n query = f\"'{current_id}' in parents and title='{folder_name}' and mimeType='application/vnd.google-apps.folder' and trashed=false\"\n file_list = drive.ListFile({'q': query}).GetList()\n\n if file_list:\n current_id = file_list[0]['id']\n else:\n return None # Folder not found\n\n return current_id\n\ndef create_folder_structure(drive, folder_path, parent_id='root'):\n \"\"\"\n Create nested folder structure, creating missing folders.\n\n Args:\n drive: GoogleDrive instance\n folder_path: Path like \"Clients/Acme/Images\"\n parent_id: Parent folder ID\n\n Returns:\n str: ID of the final folder\n \"\"\"\n if not folder_path:\n return parent_id\n\n parts = folder_path.split('/')\n current_id = parent_id\n\n print(f\"📁 Creating folder structure: {folder_path}\")\n\n for folder_name in parts:\n # Check if folder exists\n query = f\"'{current_id}' in parents and title='{folder_name}' and mimeType='application/vnd.google-apps.folder' and trashed=false\"\n file_list = drive.ListFile({'q': query}).GetList()\n\n if file_list:\n # Folder exists\n current_id = file_list[0]['id']\n print(f\" ✓ {folder_name} (exists)\")\n else:\n # Create folder\n folder = drive.CreateFile({\n 'title': folder_name,\n 'parents': [{'id': current_id}],\n 'mimeType': 'application/vnd.google-apps.folder'\n })\n folder.Upload()\n current_id = folder['id']\n print(f\" ✅ {folder_name} (created)\")\n\n return current_id\n\ndef upload_file(drive, file_path, folder_id, custom_name=None, share=None):\n \"\"\"\n Upload file to Google Drive.\n\n Args:\n drive: GoogleDrive instance\n file_path: Local file path\n folder_id: Destination folder ID\n custom_name: Optional custom file name\n share: Sharing option ('anyone', '[email protected]', or None)\n\n Returns:\n dict: File metadata (id, name, webViewLink)\n \"\"\"\n file_path = Path(file_path)\n if not file_path.exists():\n raise FileNotFoundError(f\"File not found: {file_path}\")\n\n file_name = custom_name or file_path.name\n\n print(f\"📤 Uploading: {file_name}\")\n\n # Create file\n file = drive.CreateFile({\n 'title': file_name,\n 'parents': [{'id': folder_id}]\n })\n\n # Upload content\n file.SetContentFile(str(file_path))\n file.Upload()\n\n # Handle sharing\n if share:\n if share == 'anyone':\n file.InsertPermission({\n 'type': 'anyone',\n 'value': 'anyone',\n 'role': 'reader'\n })\n print(f\" 🔗 Shared with anyone (read-only)\")\n else:\n # Assume it's an email\n file.InsertPermission({\n 'type': 'user',\n 'value': share,\n 'role': 'writer'\n })\n print(f\" 🔗 Shared with {share}\")\n\n result = {\n 'id': file['id'],\n 'name': file['title'],\n 'webViewLink': file['alternateLink'],\n 'size': file.get('fileSize', 'unknown')\n }\n\n print(f\" ✅ Uploaded: {result['webViewLink']}\")\n\n return result\n\ndef main():\n \"\"\"Main execution function.\"\"\"\n parser = argparse.ArgumentParser(\n description=\"Upload files to Google Drive with folder management\",\n formatter_class=argparse.RawDescriptionHelpFormatter\n )\n\n # File arguments\n parser.add_argument(\"--file\", help=\"Single file to upload\")\n parser.add_argument(\"--files\", nargs=\"+\", help=\"Multiple files to upload\")\n parser.add_argument(\"--name\", help=\"Custom name for uploaded file (single file only)\")\n\n # Folder arguments\n parser.add_argument(\"--folder\", default=\"\", help=\"Destination folder path (e.g., 'Clients/Acme/Images')\")\n parser.add_argument(\"--create-folder\", help=\"Just create folder structure (no upload)\")\n parser.add_argument(\"--parent-id\", default=\"root\", help=\"Parent folder ID (default: My Drive root)\")\n\n # Sharing arguments\n parser.add_argument(\"--share\", help=\"Share with 'anyone' or specific email\")\n\n # Options\n parser.add_argument(\"--no-auto-create\", action=\"store_true\", help=\"Don't auto-create folders\")\n\n args = parser.parse_args()\n\n try:\n # Validate setup\n validate_setup()\n\n # Authenticate\n drive = authenticate()\n\n # Handle folder creation only\n if args.create_folder:\n print(f\"\\n📁 Creating folder: {args.create_folder}\")\n folder_id = create_folder_structure(drive, args.create_folder, args.parent_id)\n print(f\"\\n✅ Folder created with ID: {folder_id}\")\n return 0\n\n # Get file list\n files_to_upload = []\n if args.file:\n files_to_upload.append(args.file)\n if args.files:\n for pattern in args.files:\n files_to_upload.extend(glob.glob(pattern))\n\n if not files_to_upload:\n print(\"❌ No files specified. Use --file or --files\")\n return 1\n\n print(f\"\\n📋 Files to upload: {len(files_to_upload)}\")\n\n # Get or create destination folder\n if args.folder:\n folder_id = find_folder_by_path(drive, args.folder, args.parent_id)\n\n if folder_id is None:\n if args.no_auto_create:\n print(f\"❌ Folder not found: {args.folder}\")\n return 1\n else:\n folder_id = create_folder_structure(drive, args.folder, args.parent_id)\n else:\n print(f\"✓ Found folder: {args.folder}\")\n else:\n folder_id = args.parent_id\n\n # Upload files\n print(f\"\\n📤 Uploading to folder ID: {folder_id}\")\n results = []\n\n for file_path in files_to_upload:\n try:\n custom_name = args.name if (args.file and len(files_to_upload) == 1) else None\n result = upload_file(drive, file_path, folder_id, custom_name, args.share)\n results.append(result)\n except Exception as e:\n print(f\" ❌ Failed to upload {file_path}: {str(e)}\")\n\n # Summary\n print(f\"\\n✅ Upload complete!\")\n print(f\" Successfully uploaded: {len(results)}/{len(files_to_upload)} files\")\n\n if results:\n print(f\"\\n📋 Uploaded files:\")\n for result in results:\n print(f\" • {result['name']}: {result['webViewLink']}\")\n\n return 0\n\n except Exception as e:\n print(f\"❌ Error: {str(e)}\")\n import traceback\n traceback.print_exc()\n return 1\n\nif __name__ == \"__main__\":\n exit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":9911,"content_sha256":"b2c1d72fbaa6fc303b84a63c9fb29be477b5341af8fe013acf991c429c26541f"},{"filename":"scripts/settings.yaml","content":"client_config_backend: file\nclient_config_file: client_secrets.json\n\nsave_credentials: True\nsave_credentials_backend: file\nsave_credentials_file: mycreds.txt\n\nget_refresh_token: True\n\noauth_scope:\n - https://www.googleapis.com/auth/drive.readonly # Read all files (no write/delete)\n - https://www.googleapis.com/auth/drive.file # Create/edit/delete files created by app only\n - https://www.googleapis.com/auth/documents # Create/edit docs (Google API doesn't support delete)\n - https://www.googleapis.com/auth/spreadsheets # Create/edit sheets (Google API doesn't support delete)\n - https://www.googleapis.com/auth/presentations # Create/edit slides (Google API doesn't support delete)\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":696,"content_sha256":"ddaff8ae8e4eb232161d5307b134a99ef65ff56b5ca92d4fea85331fa45fce1a"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Google Workspace","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Overview","type":"text"}]},{"type":"paragraph","content":[{"text":"Interact with Google Drive, Gmail, Calendar, and Docs using OAuth authentication. Supports file uploads, folder management, email search, calendar search, and document operations.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Quick Decision Tree","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"What do you need?\n│\n├── Google Drive\n│ ├── Search files/folders → references/drive-search.md\n│ │ └── Script: scripts/gdrive_search.py\n│ │\n│ ├── Upload files → references/drive-upload.md\n│ │ └── Script: scripts/google_drive_upload.py\n│ │\n│ ├── Create folder structure → references/folder-structure.md\n│ │ └── Script: scripts/gdrive_folder_structure.py\n│ │\n│ ├── Create client folder → references/create-folder.md\n│ │ └── Script: scripts/create_client_folder.py\n│ │\n│ └── Search transcripts → references/transcript-search.md\n│ └── Script: scripts/gdrive_transcript_search.py\n│\n├── Gmail\n│ └── Search emails → references/gmail-search.md\n│ └── Script: scripts/gmail_search.py\n│\n└── Calendar\n └── Search meetings → references/calendar-search.md\n └── Script: scripts/google_calendar_search.py","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Environment Setup","type":"text"}]},{"type":"paragraph","content":[{"text":"OAuth credentials are stored locally after first authentication.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Required Files","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"client_secrets.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" - From Google Cloud Console","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"settings.yaml","type":"text","marks":[{"type":"code_inline"}]},{"text":" - PyDrive2 configuration","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"mycreds.txt","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Auto-generated OAuth tokens","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"First-Time Setup","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Go to Google Cloud Console","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Enable APIs: Drive, Gmail, Calendar, Docs","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Create OAuth 2.0 credentials (Desktop app)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Download as ","type":"text"},{"text":"client_secrets.json","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Run any script - browser opens for OAuth consent","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Common Usage","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Search Client Folder","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python scripts/gdrive_search.py folder \"Microsoft\"","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Upload Files","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python scripts/google_drive_upload.py --files *.png --folder \"Clients/Acme/Assets\"","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Search Emails","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python scripts/gmail_search.py --domain \"microsoft.com\" --days 14","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Search Calendar","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python scripts/google_calendar_search.py \"Microsoft\" --days-back 30","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"OAuth Scopes","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Scope","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Purpose","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"drive","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Full Drive access","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"spreadsheets","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Sheets access","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"documents","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Docs access","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"gmail.readonly","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Read emails","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"calendar.readonly","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Read calendar","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Cost","type":"text"}]},{"type":"paragraph","content":[{"text":"Free - Google Workspace APIs have generous free quotas.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Security Notes","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Credential Handling","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"client_secrets.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" - OAuth app credentials (never commit to git)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"mycreds.txt","type":"text","marks":[{"type":"code_inline"}]},{"text":" - User OAuth tokens (never commit to git, add to .gitignore)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"settings.yaml","type":"text","marks":[{"type":"code_inline"}]},{"text":" - PyDrive2 config (can be committed, no secrets)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Tokens auto-refresh; revoke via Google Account settings if compromised","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Never share OAuth credentials between users/machines","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Data Privacy","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Access to user's personal Google Drive, Gmail, and Calendar","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Files may contain confidential business information","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Email content is highly sensitive - minimize storage","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Calendar events may contain private meeting details","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Shared Drive access respects original permissions","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Access Scopes","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Request minimum required scopes:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"drive","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Full Drive access (read/write)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"drive.readonly","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Read-only Drive access (preferred when possible)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"spreadsheets","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Google Sheets access","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"documents","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Google Docs access","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"gmail.readonly","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Read-only email access","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"calendar.readonly","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Read-only calendar access","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Review/revoke access: https://myaccount.google.com/permissions","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Compliance Considerations","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"OAuth Consent","type":"text","marks":[{"type":"strong"}]},{"text":": Users explicitly consent to access scopes","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"GDPR","type":"text","marks":[{"type":"strong"}]},{"text":": Google Workspace data contains EU user PII","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Data Residency","type":"text","marks":[{"type":"strong"}]},{"text":": Google Workspace may have data residency requirements","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Shared Drives","type":"text","marks":[{"type":"strong"}]},{"text":": Respect organizational sharing policies","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Audit Trail","type":"text","marks":[{"type":"strong"}]},{"text":": Google Admin Console tracks API access","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Credential Security","type":"text","marks":[{"type":"strong"}]},{"text":": Store ","type":"text"},{"text":"client_secrets.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" securely, not in repos","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Troubleshooting","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Common Issues","type":"text"}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Issue: OAuth token expired","type":"text"}]},{"type":"paragraph","content":[{"text":"Symptoms:","type":"text","marks":[{"type":"strong"}]},{"text":" \"Invalid credentials\" or \"Token has been expired or revoked\" error ","type":"text"},{"text":"Cause:","type":"text","marks":[{"type":"strong"}]},{"text":" OAuth refresh token expired or revoked ","type":"text"},{"text":"Solution:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Delete ","type":"text"},{"text":"mycreds.txt","type":"text","marks":[{"type":"code_inline"}]},{"text":" file","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Re-run any script to trigger fresh OAuth flow","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Complete the browser authorization","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"New ","type":"text"},{"text":"mycreds.txt","type":"text","marks":[{"type":"code_inline"}]},{"text":" will be created automatically","type":"text"}]}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Issue: File not found","type":"text"}]},{"type":"paragraph","content":[{"text":"Symptoms:","type":"text","marks":[{"type":"strong"}]},{"text":" \"File not found\" error with valid file ID ","type":"text"},{"text":"Cause:","type":"text","marks":[{"type":"strong"}]},{"text":" No access to file, file deleted, or wrong file ID ","type":"text"},{"text":"Solution:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Verify file ID from the Google Drive URL","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Check file sharing permissions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Ensure OAuth user has access to the file","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Try accessing file directly in browser first","type":"text"}]}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Issue: Quota exceeded","type":"text"}]},{"type":"paragraph","content":[{"text":"Symptoms:","type":"text","marks":[{"type":"strong"}]},{"text":" \"User rate limit exceeded\" or \"Quota exceeded\" error ","type":"text"},{"text":"Cause:","type":"text","marks":[{"type":"strong"}]},{"text":" Too many API requests in 24-hour period ","type":"text"},{"text":"Solution:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Wait 24 hours for quota reset","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Create a new Google Cloud project with fresh quota","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Implement exponential backoff in scripts","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Reduce frequency of API calls","type":"text"}]}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Issue: settings.yaml missing","type":"text"}]},{"type":"paragraph","content":[{"text":"Symptoms:","type":"text","marks":[{"type":"strong"}]},{"text":" \"settings.yaml not found\" or PyDrive2 configuration error ","type":"text"},{"text":"Cause:","type":"text","marks":[{"type":"strong"}]},{"text":" Missing PyDrive2 configuration file ","type":"text"},{"text":"Solution:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Copy from template: ","type":"text"},{"text":"cp settings.yaml.example settings.yaml","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Ensure ","type":"text"},{"text":"client_secrets.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" path is correct in settings","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Verify save_credentials_backend is set to \"file\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Check settings.yaml is in the script's working directory","type":"text"}]}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Issue: client_secrets.json invalid","type":"text"}]},{"type":"paragraph","content":[{"text":"Symptoms:","type":"text","marks":[{"type":"strong"}]},{"text":" \"Invalid client secrets\" or OAuth configuration error ","type":"text"},{"text":"Cause:","type":"text","marks":[{"type":"strong"}]},{"text":" Malformed or incorrect OAuth credentials file ","type":"text"},{"text":"Solution:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Re-download from Google Cloud Console","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Ensure \"Desktop app\" type was selected when creating credentials","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Check JSON format is valid","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Verify redirect URIs are configured for local auth","type":"text"}]}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Issue: Scope access denied","type":"text"}]},{"type":"paragraph","content":[{"text":"Symptoms:","type":"text","marks":[{"type":"strong"}]},{"text":" \"Insufficient permission\" error ","type":"text"},{"text":"Cause:","type":"text","marks":[{"type":"strong"}]},{"text":" OAuth consent missing required scopes ","type":"text"},{"text":"Solution:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Delete ","type":"text"},{"text":"mycreds.txt","type":"text","marks":[{"type":"code_inline"}]},{"text":" to reset OAuth session","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Re-authenticate and accept all requested scopes","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Verify scopes in ","type":"text"},{"text":"settings.yaml","type":"text","marks":[{"type":"code_inline"}]},{"text":" match script requirements","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Check Google Cloud Console for scope restrictions","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Resources","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/drive-search.md","type":"text","marks":[{"type":"strong"}]},{"text":" - Search files and folders","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/drive-upload.md","type":"text","marks":[{"type":"strong"}]},{"text":" - Upload files to Drive","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/folder-structure.md","type":"text","marks":[{"type":"strong"}]},{"text":" - Create folder hierarchies","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/create-folder.md","type":"text","marks":[{"type":"strong"}]},{"text":" - Create client folders","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/transcript-search.md","type":"text","marks":[{"type":"strong"}]},{"text":" - Search transcript files","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/gmail-search.md","type":"text","marks":[{"type":"strong"}]},{"text":" - Search Gmail","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/calendar-search.md","type":"text","marks":[{"type":"strong"}]},{"text":" - Search calendar meetings","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Integration Patterns","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Drive to Video Production","type":"text"}]},{"type":"paragraph","content":[{"text":"Skills:","type":"text","marks":[{"type":"strong"}]},{"text":" google-workspace → video-production ","type":"text"},{"text":"Use case:","type":"text","marks":[{"type":"strong"}]},{"text":" Assemble course videos from Drive folder ","type":"text"},{"text":"Flow:","type":"text","marks":[{"type":"strong"}]}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Search Drive for video folder with lesson files","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Download videos via video-production scripts","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Stitch videos with title slides and upload final output","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Templates to Content","type":"text"}]},{"type":"paragraph","content":[{"text":"Skills:","type":"text","marks":[{"type":"strong"}]},{"text":" google-workspace → content-generation ","type":"text"},{"text":"Use case:","type":"text","marks":[{"type":"strong"}]},{"text":" Generate documents from branded templates ","type":"text"},{"text":"Flow:","type":"text","marks":[{"type":"strong"}]}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Load template from Drive (proposal, report format)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Generate content via content-generation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Create new Google Doc with formatted content","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Calendar to Transcripts","type":"text"}]},{"type":"paragraph","content":[{"text":"Skills:","type":"text","marks":[{"type":"strong"}]},{"text":" google-workspace → transcript-search ","type":"text"},{"text":"Use case:","type":"text","marks":[{"type":"strong"}]},{"text":" Find meeting recordings from calendar events ","type":"text"},{"text":"Flow:","type":"text","marks":[{"type":"strong"}]}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Search calendar for meetings with specific client","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Get meeting dates and titles","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Search transcript-search for matching recordings","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"google-workspace","author":"@skillopedia","source":{"stars":11,"repo_name":"casper-marketplace","origin_url":"https://github.com/casper-studios/casper-marketplace/blob/HEAD/casper/skills/google-workspace/SKILL.md","repo_owner":"casper-studios","body_sha256":"7e0f471348e39a090952fbecf9f84f00d4684799683c6708a4dd4e6a7550354e","cluster_key":"2ddf4bdff9865b9e1108dfccbb925bddb30a2221e334d4abcec1b014c07275b4","clean_bundle":{"format":"clean-skill-bundle-v1","source":"casper-studios/casper-marketplace/casper/skills/google-workspace/SKILL.md","attachments":[{"id":"53adac22-852d-588f-bc08-0f4ae6eb656a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/53adac22-852d-588f-bc08-0f4ae6eb656a/attachment.md","path":"references/calendar-search.md","size":13323,"sha256":"0c0c6f4e4f7d45171edd58f84ac321ffdd23aa660995e18efeb526a78b3f83cf","contentType":"text/markdown; charset=utf-8"},{"id":"6fd4a5e9-84c0-5465-9712-a5d2bcc7db83","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6fd4a5e9-84c0-5465-9712-a5d2bcc7db83/attachment.md","path":"references/create-folder.md","size":10188,"sha256":"8619a8162668b6bae96776469aa60510eaf89bfaf2a3519e95318a1206aebddb","contentType":"text/markdown; charset=utf-8"},{"id":"7f85a0b5-3a12-57e0-87ed-33cc06c039d9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7f85a0b5-3a12-57e0-87ed-33cc06c039d9/attachment.md","path":"references/drive-search.md","size":9038,"sha256":"4329028dcb267853722121912b05246cbe889ceeb9d569b4099a339c09dbe9d0","contentType":"text/markdown; charset=utf-8"},{"id":"063e0f09-0c07-50e4-90c6-0a41485a7d5a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/063e0f09-0c07-50e4-90c6-0a41485a7d5a/attachment.md","path":"references/drive-upload.md","size":9066,"sha256":"ffd103b19c8eb42a3fbd9523ca60623604fee0e175ee7c6cec93324ec82ddb8f","contentType":"text/markdown; charset=utf-8"},{"id":"b3f87f23-b1f2-5227-a21b-b220d129991a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b3f87f23-b1f2-5227-a21b-b220d129991a/attachment.md","path":"references/folder-structure.md","size":11359,"sha256":"cb6c959c81cb04576525c3f78007488e6419e8391394fb09ef5720d2daed91d3","contentType":"text/markdown; charset=utf-8"},{"id":"2883f933-41d4-531d-99df-6463c77b3642","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2883f933-41d4-531d-99df-6463c77b3642/attachment.md","path":"references/gmail-search.md","size":11490,"sha256":"531148911b7a99c40740c406baf6ba87abfaf80521f6e8b172cfc2f49a6816f6","contentType":"text/markdown; charset=utf-8"},{"id":"2496b066-3168-584e-a363-6a0955db7ff1","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2496b066-3168-584e-a363-6a0955db7ff1/attachment.md","path":"references/transcript-search.md","size":1261,"sha256":"3cdceceb0f7e18ab0a271e43f732401b4a545900e10825d0ca7d465332c4d61a","contentType":"text/markdown; charset=utf-8"},{"id":"a5fe5f1e-181c-5912-90dd-89d93db69a22","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a5fe5f1e-181c-5912-90dd-89d93db69a22/attachment.py","path":"scripts/create_client_folder.py","size":11420,"sha256":"1d7fd4de5f0f42da0d165b500ff9f1d1b4bcf93c878863cc38ca16f098efb688","contentType":"text/x-python; charset=utf-8"},{"id":"0b81c445-a048-50ab-9c2b-bc17595ec27f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0b81c445-a048-50ab-9c2b-bc17595ec27f/attachment.py","path":"scripts/gdrive_folder_structure.py","size":9289,"sha256":"30859e12db086164963bbfbf3703eafd875dbee72ba4d4e36a4107de303cf26c","contentType":"text/x-python; charset=utf-8"},{"id":"16964589-e0b0-5c68-9498-abd9c0bd9650","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/16964589-e0b0-5c68-9498-abd9c0bd9650/attachment.py","path":"scripts/gdrive_search.py","size":14781,"sha256":"dc5c5c7108f798e66d7b4b76293eb58c1ab1eb1df69b314a226e53e94e5f1eb6","contentType":"text/x-python; charset=utf-8"},{"id":"15c2543e-b2d1-51c1-83e0-9eba6aa7f73c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/15c2543e-b2d1-51c1-83e0-9eba6aa7f73c/attachment.py","path":"scripts/gdrive_transcript_search.py","size":12319,"sha256":"95a84e3e30f94e10834409f002d4f5370f04003dfcac9d0819625d7047d31a3d","contentType":"text/x-python; charset=utf-8"},{"id":"201f52cb-19fc-594b-a016-54aaf4c73629","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/201f52cb-19fc-594b-a016-54aaf4c73629/attachment.py","path":"scripts/gmail_search.py","size":12093,"sha256":"0413c4868171a5bba58164f62f5b21c5e4e29f407f9c059388d4698df6a6b682","contentType":"text/x-python; charset=utf-8"},{"id":"35cc7201-3d76-5170-af8f-4a02d60b2fa1","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/35cc7201-3d76-5170-af8f-4a02d60b2fa1/attachment.py","path":"scripts/google_calendar_search.py","size":9992,"sha256":"5955ecb730a7b85b4a6e9b3009df5de29930e4249d7bafa1fd7f1dcaea5b09d1","contentType":"text/x-python; charset=utf-8"},{"id":"f47ae088-208b-5daa-abe6-4f05a55a1e1e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f47ae088-208b-5daa-abe6-4f05a55a1e1e/attachment.py","path":"scripts/google_drive_upload.py","size":9911,"sha256":"b2c1d72fbaa6fc303b84a63c9fb29be477b5341af8fe013acf991c429c26541f","contentType":"text/x-python; charset=utf-8"},{"id":"66bdbb0d-34cc-541d-ad49-30304e240d69","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/66bdbb0d-34cc-541d-ad49-30304e240d69/attachment.yaml","path":"scripts/settings.yaml","size":696,"sha256":"ddaff8ae8e4eb232161d5307b134a99ef65ff56b5ca92d4fea85331fa45fce1a","contentType":"application/yaml; charset=utf-8"}],"bundle_sha256":"c719dafbfb380ee1db618041b430cbf6b169d2206d54358479557a2d1b240400","attachment_count":15,"text_attachments":15,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"casper/skills/google-workspace/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"documents-office","category_label":"Documents"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"documents-office","import_tag":"clean-skills-v1","description":"Google Drive, Gmail, Calendar, and Docs operations via OAuth. Use this skill when uploading files to Drive, searching Drive folders, searching Gmail, finding calendar meetings, creating Google Docs, or managing folder structures. Triggers on Google Workspace operations, file uploads, email search, calendar search, or document creation."}},"renderedAt":1782980086047}

Google Workspace Overview Interact with Google Drive, Gmail, Calendar, and Docs using OAuth authentication. Supports file uploads, folder management, email search, calendar search, and document operations. Quick Decision Tree Environment Setup OAuth credentials are stored locally after first authentication. Required Files - - From Google Cloud Console - - PyDrive2 configuration - - Auto-generated OAuth tokens First-Time Setup 1. Go to Google Cloud Console 2. Enable APIs: Drive, Gmail, Calendar, Docs 3. Create OAuth 2.0 credentials (Desktop app) 4. Download as 5. Run any script - browser opens…