Repository: gemini-cli-extensions/workspace Branch: main Commit: ad595af82240 Files: 124 Total size: 776.8 KB Directory structure: gitextract_ojj5tm71/ ├── .gemini/ │ └── skills/ │ └── code-reviewer/ │ └── SKILL.md ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── ci.yml │ ├── deploy-docs.yml │ ├── release.yml │ └── weekly-preview.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── CONTRIBUTING.md ├── GEMINI.md ├── LICENSE ├── README.md ├── SECURITY.md ├── cloud_function/ │ ├── index.js │ └── package.json ├── commands/ │ ├── calendar/ │ │ ├── clear-schedule.toml │ │ └── get-schedule.toml │ ├── drive/ │ │ └── search.toml │ └── gmail/ │ └── search.toml ├── docs/ │ ├── .vitepress/ │ │ └── config.mts │ ├── GCP-RECREATION.md │ ├── development.md │ ├── feature-configuration.md │ ├── index.md │ ├── release.md │ └── release_notes.md ├── eslint.config.js ├── gemini-extension.json ├── jest.config.js ├── package.json ├── scripts/ │ ├── auth-utils.js │ ├── clean.js │ ├── list-deps.js │ ├── print-scopes.ts │ ├── release.js │ ├── set-version.js │ ├── setup-gcp.sh │ ├── start.js │ ├── tsconfig.json │ └── utils/ │ └── dependencies.js ├── skills/ │ ├── gmail/ │ │ └── SKILL.md │ ├── google-calendar/ │ │ └── SKILL.md │ ├── google-chat/ │ │ └── SKILL.md │ ├── google-docs/ │ │ └── SKILL.md │ ├── google-sheets/ │ │ └── SKILL.md │ └── google-slides/ │ └── SKILL.md ├── tsconfig.json └── workspace-server/ ├── .github/ │ └── workflows/ │ ├── ci.yml │ └── release.yml ├── WORKSPACE-Context.md ├── esbuild.auth-utils.js ├── esbuild.config.js ├── esbuild.headless-login.js ├── jest.config.js ├── package.json ├── src/ │ ├── __tests__/ │ │ ├── auth/ │ │ │ ├── AuthManager.test.ts │ │ │ └── token-storage/ │ │ │ ├── base-token-storage.test.ts │ │ │ ├── file-token-storage.test.ts │ │ │ ├── hybrid-token-storage.test.ts │ │ │ ├── keychain-token-storage.test.ts │ │ │ └── oauth-credential-storage.test.ts │ │ ├── features/ │ │ │ ├── feature-config.test.ts │ │ │ └── feature-resolver.test.ts │ │ ├── mocks/ │ │ │ └── wasm.js │ │ ├── services/ │ │ │ ├── CalendarService.test.ts │ │ │ ├── CalendarValidation.test.ts │ │ │ ├── ChatService.test.ts │ │ │ ├── DocsService.comments.test.ts │ │ │ ├── DocsService.test.ts │ │ │ ├── DriveService.test.ts │ │ │ ├── GmailService.test.ts │ │ │ ├── PeopleService.test.ts │ │ │ ├── SheetsService.test.ts │ │ │ ├── SlidesService.test.ts │ │ │ └── TimeService.test.ts │ │ ├── setup.ts │ │ ├── tool-normalization.test.ts │ │ └── utils/ │ │ ├── DriveQueryBuilder.test.ts │ │ ├── IdUtils.test.ts │ │ ├── MimeHelper.test.ts │ │ ├── config.test.ts │ │ ├── logger.test.ts │ │ ├── paths.test.ts │ │ ├── secure-browser-launcher.test.ts │ │ └── validation.test.ts │ ├── auth/ │ │ ├── AuthManager.ts │ │ ├── scopes.ts │ │ └── token-storage/ │ │ ├── base-token-storage.ts │ │ ├── file-token-storage.ts │ │ ├── hybrid-token-storage.ts │ │ ├── index.ts │ │ ├── keychain-token-storage.ts │ │ ├── oauth-credential-storage.ts │ │ └── types.ts │ ├── cli/ │ │ └── headless-login.ts │ ├── features/ │ │ ├── feature-config.ts │ │ ├── feature-resolver.ts │ │ └── index.ts │ ├── index.ts │ ├── services/ │ │ ├── CalendarService.ts │ │ ├── CalendarValidation.ts │ │ ├── ChatService.ts │ │ ├── DocsService.ts │ │ ├── DriveService.ts │ │ ├── GmailService.ts │ │ ├── PeopleService.ts │ │ ├── SheetsService.ts │ │ ├── SlidesService.ts │ │ └── TimeService.ts │ └── utils/ │ ├── DriveQueryBuilder.ts │ ├── GaxiosConfig.ts │ ├── IdUtils.ts │ ├── MimeHelper.ts │ ├── config.ts │ ├── constants.ts │ ├── logger.ts │ ├── open-wrapper.ts │ ├── paths.ts │ ├── secure-browser-launcher.ts │ ├── tool-normalization.ts │ └── validation.ts ├── tsconfig.json └── tsconfig.test.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gemini/skills/code-reviewer/SKILL.md ================================================ --- name: code-reviewer description: Use this skill to review code. It supports both local changes (staged or working tree) and remote Pull Requests (by ID or URL). It focuses on correctness, maintainability, and adherence to project standards. --- # Code Reviewer This skill guides the agent in conducting professional and thorough code reviews for both local development and remote Pull Requests. ## Workflow ### 1. Determine Review Target - **Remote PR**: If the user provides a PR number or URL (e.g., "Review PR #123"), target that remote PR. - **Local Changes**: If no specific PR is mentioned, or if the user asks to "review my changes", target the current local file system states (staged and unstaged changes). ### 2. Preparation #### For Remote PRs: 1. **Checkout**: Use the GitHub CLI to checkout the PR. ```bash gh pr checkout ``` 2. **Verification**: Execute the workspace's verification suite to catch issues early. Capture the output of these commands to inform your review (e.g., note any failed tests or linting errors). ```bash npm install npm run build npm run test npm run lint npm run format:check ``` 3. **Context**: Read the PR description and any existing comments to understand the goal and history. #### For Local Changes: 1. **Identify Changes**: - Check status: `git status` - Read diffs: `git diff` (working tree) and/or `git diff --staged` (staged). 2. **Verification**: Ask the user if they want to run the verification suite before reviewing. If yes, run the same commands as for remote PRs. ### 3. In-Depth Analysis Analyze the code changes based on the following pillars: - **Correctness**: Does the code achieve its stated purpose without bugs or logical errors? - **Maintainability**: Is the code clean, well-structured, and easy to understand and modify in the future? Consider factors like code clarity, modularity, and adherence to established design patterns. - **Readability**: Is the code well-commented (where necessary) and consistently formatted according to our project's coding style guidelines? - **Efficiency**: Are there any obvious performance bottlenecks or resource inefficiencies introduced by the changes? - **Security**: Are there any potential security vulnerabilities or insecure coding practices? - **Edge Cases and Error Handling**: Does the code appropriately handle edge cases and potential errors? - **Testability**: Is the new or modified code adequately covered by tests (even if preflight checks pass)? Suggest additional test cases that would improve coverage or robustness. ### 4. Draft Feedback (DO NOT FIX) **IMPORTANT**: You are a reviewer, NOT a fixer. Do NOT attempt to fix the code yourself. Your goal is to provide high-quality feedback. #### Structure - **Summary**: A high-level overview of the review. - **Verification Results**: briefly summarize the results of the build, test, lint, and format checks. - **Findings**: - **Critical**: Bugs, security issues, test failures, lint errors, or breaking changes. - **Improvements**: Suggestions for better code quality or performance. - **Nitpicks**: Formatting or minor style issues (optional). - **Conclusion**: Clear recommendation (Approved / Request Changes). #### Tone - Be constructive, professional, and friendly. - Explain _why_ a change is requested. - For approvals, acknowledge the specific value of the contribution. ### 5. Interactive Refinement 1. **Present Draft**: Show the drafted review feedback to the user. 2. **Solicit Input**: Ask the user for their thoughts. - "Does this feedback look accurate?" - "Is the tone appropriate?" - "Did I miss any context?" 3. **Iterate**: If the user provides suggestions or corrections, update the draft feedback accordingly. 4. **Finalize**: specific approval from the user is not strictly required if they don't have further comments, but ensure they have a chance to respond. ### 6. Cleanup (Remote PRs only) - After the review is complete, ask the user if they want to switch back to the default branch (e.g., `main` or `master`). ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: 'npm' directory: '/' schedule: interval: 'weekly' open-pull-requests-limit: 5 groups: typescript: patterns: - 'typescript' npm-root: patterns: - '*' labels: - 'dependencies' - 'npm' commit-message: prefix: 'chore' include: 'scope' - package-ecosystem: 'npm' directory: '/workspace-server' schedule: interval: 'weekly' open-pull-requests-limit: 5 groups: npm-workspace-server: patterns: - '*' labels: - 'dependencies' - 'npm' commit-message: prefix: 'chore' include: 'scope' - package-ecosystem: 'github-actions' directory: '/' schedule: interval: 'weekly' open-pull-requests-limit: 5 groups: github-actions: patterns: - '*' labels: - 'dependencies' - 'github-actions' commit-message: prefix: 'chore' include: 'scope' ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: [main, develop] pull_request: branches: [main] jobs: verify: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Use Node.js 20.x uses: actions/setup-node@v6 with: node-version: '20.x' cache: 'npm' cache-dependency-path: package-lock.json - name: Install dependencies run: npm ci - name: Run linter run: npm run lint - name: Run Prettier check run: npm run format:check - name: Run type checking run: npx tsc --noEmit --project workspace-server test: needs: verify runs-on: ${{ matrix.os }} strategy: matrix: node-version: [20.x, 22.x, 24.x] os: [ubuntu-latest, windows-latest, macos-latest] steps: - uses: actions/checkout@v6 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} cache: 'npm' cache-dependency-path: package-lock.json - name: Install libsecret (Linux) if: runner.os == 'Linux' run: sudo apt-get update && sudo apt-get install -y libsecret-1-0 - name: Install dependencies run: npm ci - name: Run tests with coverage run: npm run test:ci - name: Upload coverage to Codecov uses: codecov/codecov-action@v6 with: directory: ./workspace-server/coverage flags: unittests name: codecov-umbrella fail_ci_if_error: false build: runs-on: ubuntu-latest needs: test steps: - uses: actions/checkout@v6 - name: Use Node.js uses: actions/setup-node@v6 with: node-version: '20.x' cache: 'npm' cache-dependency-path: package-lock.json - name: Install dependencies run: npm ci - name: Build run: npm run build - name: Upload build artifacts uses: actions/upload-artifact@v7 with: name: dist path: workspace-server/dist/ security: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Run security audit run: npm audit --audit-level=moderate continue-on-error: true - name: Check for known vulnerabilities run: npx audit-ci --moderate continue-on-error: true ================================================ FILE: .github/workflows/deploy-docs.yml ================================================ name: Deploy Docs to GitHub Pages on: push: branches: - main jobs: build: runs-on: ubuntu-latest permissions: contents: write # For actions/checkout to fetch code pages: write # For uploading the artifact id-token: write # For OIDC authentication steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 # Not strictly needed but good practice for some build tools - name: Setup Node uses: actions/setup-node@v6 with: node-version: 20 cache: npm # Cache npm dependencies - name: Install dependencies run: npm install - name: Build docs run: npm run docs:build - name: Upload artifact uses: actions/upload-pages-artifact@v5 with: path: docs/.vitepress/dist # The directory where VitePress builds the docs deploy: environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest needs: build permissions: pages: write id-token: write steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v5 ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: tags: - 'v*' jobs: release: strategy: matrix: include: - os: ubuntu-latest platform: linux - os: macos-latest platform: darwin - os: windows-latest platform: win32 runs-on: ${{ matrix.os }} permissions: contents: write steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 persist-credentials: false - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: '20' - name: Install dependencies run: npm ci - name: Build extension run: npm run build --workspace=workspace-server - name: Create release assets run: npm run release -- --platform=${{ matrix.platform }} - name: Release uses: softprops/action-gh-release@v3 if: startsWith(github.ref, 'refs/tags/') with: files: release/${{ matrix.platform }}.google-workspace-extension.tar.gz ================================================ FILE: .github/workflows/weekly-preview.yml ================================================ name: Weekly Preview Release on: schedule: # Every Monday at 09:00 UTC - cron: '0 9 * * 1' workflow_dispatch: jobs: prepare: runs-on: ubuntu-latest outputs: tag_name: ${{ steps.date.outputs.tag_name }} steps: - name: Get current date id: date run: echo "tag_name=preview-$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT create-release: needs: prepare runs-on: ubuntu-latest permissions: contents: write steps: - name: Create Release uses: softprops/action-gh-release@v3 with: tag_name: ${{ needs.prepare.outputs.tag_name }} name: Weekly Preview ${{ needs.prepare.outputs.tag_name }} prerelease: true release: needs: [prepare, create-release] name: Build and Release strategy: matrix: include: - os: ubuntu-latest platform: linux - os: macos-latest platform: darwin - os: windows-latest platform: win32 runs-on: ${{ matrix.os }} permissions: contents: write steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 persist-credentials: false - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: '20' - name: Install dependencies run: npm ci - name: Build extension run: npm run build --workspace=workspace-server - name: Create release assets shell: bash env: GITHUB_REF_NAME: ${{ needs.prepare.outputs.tag_name }} run: npm run release -- --platform=${{ matrix.platform }} - name: Upload Release Assets uses: softprops/action-gh-release@v3 with: tag_name: ${{ needs.prepare.outputs.tag_name }} files: release/${{ matrix.platform }}.google-workspace-extension.tar.gz ================================================ FILE: .gitignore ================================================ # macOS .DS_Store # logs logs # Dependencies node_modules/ # Build outputs dist/ # Environment files .env .env.local # Logs *.log # Coverage coverage/ # Editor files *.swp *.swo *~ .idea/ .vscode/ # Auth tokens token.json gemini-cli-workspace-token.json .gemini-cli-workspace-master-key commit_message.txt # Release directory release/ # VitePress docs/.vitepress/dist docs/.vitepress/cache ================================================ FILE: .prettierignore ================================================ node_modules dist coverage .vitepress/cache .vitepress/dist package-lock.json ================================================ FILE: .prettierrc.json ================================================ { "semi": true, "trailingComma": "all", "singleQuote": true, "printWidth": 80, "tabWidth": 2, "overrides": [ { "files": ["**/*.md"], "options": { "proseWrap": "always" } } ] } ================================================ FILE: CONTRIBUTING.md ================================================ # How to Contribute We would love to accept your patches and contributions to this project. ## Before you begin ### Sign our Contributor License Agreement Contributions to this project must be accompanied by a [Contributor License Agreement](https://cla.developers.google.com/about) (CLA). You (or your employer) retain the copyright to your contribution; this simply gives us permission to use and redistribute your contributions as part of the project. If you or your current employer have already signed the Google CLA (even if it was for a different project), you probably don't need to do it again. Visit to see your current agreements or to sign a new one. ### Review our Community Guidelines This project follows [Google's Open Source Community Guidelines](https://opensource.google/conduct/). ## Contribution Process ### Code Reviews All submissions, including submissions by project members, require review. We use [GitHub pull requests](https://docs.github.com/articles/about-pull-requests) for this purpose. ### Self Assigning Issues If you're looking for an issue to work on, check out our list of issues that are labeled ["help wanted"](https://github.com/gemini-cli-extensions/workspace/issues?q=is%3Aissue+state%3Aopen+label%3A%22help+wanted%22). To assign an issue to yourself, simply add a comment with the text `/assign`. The comment must contain only that text and nothing else. This command will assign the issue to you, provided it is not already assigned. Please note that you can have a maximum of 3 issues assigned to you at any given time. ### Pull Request Guidelines To help us review and merge your PRs quickly, please follow these guidelines. PRs that do not meet these standards may be closed. #### 1. Link to an Existing Issue All PRs should be linked to an existing issue in our tracker. This ensures that every change has been discussed and is aligned with the project's goals before any code is written. - **For bug fixes:** The PR should be linked to the bug report issue. - **For features:** The PR should be linked to the feature request or proposal issue that has been approved by a maintainer. If an issue for your change doesn't exist, please **open one first** and wait for feedback before you start coding. #### 2. Keep It Small and Focused We favor small, atomic PRs that address a single issue or add a single, self-contained feature. - **Do:** Create a PR that fixes one specific bug or adds one specific feature. - **Don't:** Bundle multiple unrelated changes (e.g., a bug fix, a new feature, and a refactor) into a single PR. Large changes should be broken down into a series of smaller, logical PRs that can be reviewed and merged independently. #### 3. Use Draft PRs for Work in Progress If you'd like to get early feedback on your work, please use GitHub's **Draft Pull Request** feature. This signals to the maintainers that the PR is not yet ready for a formal review but is open for discussion and initial feedback. #### 4. Ensure All Checks Pass Before submitting your PR, ensure that all automated checks are passing by running: ```bash npm run test && npm run lint && npm run format:check && npx tsc --noEmit --project workspace-server ``` This command runs all tests, linting, formatting, and type checks. #### 5. Write Clear Commit Messages and a Good PR Description Your PR should have a clear, descriptive title and a detailed description of the changes. Follow the [Conventional Commits](https://www.conventionalcommits.org/) standard for your commit messages. - **Good PR Title:** `feat(cli): Add --json flag to 'config get' command` - **Bad PR Title:** `Made some changes` In the PR description, explain the "why" behind your changes and link to the relevant issue (e.g., `Fixes #123`). ## Development Setup and Workflow For information on how to build, modify, and understand the development setup of this project, please see the [development documentation](docs/development.md). ================================================ FILE: GEMINI.md ================================================ This is a Gemini extension that provides tools for interacting with Google Workspace services like Google Docs. ### Building and Running - **Install dependencies:** `npm install` - **Build the project:** `npm run build --prefix workspace-server` ### Development Conventions This project uses TypeScript and the Model Context Protocol (MCP) SDK to create a Gemini extension. The main entry point is `src/index.ts`, which initializes the MCP server and registers the available tools. The business logic for each service is separated into its own file in the `src/services` directory. For example, `src/services/DocsService.ts` contains the logic for interacting with the Google Docs API. Authentication is handled by the `src/auth/AuthManager.ts` file, which uses the `@google-cloud/local-auth` library to obtain and refresh OAuth 2.0 credentials. ### Adding New Tools To add a new tool, you need to: 1. Add a new method to the appropriate service file in `src/services`. 2. In `src/index.ts`, register the new tool with the MCP server by calling `server.registerTool()`. You will need to provide a name for the tool, a description, and the input schema using the `zod` library. ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # Google Workspace Extension for Gemini CLI [![Build Status](https://github.com/gemini-cli-extensions/workspace/actions/workflows/ci.yml/badge.svg)](https://github.com/gemini-cli-extensions/workspace/actions/workflows/ci.yml) The Google Workspace extension for Gemini CLI brings the power of your Google Workspace apps to your command line. Manage your documents, spreadsheets, presentations, emails, chat, and calendar events without leaving your terminal. ## Prerequisites Before using the Google Workspace extension, you need to be logged into your Google account. ## Installation Install the Google Workspace extension by running the following command from your terminal: ```bash gemini extensions install https://github.com/gemini-cli-extensions/workspace ``` ## Usage Once the extension is installed, you can use it to interact with your Google Workspace apps. Here are a few examples: **Create a new Google Doc:** > "Create a new Google Doc with the title 'My New Doc' and the content '# My New > Document\n\nThis is a new document created from the command line.'" **List your upcoming calendar events:** > "What's on my calendar for today?" **Search for a file in Google Drive:** > "Find the file named 'my-file.txt' in my Google Drive." ## Commands This extension provides a variety of commands. Here are a few examples: ### Get Schedule **Command:** `/calendar:get-schedule [date]` Shows your schedule for today or a specified date. ### Search Drive **Command:** `/drive:search ` Searches your Google Drive for files matching the given query. ## Headless / Remote Environments If you're using the extension over SSH, WSL, Cloud Shell, or another environment without a local browser, you can authenticate using the headless login tool: ```bash npm run auth-utils -- login ``` This prints an OAuth URL you can open in any browser (local machine, phone, etc.). After signing in, paste the credentials JSON into the CLI. Credentials are read securely from `/dev/tty` and are never exposed to the AI model. See the [development docs](docs/development.md#headless--remote-environments) for more details. ## Deployment If you want to host your own version of this extension's infrastructure, see the [GCP Recreation Guide](docs/GCP-RECREATION.md). ## Resources - [Documentation](docs/index.md): Detailed documentation on all the available tools. - [GitHub Issues](https://github.com/gemini-cli-extensions/workspace/issues): Report bugs or request features. ## Important security consideration: Indirect Prompt Injection Risk When exposing any language model to untrusted data, there's a risk of an [indirect prompt injection attack](https://en.wikipedia.org/wiki/Prompt_injection). Agentic tools like Gemini CLI, connected to MCP servers, have access to a wide array of tools and APIs. This MCP server grants the agent the ability to read, modify, and delete your Google Account data, as well as other data shared with you. - Never use this with untrusted tools - Never include untrusted inputs into the model context. This includes asking Gemini CLI to process mail, documents, or other resources from unverified sources. - Untrusted inputs may contain hidden instructions that could hijack your CLI session. Attackers can then leverage this to modify, steal, or destroy your data. - Always carefully review actions taken by Gemini CLI on your behalf to ensure they are correct and align with your intentions. ## Contributing Contributions are welcome! Please read the [CONTRIBUTING.md](CONTRIBUTING.md) file for details on how to contribute to this project. ## 📄 Legal - **License**: [Apache License 2.0](LICENSE) - **Terms of Service**: [Terms of Service](https://policies.google.com/terms) - **Privacy Policy**: [Privacy Policy](https://policies.google.com/privacy) - **Security**: [Security Policy](SECURITY.md) ================================================ FILE: SECURITY.md ================================================ # Reporting Security Issues To report a security issue, please use [https://g.co/vulnz](https://g.co/vulnz). We use g.co/vulnz for our intake, and do coordination and disclosure here on GitHub (including using GitHub Security Advisory). The Google Security Team will respond within 5 working days of your report on g.co/vulnz. ================================================ FILE: cloud_function/index.js ================================================ /** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ // Import required packages const functions = require('@google-cloud/functions-framework'); const { SecretManagerServiceClient } = require('@google-cloud/secret-manager'); const axios = require('axios'); const { URL } = require('node:url'); // --- Configuration loaded from Environment Variables --- // These are set in the Google Cloud Function's configuration. // They may be absent on the initial deploy (before OAuth credentials exist) // and are set on the final deploy. Validation happens at request time. const CLIENT_ID = process.env.CLIENT_ID; const SECRET_NAME = process.env.SECRET_NAME; const REDIRECT_URI = process.env.REDIRECT_URI; // --- Configuration for local storage (used in instructions) --- const KEYCHAIN_SERVICE_NAME = 'gemini-cli-workspace-oauth'; const KEYCHAIN_ACCOUNT_NAME = 'main-account'; // --- END CONFIGURATION --- // Initialize the Secret Manager client const secretClient = new SecretManagerServiceClient(); /** * Helper function to access a secret from Secret Manager. */ async function getClientSecret() { try { const [version] = await secretClient.accessSecretVersion({ name: SECRET_NAME, }); const payload = version.payload.data.toString('utf8'); return payload; } catch (error) { console.error('Failed to access secret version:', error); throw new Error('Could not retrieve client secret.'); } } /** * Handles the OAuth 2.0 callback. * @param {Object} req Express request object. * @param {Object} res Express response object. */ async function handleCallback(req, res) { const code = req.query.code; const state = req.query.state; // The state is the base64 encoded local redirect URI if (!code) { console.error('Missing authorization code in request query parameters.'); return res.status(400).send('Error: Missing authorization code.'); } try { const clientSecret = await getClientSecret(); const tokenResponse = await axios.post( 'https://oauth2.googleapis.com/token', { client_id: CLIENT_ID, client_secret: clientSecret, code: code, grant_type: 'authorization_code', redirect_uri: REDIRECT_URI, }, ); const { access_token, refresh_token, expires_in, scope, token_type } = tokenResponse.data; // Calculate expiry_date (timestamp in milliseconds) const expiry_date = Date.now() + expires_in * 1000; // If state is present, decode it and decide whether to redirect or show manual page. if (state) { try { // SECURITY: Enforce a reasonable size limit on the state parameter to prevent DoS. if (state.length > 4096) { throw new Error('State parameter exceeds size limit of 4KB.'); } const payload = JSON.parse( Buffer.from(state, 'base64').toString('utf8'), ); // If not in manual mode and a URI is present, perform the redirect. if (payload && payload.manual === false && payload.uri) { const redirectUrl = new URL(payload.uri); // SECURITY: Validate the redirect URI to prevent open redirect attacks. if ( redirectUrl.hostname !== 'localhost' && redirectUrl.hostname !== '127.0.0.1' ) { throw new Error( `Invalid redirect hostname: ${redirectUrl.hostname}. Must be localhost or 127.0.0.1.`, ); } const finalUrl = redirectUrl; // Use the validated URL object finalUrl.searchParams.append('access_token', access_token); if (refresh_token) { finalUrl.searchParams.append('refresh_token', refresh_token); } finalUrl.searchParams.append('scope', scope); finalUrl.searchParams.append('token_type', token_type); finalUrl.searchParams.append('expiry_date', expiry_date.toString()); // SECURITY: Pass the CSRF token back to the client for validation. if (payload.csrf) { finalUrl.searchParams.append('state', payload.csrf); } return res.redirect(302, finalUrl.toString()); } } catch (e) { console.error( 'Error processing state or redirect. Falling back to manual page.', e, ); } } // --- Fallback to manual instructions --- const credentialsJson = JSON.stringify( { refresh_token: refresh_token, scope: scope, token_type: token_type, access_token: access_token, expiry_date: expiry_date, }, null, 2, ); // Pretty print JSON // 4. Display the JSON and add a copy button + instructions res.set('Content-Type', 'text/html'); res.status(200).send(` OAuth Token Generated

Success! Credentials Ready

Copy the JSON block below. You'll need to store this as the password/secret in your operating system's keychain.

Credentials JSON

Copied!

CLI Login (Recommended):

In your terminal, run:

node dist/headless-login.js

Then paste the JSON above when prompted. The CLI will securely store your credentials.

Advanced: Manual Keychain Storage
  1. Open your OS Keychain/Credential Manager.
  2. Create a new secure entry (e.g., a "Generic Password" on macOS, a "Windows Credential", or similar on Linux).
  3. Set the Service (or equivalent field) to: ${KEYCHAIN_SERVICE_NAME}
  4. Set the Account (or username field) to: ${KEYCHAIN_ACCOUNT_NAME}
  5. Paste the copied JSON into the Password/Secret field.
  6. Save the entry.

(If keychain is unavailable, the server falls back to an encrypted file, but keychain is recommended.)

`); } catch (error) { if (axios.isAxiosError(error) && error.response) { console.error('Error during token exchange:', error.response.data); } else { console.error( 'Error during token exchange:', error instanceof Error ? error.message : error, ); } res .status(500) .send( 'An error occurred during the token exchange. Check function logs for details.', ); } } /** * Handles token refresh. * Accepts a refresh_token and returns a new access_token. * @param {Object} req Express request object. * @param {Object} res Express response object. */ async function handleRefreshToken(req, res) { // Only accept POST requests if (req.method !== 'POST') { console.error('Invalid method for refreshToken:', req.method); return res.status(405).send('Method Not Allowed'); } const { refresh_token } = req.body; if (!refresh_token) { console.error('Missing refresh_token in request body'); return res .status(400) .send('Error: Missing refresh_token in request body.'); } try { const clientSecret = await getClientSecret(); const tokenResponse = await axios.post( 'https://oauth2.googleapis.com/token', { client_id: CLIENT_ID, client_secret: clientSecret, refresh_token: refresh_token, grant_type: 'refresh_token', }, ); const { access_token, expires_in, scope, token_type } = tokenResponse.data; // Calculate expiry_date (timestamp in milliseconds) const expiry_date = Date.now() + expires_in * 1000; // Return the new credentials // Note: Google does NOT return a new refresh_token on refresh // The client must preserve the original refresh_token res.status(200).json({ access_token, expiry_date, token_type, scope, }); } catch (error) { if (axios.isAxiosError(error) && error.response) { console.error('Error during token refresh:', error.response.data); res.status(error.response.status).json(error.response.data); } else { console.error( 'Error during token refresh:', error instanceof Error ? error.message : error, ); res.status(500).send('An error occurred during token refresh.'); } } } /** * Main entry point for the Cloud Function. * Routes requests to either the callback handler or the refresh handler. */ functions.http('oauthHandler', async (req, res) => { // Validate required environment variables at request time if (!CLIENT_ID || !SECRET_NAME || !REDIRECT_URI) { return res .status(503) .send( 'Function not yet configured. Missing required environment variables: CLIENT_ID, SECRET_NAME, REDIRECT_URI.', ); } // Route to refresh handler if path ends with /refresh or /refreshToken or it's a POST with refresh_token if ( ['/refresh', '/refreshToken'].includes(req.path) || (req.method === 'POST' && req.body?.refresh_token) ) { return handleRefreshToken(req, res); } // Route to callback handler if path ends with /callback or /oauth2callback or has 'code' query param if (['/callback', '/oauth2callback'].includes(req.path) || req.query.code) { return handleCallback(req, res); } // Default/Error case res .status(400) .send( 'Unknown request type. Expected OAuth callback or token refresh request.', ); }); ================================================ FILE: cloud_function/package.json ================================================ { "name": "oauth-handler", "version": "1.0.0", "main": "index.js", "dependencies": { "@google-cloud/functions-framework": "^3.0.0", "@google-cloud/secret-manager": "^5.0.0", "axios": "^1.15.0" } } ================================================ FILE: commands/calendar/clear-schedule.toml ================================================ description = "Clear all events for a specific date or range by deleting or declining them" prompt = """ Please help me clear my schedule for a specific date or date range. Follow these steps carefully: 1. **Identify User & Context:** - Call `people.getMe` to get my email address and details. - Call `time.getTimeZone` to get my local time zone. 2. **Determine Date Range:** - Parse the date or date range from the arguments: "{{args}}". - If no arguments are provided, use `time.getCurrentDate` to default to today. 3. **Fetch Events:** - Call `calendar.listEvents` for the determined date range using my primary calendar. - **Important:** Include ALL events in the list, even those often filtered out like "Commute Time", "DNS", personal blocks, or events where I am the only attendee. 4. **Review & Confirm (CRITICAL):** - List all the events found for that period in a clear, numbered list showing the Time and Title. - **Specifically call out any all-day events** and ask if I want to include those in the clear operation. - **STOP** and ask me for explicit confirmation before proceeding. Example: "I found 5 events (including 1 all-day event). Do you want me to clear all of them?" 5. **Execute Clearing:** - Once I confirm, iterate through each event in the list: - **If I am the organizer** (check `organizer.self` is true or `organizer.email` matches mine): - Call `calendar.deleteEvent` to remove it. - **If I am NOT the organizer**: - Call `calendar.respondToEvent` with `responseStatus` set to "declined". 6. **Final Report:** - Summarize the actions taken (e.g., "Deleted 2 events and declined 3 events."). """ ================================================ FILE: commands/calendar/get-schedule.toml ================================================ description = "Show your schedule for today, or the date specified" prompt = """ Please show me my schedule for today. To do that use the following steps: 1) Use the people.getMe tool to get my information. 2) Use the time.getTimeZone to get my local time zone. 3) Either use the time.getCurrentDate or the user supplied date here: {{args}} to understand the current date. 4) Call calendar.list to identify the calendar associated with me from step 1. 5) Call calendar.listEvents to identify events on my calendar for the date we determined in step 3. 6) Format the list of events in my local time zone from step 2 in the following format: > HH:MM - HH:MM: Event Title (attendance status) - Event Location > HH:MM - HH:MM: Event 2 Title (attendance status) - Event Location Note: If a calendar event has conflicting timezone information, prioritize the dateTime field over the timeZone field. """ ================================================ FILE: commands/drive/search.toml ================================================ description = "Searches Google Drive for files matching a query." prompt = """ You are tasked with searching Google Drive for files. 1) The user's search query is: {{args}} 2) Call the `drive.search` tool, passing the user's query as the `query` argument. 3) The `drive.search` tool returns a JSON object containing a list of files. 4) Format the result into a clear, readable list, showing only the `name` and `id` for each file found. 5) If no files are found, state that clearly. Example Output Format: > File Name: My Document (ID: 1a2b3c4d5e6f7g8h9i0j) > File Name: Project Report (ID: k1l2m3n4o5p6q7r8s9t0) """ ================================================ FILE: commands/gmail/search.toml ================================================ description = "Searches for emails in Gmail matching a query." prompt = """ You are tasked with searching Gmail for emails. 1) The user's search query is: {{args}} 2) Call the `gmail.search` tool, passing the user's query as the `query` argument. 3) The `gmail.search` tool returns a JSON object containing a list of emails (id and threadId). 4) For each email found, you MUST call `gmail.get` with the `messageId` and `format='metadata'` to get the From, Subject, and Snippet. 5) Format the result into a clear, readable list. 6) If no emails are found, state that clearly. Example Output Format: > From: sender@example.com Subject: Meeting Reminder Snippet: Don't forget about the meeting tomorrow... --- > From: newsletter@example.com Subject: Weekly News Snippet: Here are the top stories for this week... """ ================================================ FILE: docs/.vitepress/config.mts ================================================ import { defineConfig } from 'vitepress'; // https://vitepress.dev/reference/site-config export default defineConfig({ base: '/workspace/', title: 'Gemini Workspace Extension', description: 'Documentation for the Google Workspace Server Extension', themeConfig: { // https://vitepress.dev/reference/default-theme-config nav: [ { text: 'Home', link: '/' }, { text: 'Configuration', link: '/feature-configuration' }, { text: 'Development', link: '/development' }, { text: 'Release', link: '/release' }, { text: 'Release Notes', link: '/release_notes' }, ], sidebar: [ { text: 'Documentation', items: [ { text: 'Overview', link: '/' }, { text: 'Feature Configuration', link: '/feature-configuration' }, { text: 'Development Guide', link: '/development' }, { text: 'GCP Setup Guide', link: '/GCP-RECREATION' }, { text: 'Release Guide', link: '/release' }, { text: 'Release Notes', link: '/release_notes' }, ], }, ], socialLinks: [ { icon: 'github', link: 'https://github.com/gemini-cli-extensions/workspace', }, ], }, }); ================================================ FILE: docs/GCP-RECREATION.md ================================================ # Recreating the GCP Project This guide provides step-by-step instructions to recreate the Google Cloud Platform (GCP) project and infrastructure required for the Google Workspace Extension. ## Overview The extension uses a "Hybrid" OAuth flow for security: 1. **Local Client**: Requests authorization from the user. 2. **Cloud Function**: Acts as a secure proxy to exchange the authorization code for tokens. It holds the `CLIENT_SECRET` securely in Secret Manager. 3. **Secret Manager**: Stores the OAuth Client Secret. ## Prerequisites - A Google Cloud Project with billing enabled. - [Google Cloud CLI (gcloud)](https://cloud.google.com/sdk/docs/install) installed and authenticated. - Node.js and npm installed. ## Step 1: Run the Automated Setup Script The setup script handles the full infrastructure setup in the correct order, including guided configuration of the OAuth consent screen. 1. Set your project ID: ```bash gcloud config set project YOUR_PROJECT_ID ``` 2. Run the setup script: ```bash ./scripts/setup-gcp.sh ``` The script will: 1. Enable all required GCP APIs. 2. Guide you through configuring the **OAuth consent screen** with the required scopes and test users (opens the Cloud Console automatically). 3. Deploy the Cloud Function and display its URL. 4. Prompt you to create an **OAuth 2.0 Client ID** in the Google Cloud Console using the deployed function URL as the redirect URI. 5. Collect your Client ID and Client Secret. 6. Store the Client Secret in Secret Manager. 7. Update the Cloud Function with the OAuth configuration. 8. Grant the Cloud Function access to the secret. ## Step 2: Local Configuration After running the script, set the following environment variables in your shell (e.g., in `.zshrc` or `.bashrc`): ```bash export WORKSPACE_CLIENT_ID="your-client-id" export WORKSPACE_CLOUD_FUNCTION_URL="https://your-cloud-function-url" ``` The script will display the exact values to use. Alternatively, you can modify the `DEFAULT_CONFIG` in `workspace-server/src/utils/config.ts`. ## Why a Cloud Function? The extension uses a Cloud Function to protect your `CLIENT_SECRET`. - If the `CLIENT_SECRET` were included in the local extension code, anyone with access to the extension could steal it. - By using a Cloud Function, the secret stays in your GCP project and is only used server-side during the token exchange. - The local client only ever sees the resulting tokens, never the secret. ================================================ FILE: docs/development.md ================================================ # Development This document provides instructions for developing the Google Workspace extension. ## Development Setup and Workflow This section guides contributors on how to build, modify, and understand the development setup of this project. ### Setting Up the Development Environment **Prerequisites:** 1. **Node.js**: - **Development:** Please use Node.js `~20.19.0`. This specific version is required due to an upstream development dependency issue. You can use a tool like [nvm](https://github.com/nvm-sh/nvm) to manage Node.js versions. - **Production:** For running the CLI in a production environment, any version of Node.js `>=20` is acceptable. 2. **Git** ### Build Process To clone the repository: ```bash git clone https://github.com/gemini-cli-extensions/workspace.git # Or your fork's URL cd workspace ``` To install dependencies defined in `package.json` as well as root dependencies: ```bash npm install ``` To build the entire project (all packages): ```bash npm run build ``` This command typically compiles TypeScript to JavaScript, bundles assets, and prepares the packages for execution. Refer to `scripts/build.js` and `package.json` scripts for more details on what happens during the build. ### Running Tests This project contains unit tests. #### Unit Tests To execute the unit test suite for the project: ```bash npm run test ``` This will run tests located in the `workspace-server/src/__tests__` directory. Ensure tests pass before submitting any changes. For a more comprehensive check, it is recommended to run `npm run test && npm run lint`. To test a single file, you can pass its path from the project root as an argument. For example: ````bash npm run test -- workspace-server/src/__tests__/GmailService.test.ts ``` ### Linting and Style Checks To ensure code quality and formatting consistency, run the linter and tests: ```bash npm run test && npm run lint ```` This command will run ESLint, Prettier, all tests, and other checks as defined in the project's `package.json`. > [!TIP] After cloning create a git pre-commit hook file to ensure your commits > are always clean. > > ```bash > cat <<'EOF' > .git/hooks/pre-commit > #!/bin/sh > # Run tests and linting before commit > if ! (npm run test && npm run lint); then > echo "Pre-commit checks failed. Commit aborted." > exit 1 > fi > EOF > chmod +x .git/hooks/pre-commit > ``` #### Formatting To separately format the code in this project by running the following command from the root directory: ```bash npm run format ``` This command uses Prettier to format the code according to the project's style guidelines. #### Linting To separately lint the code in this project, run the following command from the root directory: ```bash npm run lint ``` #### Testing with Gemini CLI To test your code changes with Gemini CLI you can run: ```bash gemini extensions uninstall google-workspace npm install && npm run build gemini extensions link . gemini extensions list gemini --debug # Prompt to test your feature/bug fix ``` ### Coding Conventions - Please adhere to the coding style, patterns, and conventions used throughout the existing codebase. - Consult [GEMINI.md](https://github.com/gemini-cli-extensions/workspace/blob/main/GEMINI.md) (typically found in the project root) for specific instructions related to AI-assisted development, including conventions for comments, and Git usage. - **Imports:** Pay special attention to import paths. The project uses ESLint to enforce restrictions on relative imports between packages. ### Tool Naming Tool names in source use dot notation (e.g., `docs.create`) for logical grouping. By default, these are normalized to underscores at runtime (e.g., `docs_create`) for compatibility with a broader set of applications that use MCP including Google Antigravity. When the server is run as a Gemini CLI extension the `--use-dot-names` flag is used to maintain dot notation and avoid breaking existing configurations. ### Project Structure - `workspace-server/`: The main workspace for the MCP server. - `src/`: Contains the source code for the server. - `__tests__/`: Contains all the tests. - `auth/`: Handles authentication. - `cli/`: CLI tools (e.g., headless OAuth login). - `features/`: Feature configuration registry and resolver. See the [Feature Configuration](feature-configuration.md) docs. - `services/`: Contains the business logic for each service. - `utils/`: Contains utility functions. - `config/`: Contains configuration files. - `scripts/`: Utility scripts for building, testing, and development tasks. ## Authentication The extension uses OAuth 2.0 to authenticate with Google Workspace APIs. The `scripts/auth-utils.js` script provides a command-line interface to manage authentication credentials. ### Usage To use the script, run the following command: ```bash npm run auth-utils -- ``` ### Commands - `login`: Authenticate via headless OAuth flow (for SSH/WSL/Cloud Shell). Reads credentials securely from `/dev/tty` so they are not visible to AI models. - `clear`: Clear all authentication credentials. - `expire`: Force the access token to expire (for testing refresh). - `status`: Show current authentication status. - `help`: Show the help message. ### Headless / Remote Environments If you are running the server in an environment without a browser (SSH, WSL, Cloud Shell, VMs), authentication requires manual steps: 1. Run the login tool: ```bash npm run auth-utils -- login ``` Or, from the `workspace-server` directory: ```bash node dist/headless-login.js ``` 2. Open the printed OAuth URL in any browser (your local machine, phone, etc.). 3. Complete Google sign-in. The browser will display a credentials JSON block. 4. Copy the JSON and paste it into the CLI when prompted. The CLI reads input from `/dev/tty` (Unix) or `CON` (Windows) rather than process stdin, so credentials are never exposed to an AI model that may have spawned the process. Use `--force` to re-authenticate if credentials already exist. ### Token Storage The extension uses a **hybrid storage strategy** for OAuth credentials. It first attempts to use the OS-level secure storage (via the [keytar](https://github.com/atom/node-keytar) library). If the keychain is unavailable, it falls back to AES-256-GCM encrypted file storage. Credentials are stored under the service name `gemini-cli-workspace-oauth` with the account name `main-account`. #### OS Keychain (Primary) | Platform | Backend | How to find stored credentials | | ----------- | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | **macOS** | Keychain Access | Open **Keychain Access** → search for `gemini-cli-workspace-oauth` | | **Windows** | Windows Credential Manager | Start Menu → search **Credential Manager** → **Windows Credentials** → **Generic Credentials** → `gemini-cli-workspace-oauth` | | **Linux** | GNOME Keyring / KWallet (`libsecret`) | Use `secret-tool search service gemini-cli-workspace-oauth` or your desktop's keyring manager | #### Encrypted File Fallback When the OS keychain is not available (e.g., headless servers, containers, or CI environments), the extension stores credentials in an encrypted file within the extension's installation directory: | File | Purpose | | ---------------------------------- | ---------------------------------------------------- | | `gemini-cli-workspace-token.json` | AES-256-GCM encrypted token data | | `.gemini-cli-workspace-master-key` | 256-bit master key used to derive the encryption key | Both files are created with restrictive permissions (`0o600`) and their containing directory with `0o700`. The encryption key is derived from the master key using `scrypt` with a machine-specific salt. #### Forcing File Storage To bypass the OS keychain and always use encrypted file storage, set the environment variable: ```bash export GEMINI_CLI_WORKSPACE_FORCE_FILE_STORAGE=true ``` ================================================ FILE: docs/feature-configuration.md ================================================ # Feature Configuration The extension provides a feature configuration system that lets you control which services and scopes are enabled. Each Google Workspace service is split into **read** and **write** feature groups, giving you granular control over what the extension can access. ## Feature Groups | Service | Group | Scopes | Default | | ---------- | ----- | ----------------------------------------------------------------------------- | ------- | | `docs` | read | `documents` | ON | | `docs` | write | `documents` | ON | | `drive` | read | `drive.readonly` | ON | | `drive` | write | `drive` | ON | | `calendar` | read | `calendar.readonly` | ON | | `calendar` | write | `calendar` | ON | | `chat` | read | `chat.spaces.readonly`, `chat.messages.readonly`, `chat.memberships.readonly` | ON | | `chat` | write | `chat.spaces`, `chat.messages`, `chat.memberships` | ON | | `gmail` | read | `gmail.readonly` | ON | | `gmail` | write | `gmail.modify` | ON | | `people` | read | `userinfo.profile`, `directory.readonly` | ON | | `slides` | read | `presentations.readonly` | ON | | `slides` | write | `presentations` | **OFF** | | `sheets` | read | `spreadsheets.readonly` | ON | | `sheets` | write | `spreadsheets` | **OFF** | | `time` | read | _(none)_ | ON | | `tasks` | read | `tasks.readonly` | **OFF** | | `tasks` | write | `tasks` | **OFF** | **Read** groups contain tools with no side effects (search, get, list). **Write** groups contain tools that perform mutations (create, update, delete, send). Services whose write scopes aren't in the published GCP project (Slides write, Sheets write, Tasks) default to **OFF**. These can be enabled by contributors using their own GCP projects. ## Configuration via `WORKSPACE_FEATURE_OVERRIDES` Use the `WORKSPACE_FEATURE_OVERRIDES` environment variable to enable or disable feature groups and individual tools. ### Syntax ``` WORKSPACE_FEATURE_OVERRIDES="key:on|off,key:on|off,..." ``` Each entry is a comma-separated `key:value` pair where: - `key` is a feature group (e.g., `gmail.write`) or a tool name (e.g., `calendar.deleteEvent`) - `value` is `on` or `off` ### Group-Level Overrides Disable or enable entire feature groups: ```bash # Disable Gmail write tools (send, createDraft, modify, etc.) export WORKSPACE_FEATURE_OVERRIDES="gmail.write:off" # Disable all of Chat export WORKSPACE_FEATURE_OVERRIDES="chat.read:off,chat.write:off" # Enable experimental features (Slides write, Tasks) export WORKSPACE_FEATURE_OVERRIDES="slides.write:on,tasks.read:on,tasks.write:on" ``` ### Tool-Level Overrides Disable specific tools within an enabled group (subtractive only): ```bash # Keep calendar.write enabled but disable delete export WORKSPACE_FEATURE_OVERRIDES="calendar.deleteEvent:off" # Disable destructive Gmail tools while keeping modify/label tools export WORKSPACE_FEATURE_OVERRIDES="gmail.send:off,gmail.sendDraft:off" # Combine group and tool overrides export WORKSPACE_FEATURE_OVERRIDES="gmail.write:off,calendar.deleteEvent:off,slides.write:on" ``` ::: warning Tool-level overrides are **subtractive only**. You cannot use `tool:on` to enable a tool whose feature group is disabled. To enable tools, enable their parent feature group. ::: ### Precedence The configuration follows a three-layer precedence model: 1. **Baked-in defaults** — Current services default ON; experimental services default OFF 2. **Settings** — Future: overrides from the install-time settings UI 3. **`WORKSPACE_FEATURE_OVERRIDES`** — Highest precedence; overrides everything ### Effects When a feature group is disabled: - Its **tools are not registered** with the MCP server (clients won't see them) - Its **OAuth scopes are not requested** during authentication - If you re-enable a previously disabled feature, you may need to re-authenticate to grant the new scopes ## Tools by Feature Group ### `docs.read` - `docs.getSuggestions` - `docs.getText` ### `docs.write` - `docs.create` - `docs.writeText` - `docs.replaceText` - `docs.formatText` ### `drive.read` - `drive.getComments` - `drive.findFolder` - `drive.search` - `drive.downloadFile` ### `drive.write` - `drive.createFolder` - `drive.moveFile` - `drive.trashFile` - `drive.renameFile` ### `calendar.read` - `calendar.list` - `calendar.listEvents` - `calendar.getEvent` - `calendar.findFreeTime` ### `calendar.write` - `calendar.createEvent` - `calendar.updateEvent` - `calendar.respondToEvent` - `calendar.deleteEvent` ### `chat.read` - `chat.listSpaces` - `chat.findSpaceByName` - `chat.getMessages` - `chat.findDmByEmail` - `chat.listThreads` ### `chat.write` - `chat.sendMessage` - `chat.sendDm` - `chat.setUpSpace` ### `gmail.read` - `gmail.search` - `gmail.get` - `gmail.downloadAttachment` - `gmail.listLabels` ### `gmail.write` - `gmail.modify` - `gmail.batchModify` - `gmail.modifyThread` - `gmail.send` - `gmail.createDraft` - `gmail.sendDraft` - `gmail.createLabel` ### `people.read` - `people.getUserProfile` - `people.getMe` - `people.getUserRelations` ### `slides.read` - `slides.getText` - `slides.getMetadata` - `slides.getImages` - `slides.getSlideThumbnail` ### `sheets.read` - `sheets.getText` - `sheets.getRange` - `sheets.getMetadata` ### `time.read` - `time.getCurrentDate` - `time.getCurrentTime` - `time.getTimeZone` ## For Contributors When adding a new service or tools: 1. Define read and write feature group entries in `workspace-server/src/features/feature-config.ts` 2. Set the default state — **ON** for scopes in the published GCP project, **OFF** otherwise 3. Register your tools in `index.ts` as usual — the feature config wrapper automatically skips disabled tools This lets contributors develop and merge new features without being blocked by the published GCP project's scope configuration. Contributors can test with their own GCP projects by enabling the feature via `WORKSPACE_FEATURE_OVERRIDES`. ================================================ FILE: docs/index.md ================================================ # Google Workspace Extension Documentation This document provides an overview of the Google Workspace extension for Gemini CLI. ## Available Tools The extension provides the following tools: ### Authentication - `auth.clear`: Clears the authentication credentials, forcing a re-login on the next request. - `auth.refreshToken`: Manually triggers the token refresh process. ### Google Docs - `docs.create`: Creates a new Google Doc. - `docs.getSuggestions`: Retrieves suggested edits from a Google Doc. - `docs.getComments`: Retrieves comments from a Google Doc. - `docs.writeText`: Writes text to a Google Doc at a specified position. - `docs.getText`: Retrieves the text content of a Google Doc. - `docs.replaceText`: Replaces all occurrences of a given text with new text in a Google Doc. - `docs.formatText`: Applies formatting (bold, italic, headings, etc.) to text ranges in a Google Doc. ### Google Slides - `slides.getText`: Retrieves the text content of a Google Slides presentation. - `slides.getMetadata`: Gets metadata about a Google Slides presentation. - `slides.getImages`: Downloads all images embedded in a Google Slides presentation to a local directory. - `slides.getSlideThumbnail`: Downloads a thumbnail image for a specific slide in a Google Slides presentation to a local path. ### Google Sheets - `sheets.getText`: Retrieves the content of a Google Sheets spreadsheet. - `sheets.getRange`: Gets values from a specific range in a Google Sheets spreadsheet. - `sheets.getMetadata`: Gets metadata about a Google Sheets spreadsheet. ### Google Drive - `drive.search`: Searches for files and folders in Google Drive. - `drive.findFolder`: Finds a folder by name in Google Drive. - `drive.createFolder`: Creates a new folder in Google Drive. - `drive.downloadFile`: Downloads a file from Google Drive to a local path. - `drive.trashFile`: Moves a file or folder to the trash in Google Drive. - `drive.renameFile`: Renames a file or folder in Google Drive. ### Google Calendar - `calendar.list`: Lists all of the user's calendars. - `calendar.createEvent`: Creates a new event in a calendar. - `calendar.listEvents`: Lists events from a calendar. - `calendar.getEvent`: Gets the details of a specific calendar event. - `calendar.findFreeTime`: Finds a free time slot for multiple people to meet. - `calendar.updateEvent`: Updates an existing event in a calendar. - `calendar.respondToEvent`: Responds to a meeting invitation (accept, decline, or tentative). - `calendar.deleteEvent`: Deletes an event from a calendar. ### Google Chat - `chat.listSpaces`: Lists the spaces the user is a member of. - `chat.findSpaceByName`: Finds a Google Chat space by its display name. - `chat.sendMessage`: Sends a message to a Google Chat space. - `chat.getMessages`: Gets messages from a Google Chat space. - `chat.sendDm`: Sends a direct message to a user. - `chat.findDmByEmail`: Finds a Google Chat DM space by a user's email address. - `chat.listThreads`: Lists threads from a Google Chat space in reverse chronological order. - `chat.setUpSpace`: Sets up a new Google Chat space with a display name and a list of members. ### Gmail - `gmail.search`: Search for emails in Gmail using query parameters. - `gmail.get`: Get the full content of a specific email message. - `gmail.downloadAttachment`: Downloads an attachment from a Gmail message to a local file. - `gmail.modify`: Modify a Gmail message. - `gmail.batchModify`: Bulk modify up to 1,000 Gmail messages at once. - `gmail.modifyThread`: Modify labels on all messages in a Gmail thread. - `gmail.send`: Send an email message. - `gmail.createDraft`: Create a draft email message. - `gmail.sendDraft`: Send a previously created draft email. - `gmail.listLabels`: List all Gmail labels in the user's mailbox. - `gmail.createLabel`: Create a new Gmail label. ### Time - `time.getCurrentDate`: Gets the current date. Returns both UTC (for API use) and local time (for user display), along with the timezone. - `time.getCurrentTime`: Gets the current time. Returns both UTC (for API use) and local time (for user display), along with the timezone. - `time.getTimeZone`: Gets the local timezone. ### People - `people.getUserProfile`: Gets a user's profile information. - `people.getMe`: Gets the profile information of the authenticated user. - `people.getUserRelations`: Gets a user's relations (e.g., manager, spouse, assistant). Defaults to the authenticated user and supports filtering by relation type. ## Custom Commands The extension includes several pre-configured commands for common tasks: - `/calendar/get-schedule`: Show your schedule for today, or a specified date. - `/calendar/clear-schedule`: Clear all events for a specific date or range by deleting or declining them. - `/drive/search`: Searches Google Drive for files matching a query and displays their name and ID. - `/gmail/search`: Searches for emails in Gmail matching a query and displays the sender, subject, and snippet. ## Release Notes See the [Release Notes](release_notes.md) for details on new features and changes. ================================================ FILE: docs/release.md ================================================ # Release Process This project uses GitHub Actions to automate the release process. ## Prerequisites - [GitHub CLI](https://cli.github.com/) (`gh`) installed and authenticated. - Write permissions to the repository. ## Creating a Release To streamline the release process: 1. **Update Version**: Run the `set-version` script to update the version in `package.json` files. The `workspace-server` will now dynamically read its version from its `package.json`. ```bash npm run set-version #0.0.x for example ``` 2. **Commit Changes**: Commit the version bump and push the changes to `main` (either directly or via a PR). ```bash git commit -am "chore: bump version to " git push origin main ``` 3. **Create Release**: Use the `gh release create` command. This will trigger the GitHub Actions workflow to build the extension and attach the artifacts to the release. ```bash # Syntax: gh release create --generate-notes gh release create v --generate-notes ``` ### What happens next? 1. **GitHub Actions Trigger**: The `release.yml` workflow is triggered by the new tag. 2. **Build**: The workflow builds the project using `npm run build`. 3. **Package**: It creates a `workspace-server.tar.gz` file containing the extension. 4. **Upload**: The workflow uploads the tarball to the release you just created. ## Manual Release (Alternative) If you prefer not to use the CLI, you can also push a tag manually: ```bash git tag v1.0.0 git push origin v1.0.0 ``` This pushes the tag to GitHub, which triggers the release workflow to create a release and upload the artifacts. However, using `gh release create` is recommended as it allows you to easily generate release notes. ================================================ FILE: docs/release_notes.md ================================================ # Release Notes ## 0.0.8 (2026-05-01) ### New Features - **Google Calendar**: Added support for `eventType` (Out of Office, Focus Time, Working Location) in Calendar Service. - **Feature Configuration**: Introduced a new feature configuration service for scope-based toggles, allowing more granular control over available tools. ### Improvements - **Authentication**: Refactored OAuth scope management to use a single source of truth and deduplicate read scopes, improving security and consistency. - **Google Calendar**: Refactored validation logic to be independent of the service layer. - **Google Docs**: Improved `getText` and `getSuggestions` tools to include the document title in their output. ### Fixes - **Google Docs**: Resolved API errors in `docs.getText`. - **Windows Support**: Fixed issues with `npm run clean` and handled `npm.cmd` correctly on Windows. - **Documentation**: Fixed various documentation links and improved clarity of API usage. ### Chores - **Dependencies**: Major update to TypeScript 6.0.3 and various other dependency bumps (Vite 8, Hono 4.12, etc.). ## 0.0.7 (2026-03-11) ### Breaking Changes - **Google Sheets**: Removed `sheets.find` tool. Use `drive.search` with MIME type filter instead (e.g., `mimeType='application/vnd.google-apps.spreadsheet'`). - **Google Slides**: Removed `slides.find` tool. Use `drive.search` with MIME type filter instead (e.g., `mimeType='application/vnd.google-apps.presentation'`). ### Improvements - **Skills**: Renamed all skill directories to `google-*` prefix (e.g., `calendar` → `google-calendar`) to avoid slash command conflicts. - **Calendar Skill**: Added explicit `calendarId='primary'` mandate to prevent agents from omitting the required parameter. ### Skills - **Sheets Skill**: New skill with `drive.search` guidance for finding spreadsheets. - **Slides Skill**: New skill with `drive.search` guidance for finding presentations. - **Docs Skill**: Updated with `drive.search` guidance and removed stale `docs.find`/`docs.move` references from skill and documentation. ## 0.0.6 (2026-03-08) ### New Features - **Google Docs**: Parse rich smart chips (person, date, rich link) in document text output. - **Google Docs**: Added `getSuggestions` and `getComments` tools for reading document suggestions and comments. - **Google Docs**: Added `formatText` tool for applying rich formatting (bold, italic, headings, etc.) to text ranges. - **Google Calendar**: Added Google Meet link generation and Google Drive file attachment support for `createEvent` and `updateEvent`. - **Google Calendar**: Added `sendUpdates` parameter to `createEvent` for controlling attendee notifications. - **Google Drive**: Added `trashFile` tool to move files and folders to trash. - **Google Drive**: Added `renameFile` tool to rename files and folders. - **Gmail**: Added `batchModify` tool for bulk modifying up to 1,000 messages at once. - **Gmail**: Added `modifyThread` tool for modifying all messages in a Gmail thread. - **Gmail**: Added `threadId` support in `createDraft` for creating reply drafts. - **Authentication**: Added headless OAuth login for SSH, WSL, and Cloud Shell environments. ### Skills - **Gmail Skill**: Added rich HTML formatting guidance for email composition. - **Chat Skill**: Added Google Chat messaging and space management guidance. - **Docs Skill**: Added document formatting and simplified tool primitives. - **Calendar Skill**: Added consolidated calendar scheduling guidance. ### Fixes - **Docs**: Fixed recursion into nested child tabs in DocsService. - **Docs**: Polished `getSuggestions` and `getComments` output formatting. - **Drive**: Fixed shared drive file downloads. ### Documentation & Chores - Documented token storage locations (OS keychain and encrypted file fallback). - Updated tool reference documentation with latest features. - **Dependencies**: Updated MCP SDK, Hono, Google APIs, rollup, ajv, qs, and minimatch. ## 0.0.5 (2026-02-11) ### New Features - **Gmail**: Added `createLabel` tool to manage email labels. - **Slides**: Added `getImages` and `getSlideThumbnail` tools for better visual integration, and included slide IDs in `getMetadata` output. - **Drive**: Enhanced support for shared drives. - **Calendar**: Added support for event descriptions. - **GCP**: Added comprehensive documentation and automation for GCP project recreation. - **Logging**: Added authentication status updates via MCP logging for better observability. - **Tools**: Added annotations for read-only tools to improve agent interaction. ### Fixes - **Security**: Resolved esbuild vulnerability via vite override. - **Compatibility**: Normalised tool names to underscores for better compatibility with other agents (e.g., Cursor). - **Config**: Removed unused arguments in extension configuration. ### Documentation & Chores - **Formatting**: Updated context documentation with Chat-specific formatting instructions. - **Infrastructure**: Allowed `.gemini` directory in git and added Prettier to CI/CD pipeline. - **Dependencies**: Updated MCP SDK, Hono, Google APIs, and other core libraries. ## 0.0.4 (2026-01-05) ### New Features - **Google Drive**: Added `drive.createFolder` to create new folders. - **People**: Added `people.getUserRelations` to retrieve user relationships (manager, reports, etc.). - **Google Chat**: Added threading support to `chat.sendMessage` and `chat.sendDm`, and filtering by thread in `chat.getMessages`. - **Gmail**: Added `gmail.downloadAttachment` to download email attachments. - **Google Drive**: Added `drive.downloadFile` to download files from Google Drive. - **Calendar**: Added `calendar.deleteEvent` to delete calendar events. - **Google Docs**: Added support for Tabs in DocsService. ### Improvements - **Dependencies**: Updated various dependencies including `@googleapis/drive`, `google-googleapis`, and `jsdom`. - **CI/CD**: Added a weekly preview release workflow and updated GitHub Actions versions. - **Testing**: Added documentation for the testing process with Gemini CLI. ### Fixes - Fixed an issue where the `v` prefix was not stripped correctly in the release script. - Fixed an issue with invalid assignees in dependabot config. - Fixed log directory creation. ## 0.0.3 - Initial release with support for Google Docs, Sheets, Slides, Drive, Calendar, Gmail, Chat, Time, and People. ================================================ FILE: eslint.config.js ================================================ /** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ const tseslint = require('@typescript-eslint/eslint-plugin'); const tsParser = require('@typescript-eslint/parser'); const headers = require('eslint-plugin-headers'); const importPlugin = require('eslint-plugin-import'); module.exports = [ { ignores: [ '**/dist/', '*.js', '**/node_modules/', '**/coverage/', '!eslint.config.js', '**/docs/.vitepress/cache/', '**/docs/.vitepress/dist/', ], }, { files: ['workspace-server/src/**/*.ts'], ignores: ['**/*.test.ts', '**/*.spec.ts'], languageOptions: { parser: tsParser, parserOptions: { project: true, tsconfigRootDir: __dirname, ecmaVersion: 2020, sourceType: 'module', }, }, plugins: { '@typescript-eslint': tseslint, }, rules: { ...tseslint.configs.recommended.rules, '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/no-unused-vars': [ 'warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_', }, ], 'prefer-const': 'warn', }, }, { files: [ 'workspace-server/src/**/*.test.ts', 'workspace-server/src/**/*.spec.ts', ], languageOptions: { parser: tsParser, parserOptions: { ecmaVersion: 2020, sourceType: 'module', }, }, plugins: { '@typescript-eslint': tseslint, }, rules: { ...tseslint.configs.recommended.rules, '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/no-unused-vars': [ 'warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_', }, ], 'prefer-const': 'warn', }, }, { files: ['./**/*.{tsx,ts,js}'], ignores: ['workspace-server/src/index.ts'], // Has shebang which conflicts with license header plugins: { headers, import: importPlugin, }, rules: { 'headers/header-format': [ 'error', { source: 'string', content: [ '@license', 'Copyright (year) Google LLC', 'SPDX-License-Identifier: Apache-2.0', ].join('\n'), patterns: { year: { pattern: '202[5-6]', defaultValue: '2026', }, }, }, ], 'import/enforce-node-protocol-usage': ['error', 'always'], }, }, { files: ['workspace-server/src/index.ts'], plugins: { import: importPlugin, }, rules: { 'import/enforce-node-protocol-usage': ['error', 'always'], }, }, ]; ================================================ FILE: gemini-extension.json ================================================ { "name": "google-workspace", "version": "0.0.8", "contextFileName": "workspace-server${/}WORKSPACE-Context.md", "mcpServers": { "google-workspace": { "command": "node", "args": ["scripts${/}start.js"], "cwd": "${extensionPath}" } } } ================================================ FILE: jest.config.js ================================================ /** @type {import('jest').Config} */ module.exports = { preset: 'ts-jest', testEnvironment: 'node', projects: [ { displayName: 'workspace-server', testMatch: [ '/workspace-server/src/**/*.test.ts', '/workspace-server/src/**/*.spec.ts', ], transform: { '^.+\\.ts$': [ 'ts-jest', { tsconfig: { strict: false, types: ['jest', 'node'], }, }, ], }, transformIgnorePatterns: ['node_modules/'], moduleNameMapper: { '^@/(.*)$': '/workspace-server/src/$1', '\\.wasm$': '/workspace-server/src/__tests__/mocks/wasm.js', }, roots: ['/workspace-server/src'], setupFilesAfterEnv: ['/workspace-server/src/__tests__/setup.ts'], collectCoverageFrom: [ '/workspace-server/src/**/*.ts', '!/workspace-server/src/**/*.d.ts', '!/workspace-server/src/**/*.test.ts', '!/workspace-server/src/**/*.spec.ts', '!/workspace-server/src/index.ts', ], coverageDirectory: '/coverage', coverageThreshold: { global: { branches: 45, functions: 65, lines: 60, statements: 60, }, }, }, ], coverageReporters: ['text', 'lcov', 'html'], testTimeout: 10000, verbose: true, }; ================================================ FILE: package.json ================================================ { "name": "gemini-workspace-extension", "version": "0.0.8", "description": "Google Workspace Server Extension", "private": true, "bin": { "gemini-workspace-server": "workspace-server/dist/index.js" }, "workspaces": [ "workspace-server" ], "scripts": { "prepare": "npm run build", "build": "npm run build --workspaces --if-present", "test": "npm run test --workspaces --if-present", "test:watch": "npm run test:watch --workspaces --if-present", "test:coverage": "npm run test:coverage --workspaces --if-present", "test:ci": "npm run test:ci --workspaces --if-present", "start": "npm run start --workspaces --if-present", "auth-utils": "npm run build:auth-utils -w workspace-server && node scripts/auth-utils.js", "clean": "node scripts/clean.js", "lint": "eslint .", "lint:fix": "eslint . --fix", "format:check": "prettier --check .", "format:fix": "prettier --write .", "release": "node scripts/release.js", "release:dev": "npm install && npm run build && node scripts/release.js", "set-version": "node scripts/set-version.js", "version": "node scripts/set-version.js && git add workspace-server/package.json", "docs:dev": "vitepress dev docs", "docs:build": "vitepress build docs", "docs:preview": "vitepress preview docs" }, "dependencies": { "@google-apps/chat": "^0.23.0", "@google-cloud/local-auth": "^3.0.1", "@googleapis/docs": "^9.2.1", "@googleapis/drive": "^20.1.0", "@modelcontextprotocol/sdk": "^1.29.0", "google-auth-library": "^10.6.2", "googleapis": "^171.2.0", "keytar": "^7.9.0" }, "devDependencies": { "@jest/globals": "^30.3.0", "@types/jest": "^30.0.0", "@types/node": "^25.6.0", "@typescript-eslint/eslint-plugin": "^8.59.1", "@typescript-eslint/parser": "^8.55.0", "@vercel/ncc": "^0.38.3", "archiver": "^7.0.1", "esbuild": "^0.28.0", "eslint": "^9.39.4", "eslint-plugin-headers": "^1.3.4", "eslint-plugin-import": "^2.32.0", "eslint-plugin-license-header": "^0.9.0", "jest": "^30.3.0", "minimist": "^1.2.8", "prettier": "^3.8.3", "ts-jest": "^29.4.9", "ts-node": "^10.9.2", "typescript": "^6.0.3", "vite": "^8.0.10", "vitepress": "^1.6.4", "vue": "^3.5.33" }, "repository": { "type": "git", "url": "git+https://github.com/gemini-cli-extensions/workspace.git" }, "keywords": [ "google-workspace", "gmail", "google-docs", "google-drive", "google-calendar", "google-chat", "google-people", "google-sheets", "google-slides", "gemini-cli" ], "author": "Allen Hutchison", "license": "Apache-2.0", "overrides": { "vite": "$vite" } } ================================================ FILE: scripts/auth-utils.js ================================================ /** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ const { OAuthCredentialStorage, } = require('../workspace-server/dist/auth-utils.js'); async function clearAuth() { try { await OAuthCredentialStorage.clearCredentials(); console.log('✅ Authentication credentials cleared successfully.'); } catch (error) { console.error('❌ Failed to clear authentication credentials:', error); process.exit(1); } } async function expireToken() { try { const credentials = await OAuthCredentialStorage.loadCredentials(); if (!credentials) { console.log('ℹ️ No credentials found to expire.'); return; } // Set expiry to 1 second ago credentials.expiry_date = Date.now() - 1000; await OAuthCredentialStorage.saveCredentials(credentials); console.log('✅ Access token expired successfully.'); console.log(' Next API call will trigger proactive refresh.'); } catch (error) { console.error('❌ Failed to expire token:', error); process.exit(1); } } async function showStatus() { try { const credentials = await OAuthCredentialStorage.loadCredentials(); if (!credentials) { console.log('ℹ️ No credentials found.'); return; } const now = Date.now(); const expiry = credentials.expiry_date; const hasRefreshToken = !!credentials.refresh_token; const hasAccessToken = !!credentials.access_token; const isExpired = expiry ? expiry < now : false; console.log('📊 Auth Status:'); console.log( ` Access Token: ${hasAccessToken ? '✅ Present' : '❌ Missing'}`, ); console.log( ` Refresh Token: ${hasRefreshToken ? '✅ Present' : '❌ Missing'}`, ); if (expiry) { console.log(` Expiry: ${new Date(expiry).toISOString()}`); console.log(` Status: ${isExpired ? '❌ EXPIRED' : '✅ Valid'}`); if (!isExpired) { const minutesLeft = Math.floor((expiry - now) / 1000 / 60); console.log(` Time left: ~${minutesLeft} minutes`); } } else { console.log(` Expiry: ⚠️ Unknown`); } } catch (error) { console.error('❌ Failed to get auth status:', error); process.exit(1); } } async function login() { try { require('../workspace-server/dist/headless-login.js'); } catch (error) { console.error( '❌ Failed to load headless-login module. Run "npm run build:headless-login" first.', ); console.error(error.message); process.exit(1); } } function showHelp() { console.log(` Auth Management CLI Usage: npm run auth-utils -- Commands: login Authenticate via headless OAuth flow (for SSH/WSL/Cloud Shell) clear Clear all authentication credentials expire Force the access token to expire (for testing refresh) status Show current authentication status help Show this help message Examples: npm run auth-utils -- login npm run auth-utils -- clear npm run auth-utils -- expire npm run auth-utils -- status `); } async function main() { const command = process.argv[2]; switch (command) { case 'login': await login(); break; case 'clear': await clearAuth(); break; case 'expire': await expireToken(); break; case 'status': await showStatus(); break; case 'help': case '--help': case '-h': showHelp(); break; default: if (!command) { console.error('❌ No command specified.'); } else { console.error(`❌ Unknown command: ${command}`); } showHelp(); process.exit(1); } } main(); ================================================ FILE: scripts/clean.js ================================================ /** * @license * Copyright 2026 Google LLC * SPDX-License-Identifier: Apache-2.0 */ const { rmSync, readFileSync } = require('node:fs'); const { join } = require('node:path'); const root = join(__dirname, '..'); const RMRF = { recursive: true, force: true }; function rmrfSyncVerbose(path) { console.log(`Removing ${path}`); rmSync(path, RMRF); } // Clean up all workspaces. const { workspaces } = JSON.parse( readFileSync(join(root, 'package.json'), 'utf-8'), ); for (const workspace of workspaces) { rmrfSyncVerbose(join(root, workspace, 'dist')); } // Root artifacts. rmrfSyncVerbose(join(root, 'node_modules')); rmrfSyncVerbose(join(root, 'release')); rmrfSyncVerbose(join(root, 'logs')); rmrfSyncVerbose(join(root, 'docs', '.vitepress', 'cache')); rmrfSyncVerbose(join(root, 'docs', '.vitepress', 'dist')); ================================================ FILE: scripts/list-deps.js ================================================ /** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ const path = require('node:path'); const { getTransitiveDependencies } = require('./utils/dependencies'); const root = path.join(__dirname, '..'); const targetPackages = process.argv.slice(2); if (targetPackages.length === 0) { console.log('Usage: node scripts/list-deps.js [package2...]'); process.exit(1); } console.log(`Analyzing dependencies for: ${targetPackages.join(', ')}`); const allDeps = getTransitiveDependencies(root, targetPackages); console.log('\nTransitive Dependencies:'); Array.from(allDeps) .sort() .forEach((dep) => { console.log(`- ${dep}`); }); ================================================ FILE: scripts/print-scopes.ts ================================================ /** * @license * Copyright 2026 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /** * Prints the OAuth scopes that should be registered on the GCP consent * screen, one per line. Sourced from FEATURE_GROUPS so the registration * list and the runtime request list cannot drift. * * Used by scripts/setup-gcp.sh. */ import { getAllPossibleScopes } from '../workspace-server/src/features/feature-config'; for (const scope of getAllPossibleScopes()) { console.log(scope); } ================================================ FILE: scripts/release.js ================================================ /** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ const fs = require('node:fs'); const path = require('node:path'); const archiver = require('archiver'); const argv = require('minimist')(process.argv.slice(2)); const deleteFilesByExtension = (dir, ext) => { if (!fs.existsSync(dir)) { return; } const files = fs.readdirSync(dir); for (const file of files) { const filePath = path.join(dir, file); const stat = fs.lstatSync(filePath); if (stat.isDirectory()) { deleteFilesByExtension(filePath, ext); } else if (filePath.endsWith(ext)) { fs.unlinkSync(filePath); } } }; const main = async () => { const platform = argv.platform; if (platform && typeof platform !== 'string') { console.error( 'Error: The --platform argument must be a string (e.g., --platform=linux).', ); process.exit(1); } const baseName = 'google-workspace-extension'; const name = platform ? `${platform}.${baseName}` : baseName; const extension = 'tar.gz'; const rootDir = path.join(__dirname, '..'); const releaseDir = path.join(rootDir, 'release'); fs.rmSync(releaseDir, { recursive: true, force: true }); const archiveName = `${name}.${extension}`; const archiveDir = path.join(releaseDir, name); const workspaceMcpServerDir = path.join(rootDir, 'workspace-server'); // Create the release directory fs.mkdirSync(releaseDir, { recursive: true }); // Create the platform-specific directory fs.mkdirSync(archiveDir, { recursive: true }); // Copy the dist directory fs.cpSync( path.join(workspaceMcpServerDir, 'dist'), path.join(archiveDir, 'dist'), { recursive: true }, ); // Clean up the dist directory const distDir = path.join(archiveDir, 'dist'); deleteFilesByExtension(distDir, '.d.ts'); deleteFilesByExtension(distDir, '.map'); fs.rmSync(path.join(distDir, '__tests__'), { recursive: true, force: true }); fs.rmSync(path.join(distDir, 'auth'), { recursive: true, force: true }); fs.rmSync(path.join(distDir, 'services'), { recursive: true, force: true }); fs.rmSync(path.join(distDir, 'utils'), { recursive: true, force: true }); // Copy native modules and dependencies (keytar, jsdom) const nodeModulesDir = path.join(archiveDir, 'node_modules'); fs.mkdirSync(nodeModulesDir, { recursive: true }); const { getTransitiveDependencies } = require('./utils/dependencies'); const visited = getTransitiveDependencies(rootDir, ['keytar', 'jsdom']); visited.forEach((pkg) => { const source = path.join(rootDir, 'node_modules', pkg); const dest = path.join(nodeModulesDir, pkg); if (fs.existsSync(source)) { fs.cpSync(source, dest, { recursive: true }); } }); const packageJson = require('../package.json'); const version = (process.env.GITHUB_REF_NAME || packageJson.version).replace( /^v/, '', ); // Generate the gemini-extension.json file const geminiExtensionJson = { name: 'google-workspace', version, contextFileName: 'WORKSPACE-Context.md', mcpServers: { 'google-workspace': { command: 'node', args: ['dist/index.js', '--use-dot-names'], cwd: '${extensionPath}', }, }, }; fs.writeFileSync( path.join(archiveDir, 'gemini-extension.json'), JSON.stringify(geminiExtensionJson, null, 2), ); // Copy the WORKSPACE-Context.md file fs.copyFileSync( path.join(workspaceMcpServerDir, 'WORKSPACE-Context.md'), path.join(archiveDir, 'WORKSPACE-Context.md'), ); // Copy the commands directory const commandsDir = path.join(rootDir, 'commands'); if (fs.existsSync(commandsDir)) { fs.cpSync(commandsDir, path.join(archiveDir, 'commands'), { recursive: true, }); } // Copy the skills directory const skillsDir = path.join(rootDir, 'skills'); if (fs.existsSync(skillsDir)) { fs.cpSync(skillsDir, path.join(archiveDir, 'skills'), { recursive: true, }); } // Create the archive const output = fs.createWriteStream(path.join(releaseDir, archiveName)); const archive = archiver('tar', { gzip: true, }); const archivePromise = new Promise((resolve, reject) => { output.on('close', function () { console.log(archive.pointer() + ' total bytes'); console.log( 'archiver has been finalized and the output file descriptor has closed.', ); resolve(); }); archive.on('error', function (err) { reject(err); }); }); archive.pipe(output); archive.directory(archiveDir, false); archive.finalize(); await archivePromise; }; main().catch((err) => { console.error(err); process.exit(1); }); ================================================ FILE: scripts/set-version.js ================================================ /** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ const fs = require('node:fs'); const path = require('node:path'); const rootDir = path.join(__dirname, '..'); const packageJsonPath = path.join(rootDir, 'package.json'); const workspaceServerPackageJsonPath = path.join( rootDir, 'workspace-server', 'package.json', ); const geminiExtensionJsonPath = path.join(rootDir, 'gemini-extension.json'); const workspaceServerIndexPath = path.join( rootDir, 'workspace-server', 'src', 'index.ts', ); const updateJsonFile = (filePath, version) => { try { const content = JSON.parse(fs.readFileSync(filePath, 'utf8')); content.version = version; fs.writeFileSync(filePath, JSON.stringify(content, null, 2) + '\n'); console.log( `Updated ${path.relative(rootDir, filePath)} to version ${version}`, ); } catch (error) { console.error( `Failed to update JSON file at ${path.relative(rootDir, filePath)}:`, error, ); process.exit(1); } }; const main = () => { let version = process.argv[2]; if (version) { // If version is provided as arg, update root package.json first updateJsonFile(packageJsonPath, version); } else { // Otherwise read from root package.json const packageJson = require(packageJsonPath); version = packageJson.version; console.log(`Using version from package.json: ${version}`); } if (!version) { console.error('No version specified and no version found in package.json'); process.exit(1); } updateJsonFile(workspaceServerPackageJsonPath, version); updateJsonFile(geminiExtensionJsonPath, version); }; main(); ================================================ FILE: scripts/setup-gcp.sh ================================================ #!/bin/bash # GCP Setup Script for Google Workspace Extension # This script is idempotent — it can be safely re-run without breaking existing config. set -e # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color # Helper to open a URL in the default browser open_url() { if command -v open &> /dev/null; then open "$1" elif command -v xdg-open &> /dev/null; then xdg-open "$1" else echo -e "Could not open browser automatically." fi } echo -e "${YELLOW}Starting Google Cloud Platform setup...${NC}" # Check if gcloud is installed if ! command -v gcloud &> /dev/null; then echo -e "${RED}Error: gcloud CLI is not installed. Please install it first.${NC}" exit 1 fi # Get current project ID PROJECT_ID=$(gcloud config get-value project 2>/dev/null) if [ -z "$PROJECT_ID" ]; then echo -e "${RED}Error: No Google Cloud project is currently set.${NC}" echo "Please run: gcloud config set project [PROJECT_ID]" exit 1 fi echo -e "Using project: ${GREEN}$PROJECT_ID${NC}" SECRET_ID="workspace-oauth-client-secret" FUNCTION_NAME="workspace-oauth-handler" # 1. Enable Required APIs echo -e "\n${YELLOW}Step 1: Enabling Required APIs...${NC}" APIS=( "drive.googleapis.com" "docs.googleapis.com" "calendar-json.googleapis.com" "chat.googleapis.com" "gmail.googleapis.com" "people.googleapis.com" "slides.googleapis.com" "sheets.googleapis.com" "admin.googleapis.com" "secretmanager.googleapis.com" "cloudfunctions.googleapis.com" "cloudbuild.googleapis.com" "run.googleapis.com" "artifactregistry.googleapis.com" ) for api in "${APIS[@]}"; do echo "Enabling $api..." gcloud services enable "$api" done echo -e "${GREEN}APIs enabled successfully.${NC}" # 2. Configure OAuth Consent Screen echo -e "\n${YELLOW}Step 2: Configure OAuth Consent Screen${NC}" echo -e "The OAuth consent screen must be configured before creating credentials." echo "" CONSENT_URL="https://console.cloud.google.com/apis/credentials/consent?project=$PROJECT_ID" echo -e "Opening the OAuth consent screen configuration page..." open_url "$CONSENT_URL" echo "" echo -e "If the page did not open, go to:" echo -e " ${GREEN}$CONSENT_URL${NC}" echo "" echo -e "Configure the consent screen with these settings:" echo -e " 1. Select ${GREEN}Internal${NC} (Google Workspace) or ${GREEN}External${NC}" echo -e " 2. Fill in the App name and Support email" echo -e " 3. Add the following ${GREEN}scopes${NC} (listed below)" echo -e " 4. Under ${GREEN}Test users${NC}, add the email addresses of anyone" echo -e " who will use this extension (required while in Testing mode)" echo "" # Single source of truth: scopes are computed from FEATURE_GROUPS in # workspace-server/src/features/feature-config.ts. See issue #323. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" SCOPES_OUTPUT=$(cd "$REPO_ROOT" && npx --no-install ts-node --transpile-only --project scripts/tsconfig.json scripts/print-scopes.ts 2>&1) if [ $? -ne 0 ]; then echo -e "${RED}Error: Failed to compute OAuth scopes from feature-config.ts.${NC}" echo -e "${RED}Did you run 'npm install' at the repo root?${NC}" echo "$SCOPES_OUTPUT" exit 1 fi SCOPES=() # Filter to https:// lines so any Node/ts-node warnings written to stderr # (captured via 2>&1 above so we can surface them on failure) don't end up # in the user-visible scope list. while IFS= read -r line; do [[ "$line" == https://* ]] && SCOPES+=("$line") done <<< "$SCOPES_OUTPUT" if [ ${#SCOPES[@]} -eq 0 ]; then echo -e "${RED}Error: print-scopes.ts produced no scope output.${NC}" echo "$SCOPES_OUTPUT" exit 1 fi for scope in "${SCOPES[@]}"; do echo -e " ${GREEN}$scope${NC}" done echo "" echo -e "${YELLOW}Have you finished configuring the OAuth consent screen? (y/n)${NC}" read CONSENT_DONE if [ "$CONSENT_DONE" != "y" ] && [ "$CONSENT_DONE" != "Y" ]; then echo -e "${RED}Please configure the OAuth consent screen before continuing.${NC}" echo -e "Re-run this script when ready." exit 1 fi # 3. Deploy Cloud Function echo -e "\n${YELLOW}Step 3: Deploying Cloud Function...${NC}" echo -e "${YELLOW}Please enter the GCP region for your Cloud Function (e.g., us-central1):${NC}" read REGION if [ -z "$REGION" ]; then REGION="us-central1" echo -e "${YELLOW}No region entered, defaulting to $REGION.${NC}" fi # Check if the function already exists and get its URL FUNCTION_URL="" if gcloud functions describe "$FUNCTION_NAME" --region="$REGION" &> /dev/null; then FUNCTION_URL=$(gcloud functions describe "$FUNCTION_NAME" --region="$REGION" --format='value(serviceConfig.uri)') echo -e "${GREEN}Cloud Function already exists at: $FUNCTION_URL${NC}" echo -e "It will be updated with the final configuration in a later step." else echo "Deploying Cloud Function (initial)..." gcloud functions deploy "$FUNCTION_NAME" \ --gen2 \ --runtime=nodejs22 \ --region="$REGION" \ --source="./cloud_function" \ --entry-point=oauthHandler \ --trigger-http \ --allow-unauthenticated FUNCTION_URL=$(gcloud functions describe "$FUNCTION_NAME" --region="$REGION" --format='value(serviceConfig.uri)') fi if [ -z "$FUNCTION_URL" ]; then echo -e "${RED}Error: Could not retrieve Cloud Function URL. Please check the deployment logs.${NC}" exit 1 fi echo -e "${GREEN}Cloud Function URL: $FUNCTION_URL${NC}" # 4. Collect OAuth credentials echo -e "\n${YELLOW}Step 4: Configuring OAuth credentials...${NC}" echo -e "Create an OAuth 2.0 Client ID in the Google Cloud Console" echo -e "(or locate your existing one):" echo -e " 1. Go to APIs & Services > Credentials > Create Credentials > OAuth client ID" echo -e " 2. Select ${GREEN}Web application${NC}" echo -e " 3. Add the following as an Authorized redirect URI:" echo -e " ${GREEN}$FUNCTION_URL${NC}" echo -e " 4. Copy the Client ID and Client Secret" echo "" CREDENTIALS_URL="https://console.cloud.google.com/apis/credentials?project=$PROJECT_ID" echo -e "Opening the Credentials page..." open_url "$CREDENTIALS_URL" echo "" echo -e "If the page did not open, go to:" echo -e " ${GREEN}$CREDENTIALS_URL${NC}" echo "" echo -e "${YELLOW}Please enter the OAuth 2.0 Client ID:${NC}" read CLIENT_ID if [ -z "$CLIENT_ID" ]; then echo -e "${RED}Error: Client ID cannot be empty.${NC}" exit 1 fi echo -e "${YELLOW}Please enter the OAuth 2.0 Client Secret:${NC}" read -s CLIENT_SECRET echo if [ -z "$CLIENT_SECRET" ]; then echo -e "${RED}Error: Client Secret cannot be empty.${NC}" exit 1 fi # 5. Setup Secret Manager echo -e "\n${YELLOW}Step 5: Storing Client Secret in Secret Manager...${NC}" if gcloud secrets describe "$SECRET_ID" &> /dev/null; then echo "Secret $SECRET_ID already exists. Adding new version..." else echo "Creating secret $SECRET_ID..." gcloud secrets create "$SECRET_ID" --replication-policy=automatic fi echo -n "$CLIENT_SECRET" | gcloud secrets versions add "$SECRET_ID" --data-file=- echo -e "${GREEN}Secret stored successfully.${NC}" # 6. Update Cloud Function with OAuth configuration echo -e "\n${YELLOW}Step 6: Updating Cloud Function with OAuth configuration...${NC}" gcloud functions deploy "$FUNCTION_NAME" \ --gen2 \ --runtime=nodejs22 \ --region="$REGION" \ --source="./cloud_function" \ --entry-point=oauthHandler \ --trigger-http \ --allow-unauthenticated \ --set-env-vars "CLIENT_ID=$CLIENT_ID,SECRET_NAME=projects/$PROJECT_ID/secrets/$SECRET_ID/versions/latest,REDIRECT_URI=$FUNCTION_URL" echo -e "${GREEN}Cloud Function updated with OAuth configuration.${NC}" # 7. Grant Permissions echo -e "\n${YELLOW}Step 7: Granting Secret Manager Access to Cloud Function...${NC}" SERVICE_ACCOUNT=$(gcloud functions describe "$FUNCTION_NAME" --region="$REGION" --format='value(serviceConfig.serviceAccountEmail)') gcloud secrets add-iam-policy-binding "$SECRET_ID" \ --member="serviceAccount:$SERVICE_ACCOUNT" \ --role="roles/secretmanager.secretAccessor" echo -e "${GREEN}Permissions granted successfully.${NC}" echo -e "\n${GREEN}GCP Setup Complete!${NC}" echo -e "---------------------------------------------------" echo -e "${YELLOW}Next Steps:${NC}" echo "Set the following environment variables in your local environment:" echo -e " ${GREEN}export WORKSPACE_CLIENT_ID=\"$CLIENT_ID\"${NC}" echo -e " ${GREEN}export WORKSPACE_CLOUD_FUNCTION_URL=\"$FUNCTION_URL\"${NC}" echo -e "---------------------------------------------------" ================================================ FILE: scripts/start.js ================================================ /** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ const { spawn } = require('node:child_process'); const path = require('node:path'); function runCommand(command, args, options) { return new Promise((resolve, reject) => { // On Windows, npm is a batch file and needs a shell if (process.platform === 'win32' && command === 'npm') { command = `${command}.cmd`; options = { ...options, shell: true }; } const child = spawn(command, args, options); // Pipe stderr to the parent process's stderr if it's available. // This is more efficient than listening for 'data' events. if (child.stderr) { child.stderr.pipe(process.stderr); } child.on('close', (code) => { if (code !== 0) { reject( new Error( `Command failed with code ${code}: ${command} ${args.join(' ')}`, ), ); } else { resolve(); } }); child.on('error', (err) => { reject(err); }); }); } async function main() { try { await runCommand('npm', ['install'], { stdio: ['ignore', 'ignore', 'pipe'], }); const SERVER_PATH = path.join( __dirname, '..', 'workspace-server', 'dist', 'index.js', ); await runCommand('node', [SERVER_PATH, '--debug', '--use-dot-names'], { stdio: 'inherit', }); } catch (error) { console.error(error); process.exit(1); } } main(); ================================================ FILE: scripts/tsconfig.json ================================================ { "extends": "../tsconfig.json", "compilerOptions": { "rootDir": ".." }, "include": ["**/*.ts", "../workspace-server/src/**/*"] } ================================================ FILE: scripts/utils/dependencies.js ================================================ /** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ const fs = require('node:fs'); const path = require('node:path'); /** * Gets the direct dependencies of a package from its package.json. * @param {string} rootDir - The root directory containing node_modules. * @param {string} pkgName - The name of the package. * @returns {string[]} - A list of dependency names. */ function getDependencies(rootDir, pkgName) { const pkgPath = path.join(rootDir, 'node_modules', pkgName, 'package.json'); if (!fs.existsSync(pkgPath)) { return []; } const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); return Object.keys(pkg.dependencies || {}); } /** * Recursively finds all transitive dependencies for a list of packages. * @param {string} rootDir - The root directory containing node_modules. * @param {string[]} startPkgs - The list of initial packages to resolve. * @returns {Set} - A set of all transitive dependencies (including startPkgs). */ function getTransitiveDependencies(rootDir, startPkgs) { const visited = new Set(); const toVisit = [...startPkgs]; while (toVisit.length > 0) { const pkg = toVisit.pop(); if (visited.has(pkg)) continue; visited.add(pkg); const deps = getDependencies(rootDir, pkg); deps.forEach((dep) => { if (!visited.has(dep)) { toVisit.push(dep); } }); } return visited; } module.exports = { getDependencies, getTransitiveDependencies, }; ================================================ FILE: skills/gmail/SKILL.md ================================================ --- name: gmail description: > CRITICAL: You MUST activate this skill BEFORE composing, sending, drafting, or searching emails. Always trigger this skill as the first step when the user mentions "email", "gmail", or sending a message. Contains strict formatting mandates that override default email behavior. --- # Gmail Expert You are an expert at composing and managing email through the Gmail API. Follow these guidelines when helping users with email tasks. ## Rich Text Email Formatting When composing emails (via `gmail.send` or `gmail.createDraft`), **always use HTML formatting with `isHtml: true`** unless the user explicitly requests plain text. Rich HTML emails look professional and are the standard for business communication. ### Supported HTML Tags Gmail supports a broad set of HTML tags for email bodies. Use these freely: | Category | Tags | | :------- | :---------------------------------------------------------------- | | Text | `

`, `
`, ``, `

`, `
`, `
`, `
` | | Headings | `

` through `

` | | Emphasis | ``, ``, ``, ``, ``, ``, `` | | Code | ``, `
`                                                 |
| Lists    | `
    `, `
      `, `
    1. ` | | Tables | ``, ``, ``, ``, `
      `, `` | | Links | `` | | Images | `...` | ### Inline CSS Styling Gmail strips ` ``` ### Common Inline CSS Properties These CSS properties work reliably across Gmail clients: - **Typography**: `font-family`, `font-size`, `font-weight`, `font-style`, `color`, `text-align`, `text-decoration`, `line-height`, `letter-spacing` - **Spacing**: `margin`, `padding` (use on `` for table cell spacing) - **Borders**: `border`, `border-collapse` (on ``) - **Background**: `background-color` - **Layout**: `width`, `max-width`, `height` (on tables and images) ### Things to Avoid - ❌ `