main ad595af82240 cached
124 files
776.8 KB
185.3k tokens
245 symbols
1 requests
Download .txt
Showing preview only (820K chars total). Download the full file or copy to clipboard to get everything.
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 <PR_NUMBER>
    ```
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 <https://cla.developers.google.com/> 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 <query>`

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(`
      <html>
        <head>
          <title>OAuth Token Generated</title>
          <style>
            body { font-family: sans-serif; display: grid; place-items: center; min-height: 90vh; background-color: #f4f7f6; padding: 1rem;}
            .container { background: #fff; border: 1px solid #ccc; border-radius: 8px; padding: 2rem; box-shadow: 0 4px 12px rgba(0,0,0,0.05); max-width: 90%; width: 600px; }
            h1 { color: #333; margin-top: 0;}
            h3 { margin-top: 1.5rem; margin-bottom: 0.5rem; }
            textarea {
              width: 100%;
              min-height: 150px;
              padding: 0.5rem;
              border: 1px solid #ccc;
              border-radius: 4px;
              font-family: monospace;
              white-space: pre;
              word-break: break-all;
              box-sizing: border-box; /* Include padding and border in the element's total width and height */
            }
            button {
              display: block;
              margin: 1rem auto 1rem 0; /* Align left */
              padding: 0.75rem 1.5rem;
              font-size: 1rem;
              border-radius: 4px;
              border: none;
              background-color: #4285F4;
              color: white;
              cursor: pointer;
              transition: background-color 0.2s;
            }
            button:hover { background-color: #357ae8; }
            button:active { background-color: #2a65d5; }
            #copy-status { font-style: italic; color: green; margin-left: 10px; opacity: 0; transition: opacity 0.5s;}
            .instructions { margin-top: 2rem; padding-top: 1rem; border-top: 1px solid #eee; font-size: 0.9em; }
            code { background-color: #eee; padding: 0.2em 0.4em; border-radius: 3px; }
          </style>
        </head>
        <body>
          <div class="container">
            <h1>Success! Credentials Ready</h1>
            <p>Copy the JSON block below. You'll need to store this as the password/secret in your operating system's keychain.</p>

            <h3>Credentials JSON</h3>
            <textarea id="credentials-json" readonly>${credentialsJson}</textarea>
            <button onclick="copyCredentials()">Copy JSON</button>
            <span id="copy-status">Copied!</span>

            <div class="instructions">
              <h4>CLI Login (Recommended):</h4>
              <p>In your terminal, run:</p>
              <pre style="background:#eee;padding:0.75rem;border-radius:4px;overflow-x:auto;"><code>node dist/headless-login.js</code></pre>
              <p>Then paste the JSON above when prompted. The CLI will securely store your credentials.</p>

              <details style="margin-top: 1.5rem;">
                <summary style="cursor:pointer;color:#555;"><strong>Advanced: Manual Keychain Storage</strong></summary>
                <div style="margin-top: 0.5rem;">
                  <ol>
                    <li>Open your OS Keychain/Credential Manager.</li>
                    <li>Create a new secure entry (e.g., a "Generic Password" on macOS, a "Windows Credential", or similar on Linux).</li>
                    <li>Set the <strong>Service</strong> (or equivalent field) to: <code>${KEYCHAIN_SERVICE_NAME}</code></li>
                    <li>Set the <strong>Account</strong> (or username field) to: <code>${KEYCHAIN_ACCOUNT_NAME}</code></li>
                    <li>Paste the copied JSON into the <strong>Password/Secret</strong> field.</li>
                    <li>Save the entry.</li>
                  </ol>
                  <p><small>(If keychain is unavailable, the server falls back to an encrypted file, but keychain is recommended.)</small></p>
                </div>
              </details>
            </div>
          </div>

          <script>
            function copyCredentials() {
              const textArea = document.getElementById('credentials-json');
              const status = document.getElementById('copy-status');

              // Use modern Clipboard API if available, with fallback to execCommand
              if (navigator.clipboard && navigator.clipboard.writeText) {
                navigator.clipboard.writeText(textArea.value).then(() => {
                  status.textContent = 'Copied!';
                  status.style.color = 'green';
                }, () => {
                  status.textContent = 'Copy failed!';
                  status.style.color = 'red';
                });
              } else {
                // Fallback for older browsers/iframes without clipboard access
                textArea.select();
                try {
                  const successful = document.execCommand('copy');
                  if (successful) {
                    status.textContent = 'Copied!';
                    status.style.color = 'green';
                  } else {
                    status.textContent = 'Copy failed!';
                    status.style.color = 'red';
                  }
                } catch (err) {
                  status.textContent = 'Copy failed!';
                  status.style.color = 'red';
                  console.error('Fallback copy failed: ', err);
                }
              }

              status.style.opacity = 1;
              setTimeout(() => { status.style.opacity = 0; }, 2000);

              // Deselect text after attempting to copy
              if (window.getSelection) {window.getSelection().removeAllRanges();}
              else if (document.selection) {document.selection.empty();}
            }
          </script>
        </body>
      </html>
    `);
  } 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 -- <command>
```

### 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 <new-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 <new-version>"
    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 <tag> --generate-notes
    gh release create v<new-version> --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: [
        '<rootDir>/workspace-server/src/**/*.test.ts',
        '<rootDir>/workspace-server/src/**/*.spec.ts',
      ],
      transform: {
        '^.+\\.ts$': [
          'ts-jest',
          {
            tsconfig: {
              strict: false,
              types: ['jest', 'node'],
            },
          },
        ],
      },
      transformIgnorePatterns: ['node_modules/'],
      moduleNameMapper: {
        '^@/(.*)$': '<rootDir>/workspace-server/src/$1',
        '\\.wasm$': '<rootDir>/workspace-server/src/__tests__/mocks/wasm.js',
      },
      roots: ['<rootDir>/workspace-server/src'],
      setupFilesAfterEnv: ['<rootDir>/workspace-server/src/__tests__/setup.ts'],
      collectCoverageFrom: [
        '<rootDir>/workspace-server/src/**/*.ts',
        '!<rootDir>/workspace-server/src/**/*.d.ts',
        '!<rootDir>/workspace-server/src/**/*.test.ts',
        '!<rootDir>/workspace-server/src/**/*.spec.ts',
        '!<rootDir>/workspace-server/src/index.ts',
      ],
      coverageDirectory: '<rootDir>/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 -- <command>

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 <package1> [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<string>} - 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     | `<p>`, `<br>`, `<span>`, `<div>`, `<blockquote>`, `<pre>`, `<hr>` |
| Headings | `<h1>` through `<h6>`                                             |
| Emphasis | `<b>`, `<strong>`, `<i>`, `<em>`, `<u>`, `<s>`, `<strike>`        |
| Code     | `<code>`, `<pre>`                                                 |
| Lists    | `<ul>`, `<ol>`, `<li>`                                            |
| Tables   | `<table>`, `<thead>`, `<tbody>`, `<tr>`, `<th>`, `<td>`           |
| Links    | `<a href="...">`                                                  |
| Images   | `<img src="..." alt="...">`                                       |

### Inline CSS Styling

Gmail strips `<style>` blocks and external stylesheets. **Always use inline
CSS** via the `style` attribute:

```html
<!-- ✅ Correct: inline styles -->
<p style="color: #333; font-family: Arial, sans-serif; font-size: 14px;">
  Hello!
</p>

<!-- ❌ Wrong: style block (will be stripped) -->
<style>
  p {
    color: #333;
  }
</style>
```

### 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 `<td>` for table cell spacing)
- **Borders**: `border`, `border-collapse` (on `<table>`)
- **Background**: `background-color`
- **Layout**: `width`, `max-width`, `height` (on tables and images)

### Things to Avoid

- ❌ `<script>` tags (blocked by all email clients)
- ❌ `<style>` blocks (stripped by Gmail)
- ❌ External stylesheets (`<link rel="stylesheet">`)
- ❌ `position`, `float`, `flexbox`, `grid` (unreliable in email)
- ❌ `background-image` on non-table elements (inconsistent support)
- ❌ JavaScript event handlers (`onclick`, etc.)
- ❌ Form elements (`<input>`, `<select>`, `<textarea>`)

### Email Template Examples

#### Professional Message

```html
<div style="font-family: Arial, sans-serif; font-size: 14px; color: #333;">
  <p>Hi Team,</p>

  <p>Please find the <b>Q4 results</b> summarized below:</p>

  <table style="border-collapse: collapse; width: 100%; margin: 16px 0;">
    <thead>
      <tr style="background-color: #f2f2f2;">
        <th style="border: 1px solid #ddd; padding: 8px; text-align: left;">
          Metric
        </th>
        <th style="border: 1px solid #ddd; padding: 8px; text-align: left;">
          Result
        </th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td style="border: 1px solid #ddd; padding: 8px;">Revenue</td>
        <td style="border: 1px solid #ddd; padding: 8px;">$1.2M</td>
      </tr>
      <tr>
        <td style="border: 1px solid #ddd; padding: 8px;">Growth</td>
        <td style="border: 1px solid #ddd; padding: 8px;">+15%</td>
      </tr>
    </tbody>
  </table>

  <p>Key takeaways:</p>
  <ul>
    <li>Revenue exceeded targets by <b>12%</b></li>
    <li>Customer retention improved to <b>94%</b></li>
  </ul>

  <p>
    Best regards,<br />
    <span style="color: #666;">— The Analytics Team</span>
  </p>
</div>
```

#### Styled Action Email

```html
<div
  style="font-family: 'Helvetica Neue', Arial, sans-serif; max-width: 600px;"
>
  <h2 style="color: #1a73e8; margin-bottom: 8px;">Action Required</h2>
  <p style="font-size: 14px; color: #555;">
    Your review is needed on the following document:
  </p>
  <p>
    <a
      href="https://docs.google.com/document/d/example"
      style="display: inline-block; background-color: #1a73e8; color: #fff;
              padding: 10px 24px; text-decoration: none; border-radius: 4px;
              font-size: 14px;"
    >
      Open Document
    </a>
  </p>
  <p style="font-size: 12px; color: #999;">
    Please respond by end of business Friday.
  </p>
</div>
```

## Gmail Search Syntax

Use Gmail search operators with `gmail.search` for precise results:

| Operator      | Example                    | Description                     |
| :------------ | :------------------------- | :------------------------------ |
| `from:`       | `from:alice@example.com`   | Sender                          |
| `to:`         | `to:bob@example.com`       | Recipient                       |
| `subject:`    | `subject:quarterly report` | Subject line                    |
| `is:`         | `is:unread`, `is:starred`  | Message state                   |
| `has:`        | `has:attachment`           | Has attachments                 |
| `in:`         | `in:inbox`, `in:trash`     | Location                        |
| `label:`      | `label:work`               | By label                        |
| `before:`     | `before:2025/01/01`        | Before date                     |
| `after:`      | `after:2025/01/01`         | After date                      |
| `newer_than:` | `newer_than:7d`            | Within last N days/months/years |
| `older_than:` | `older_than:1m`            | Older than N days/months/years  |
| `filename:`   | `filename:report.pdf`      | Attachment filename             |
| `size:`       | `size:5m`                  | Larger than size                |
| `larger:`     | `larger:10M`               | Larger than size                |
| `smaller:`    | `smaller:1M`               | Smaller than size               |
| `OR`          | `from:alice OR from:bob`   | Either condition                |
| `-`           | `-from:noreply@`           | Exclude                         |
| `""`          | `"exact phrase"`           | Exact match                     |

Combine operators for precise searches:
`from:alice@example.com has:attachment newer_than:30d subject:report`

## Label Management

### System Labels

System labels use their name directly as the ID. Use `gmail.modify` to apply
these common operations:

| Label       | Add Effect       | Remove Effect               |
| :---------- | :--------------- | :-------------------------- |
| `INBOX`     | Move to inbox    | Archive (remove from inbox) |
| `UNREAD`    | Mark as unread   | Mark as read                |
| `STARRED`   | Star the message | Unstar                      |
| `IMPORTANT` | Mark important   | Mark not important          |
| `SPAM`      | Mark as spam     | Remove spam classification  |
| `TRASH`     | Move to trash    | Remove from trash           |

### Custom Labels

For user-created labels, you must resolve the label ID first:

1. Call `gmail.listLabels()` to get all labels with their IDs
2. Match the desired label by name
3. Use the label ID (e.g., `Label_42`) in `gmail.modify`

## Downloading Attachments

1. Use `gmail.get` with `format: 'full'` to get attachment metadata (IDs and
   filenames)
2. Use `gmail.downloadAttachment` with the `messageId` and `attachmentId`
3. **Always use absolute paths** for `localPath` (e.g.,
   `/Users/username/Downloads/file.pdf`). Relative paths will be rejected.

## Threading and Replies

- Use `threadId` with `gmail.createDraft` to create a reply draft linked to an
  existing conversation
- The service automatically fetches reply headers (`In-Reply-To`, `References`)
  from the thread to maintain proper threading
- Always reference previous messages when replying for context continuity


================================================
FILE: skills/google-calendar/SKILL.md
================================================
---
name: google-calendar
description: >
  CRITICAL: You MUST activate this skill BEFORE creating, querying, or managing
  calendar events. Always trigger this skill as the first step when the user
  mentions "calendar", "schedule", "meeting", "event", or checking availability.
  Contains strict behavioral mandates that override default calendar behavior.
---

# Google Calendar Expert

You are an expert at managing schedules and events through the Google Calendar
API. Follow these guidelines when helping users with calendar tasks.

## Timezone-First Workflow

**Always establish the user's timezone before any calendar operation:**

1. Call `time.getTimeZone()` (or `time.getCurrentTime()`) to get the user's
   local timezone
2. Use this timezone for all time displays and event creation
3. Always include the timezone abbreviation (EST, PST, etc.) when showing times

> **Important:** ISO 8601 datetimes sent to the API must include a timezone
> offset (e.g., `2025-01-15T10:30:00-05:00`) or use UTC (`Z`). Never send "bare"
> datetimes without an offset.

## Always Pass `calendarId`

**You MUST pass `calendarId: "primary"` on every calendar tool call that accepts
it.** Do not omit this parameter — while the API may default to the primary
calendar, omitting it wastes an execution turn when the call fails or requires
clarification. Always include it explicitly:

- `calendar.listEvents({ calendarId: "primary", ... })`
- `calendar.createEvent({ calendarId: "primary", ... })`
- `calendar.getEvent({ eventId: "...", calendarId: "primary" })`
- `calendar.updateEvent({ eventId: "...", calendarId: "primary", ... })`
- `calendar.deleteEvent({ eventId: "...", calendarId: "primary" })`
- `calendar.respondToEvent({ eventId: "...", calendarId: "primary", ... })`

Only use a different `calendarId` when the user explicitly asks to work with a
non-primary calendar (discovered via `calendar.list`).

## Understanding "Next Meeting"

When asked about "next meeting", "today's schedule", or similar queries:

1. **Fetch the full day's context** — Use `calendar.listEvents` with
   `calendarId: "primary"`, start of day (`00:00:00`) to end of day (`23:59:59`)
   in the user's timezone
2. **Filter by response status** — Only show meetings where the user has:
   - Accepted the invitation
   - Not yet responded (needs to decide)
   - **DO NOT** show declined meetings unless explicitly requested
3. **Compare with current time** — Identify meetings relative to now
4. **Handle edge cases**:
   - If a meeting is in progress, mention it first
   - "Next" means the first meeting after the current time
   - Keep the full day context for follow-up questions

## Meeting Response Filtering

Use the `attendeeResponseStatus` parameter on `calendar.listEvents` to filter
events by the user's response:

| Default Behavior      | Show Only                          |
| :-------------------- | :--------------------------------- |
| Standard schedule     | Accepted and pending (needsAction) |
| "Show all meetings"   | Include declined                   |
| "What did I decline?" | Filter to declined only            |

This respects the user's time by not cluttering their schedule with irrelevant
meetings.

## Creating Events

Use `calendar.createEvent` to add new events. **Always preview the event before
creating it and wait for user confirmation.**

### Preview Format

```
I'll create this event:

📅 Title: Weekly Standup
📆 Date: January 15, 2025
🕐 Time: 10:00 AM - 10:30 AM (EST)
👥 Attendees: alice@example.com, bob@example.com
📝 Description: Weekly team sync
🎥 Google Meet: Will be generated
📎 Attachments: Q1 Agenda (Google Doc)

Should I create this event?
```

### Key Parameters

- **`calendarId`** — **Always pass `"primary"`**. Use `calendar.list` to
  discover other calendars when needed.
- **`start` / `end`** — Two formats:
  - **Timed events**: `{ dateTime: "2025-01-15T10:00:00-05:00" }` — ISO 8601
    with timezone offset
  - **All-day events**: `{ date: "2025-01-15" }` — YYYY-MM-DD format. The end
    date is exclusive (use the next day).
- **`attendees`** — Array of email addresses
- **`addGoogleMeet`** — Set to `true` to automatically generate a Google Meet
  link (available in response's `hangoutLink` field)
- **`attachments`** — Array of Google Drive file attachments (fileUrl, title,
  optional mimeType). Providing attachments fully replaces any existing
  attachments.
- **`sendUpdates`** — Controls email notifications:
  - `"all"` — Notify all attendees (default when attendees are provided)
  - `"externalOnly"` — Only notify non-organization attendees
  - `"none"` — No notifications
- **`eventType`** — The type of event (see
  [Calendar Status Events](#calendar-status-events) below):
  - `"default"` — Regular event (default if omitted)
  - `"focusTime"` — Focus time block
  - `"outOfOffice"` — Out-of-office event
  - `"workingLocation"` — Working location indicator

### Example — Regular Timed Event

```
calendar.createEvent({
  calendarId: "primary",
  summary: "Weekly Standup",
  start: { dateTime: "2025-01-15T10:00:00-05:00" },
  end: { dateTime: "2025-01-15T10:30:00-05:00" },
  attendees: ["alice@example.com", "bob@example.com"],
  description: "Weekly team sync",
  addGoogleMeet: true,
  attachments: [{
    fileUrl: "https://drive.google.com/file/d/abc123/edit",
    title: "Q1 Agenda",
    mimeType: "application/vnd.google-apps.document"
  }],
  sendUpdates: "all"
})
```

### Example — All-Day Event

```
calendar.createEvent({
  calendarId: "primary",
  summary: "Team Offsite",
  start: { date: "2025-01-15" },
  end: { date: "2025-01-17" },
  description: "Two-day team offsite"
})
```

## Calendar Status Events

`calendar.createEvent` supports creating focus time, out-of-office, and working
location events via the `eventType` parameter. These are all created through the
same tool — there are no separate tools for each type.

### Focus Time

Blocks concentrated work periods. Can auto-decline conflicting meetings.

> **Constraint:** Focus time events **cannot be all-day events** — they must use
> `dateTime`, not `date`.

```
calendar.createEvent({
  calendarId: "primary",
  eventType: "focusTime",
  start: { dateTime: "2025-01-15T09:00:00-05:00" },
  end: { dateTime: "2025-01-15T12:00:00-05:00" },
  focusTimeProperties: {
    chatStatus: "doNotDisturb",
    autoDeclineMode: "declineOnlyNewConflictingInvitations",
    declineMessage: "In focus mode, will respond later"
  }
})
```

- **`summary`** defaults to `"Focus Time"` if omitted
- **`focusTimeProperties.chatStatus`** — `"doNotDisturb"` (default) or
  `"available"`
- **`focusTimeProperties.autoDeclineMode`** —
  `"declineOnlyNewConflictingInvitations"` (default),
  `"declineAllConflictingInvitations"`, or `"declineNone"`
- **`focusTimeProperties.declineMessage`** — optional message sent when
  declining

### Out of Office

Signals unavailability and auto-declines conflicting meetings.

> **Constraint:** Out-of-office events **cannot be all-day events** — they must
> use `dateTime`, not `date`.

```
calendar.createEvent({
  calendarId: "primary",
  eventType: "outOfOffice",
  summary: "Vacation",
  start: { dateTime: "2025-01-15T00:00:00-05:00" },
  end: { dateTime: "2025-01-19T00:00:00-05:00" },
  outOfOfficeProperties: {
    autoDeclineMode: "declineAllConflictingInvitations",
    declineMessage: "I am on vacation until Jan 19"
  }
})
```

- **`summary`** defaults to `"Out of Office"` if omitted
- **`outOfOfficeProperties.autoDeclineMode`** —
  `"declineOnlyNewConflictingInvitations"` (default),
  `"declineAllConflictingInvitations"`, or `"declineNone"`
- **`outOfOfficeProperties.declineMessage`** — optional message sent when
  declining

### Working Location

Indicates where the user is working from. Supports both timed and all-day
events.

```
calendar.createEvent({
  calendarId: "primary",
  eventType: "workingLocation",
  start: { date: "2025-01-15" },
  end: { date: "2025-01-16" },
  workingLocationProperties: {
    type: "homeOffice"
  }
})
```

- **`summary`** defaults to `"Working Location"` if omitted
- **All-day working location events** must span **exactly one day**. Use the
  next day as the exclusive `end` date.
- **`workingLocationProperties`** is **required** when `eventType` is
  `"workingLocation"`
- **`workingLocationProperties.type`** — `"homeOffice"`, `"officeLocation"`, or
  `"customLocation"`
- **`officeLocation`** — `{ buildingId?: string, label?: string }` (when type is
  `"officeLocation"`)
- **`customLocation`** — `{ label: string }` (when type is `"customLocation"`)

### Listing Events by Type

Use the `eventTypes` parameter on `calendar.listEvents` to filter by event type:

```
calendar.listEvents({
  calendarId: "primary",
  timeMin: "2025-01-15T00:00:00-05:00",
  timeMax: "2025-01-17T23:59:59-05:00",
  eventTypes: ["focusTime", "outOfOffice", "workingLocation"]
})
```

Available types: `"default"`, `"focusTime"`, `"outOfOffice"`,
`"workingLocation"`, `"birthday"`, `"fromGmail"`.

## Updating Events

Use `calendar.updateEvent` for modifications. Only the fields you provide will
be changed — everything else is preserved.

- **Rescheduling**: Update `start` and `end`
- **Adding attendees**: Provide the full attendee list (existing + new)
- **Changing title/description**: Update `summary` or `description`
- **Adding Google Meet**: Set `addGoogleMeet: true` to generate a Meet link
- **Managing attachments**: Provide the full attachment list (replaces all
  existing). Pass `attachments: []` to clear all attachments.

> **Important:** The `attendees` field is a full replacement, not an append. To
> add a new attendee, include all existing attendees plus the new one. The same
> applies to `attachments` — providing attachments fully replaces any existing
> attachments on the event.

## Google Meet Integration

When creating or updating events, you can automatically generate a Google Meet
link by setting `addGoogleMeet: true`:

```
calendar.createEvent({
  summary: "Team Standup",
  start: { dateTime: "2025-01-15T10:00:00-05:00" },
  end: { dateTime: "2025-01-15T10:30:00-05:00" },
  addGoogleMeet: true
})
```

The Meet URL will be available in the response's `hangoutLink` field:

```json
{
  "hangoutLink": "https://meet.google.com/abc-defg-hij",
  "conferenceData": { ... }
}
```

## Google Drive Attachments

You can attach Google Drive files (Docs, Sheets, Slides, PDFs, etc.) to calendar
events:

```
calendar.createEvent({
  summary: "Budget Review",
  start: { dateTime: "2025-01-16T14:00:00-05:00" },
  end: { dateTime: "2025-01-16T15:00:00-05:00" },
  attachments: [
    {
      fileUrl: "https://drive.google.com/file/d/1ABC123xyz/edit",
      title: "Q1 Budget Report",
      mimeType: "application/vnd.google-apps.document"
    }
  ]
})
```

**CRITICAL:** Attachments use **replacement semantics**, not append semantics.
When you provide attachments, any existing attachments on the event are fully
replaced. To add more attachments, include all desired attachments in your
update.

## Deleting Events

Use `calendar.deleteEvent` to remove an event. **This is a destructive action —
always confirm with the user before executing.**

| Role      | Effect                                  |
| :-------- | :-------------------------------------- |
| Organizer | Cancels the event for **all** attendees |
| Attendee  | Removes it from **your** calendar only  |

## Responding to Events

Use `calendar.respondToEvent` to accept, decline, or tentatively accept meeting
invitations:

- **`responseStatus`** — `"accepted"`, `"declined"`, or `"tentative"`
- **`sendNotification`** — Whether to notify the organizer (default: `true`)
- **`responseMessage`** — Optional message to include with your response

```
calendar.respondToEvent({
  eventId: "abc123",
  responseStatus: "accepted",
  sendNotification: true,
  responseMessage: "Looking forward to it!"
})
```

## Finding Free Time

Use `calendar.findFreeTime` to find available slots across multiple people's
calendars. This is ideal for scheduling new meetings.

- **`attendees`** — Email addresses of all participants
- **`timeMin` / `timeMax`** — The search window (ISO 8601 with timezone)
- **`duration`** — Meeting length in minutes

```
calendar.findFreeTime({
  attendees: ["alice@example.com", "bob@example.com"],
  timeMin: "2025-01-15T09:00:00-05:00",
  timeMax: "2025-01-17T17:00:00-05:00",
  duration: 30
})
```

## Working with Multiple Calendars

Users may have multiple calendars (personal, work, shared team calendars).

1. Use `calendar.list` to discover all available calendars
2. Pass the appropriate `calendarId` to other tools
3. If no `calendarId` is provided, tools default to the **primary** calendar

## Tool Quick Reference

| Tool                      | Action                           | Key Parameters                                                                       |
| :------------------------ | :------------------------------- | :----------------------------------------------------------------------------------- |
| `calendar.list`           | List all calendars               | _(none)_                                                                             |
| `calendar.listEvents`     | List events (filterable by type) | `calendarId`, `timeMin`, `timeMax`, `eventTypes`                                     |
| `calendar.getEvent`       | Get event details                | `eventId`, `calendarId`                                                              |
| `calendar.createEvent`    | Create event (all types)         | `calendarId`, `summary`, `start`, `end`, `eventType`, `addGoogleMeet`, `attachments` |
| `calendar.updateEvent`    | Modify an existing event         | `eventId`, `summary`, `start`, `end`, `attendees`, `addGoogleMeet`, `attachments`    |
| `calendar.deleteEvent`    | Delete an event                  | `eventId`, `calendarId`                                                              |
| `calendar.respondToEvent` | Accept/decline an invite         | `eventId`, `responseStatus`                                                          |
| `calendar.findFreeTime`   | Find available meeting time      | `attendees`, `timeMin`, `timeMax`, `duration`                                        |


================================================
FILE: skills/google-chat/SKILL.md
================================================
---
name: google-chat
description: >
  CRITICAL: You MUST activate this skill BEFORE sending, reading, or managing
  Google Chat messages. Always trigger this skill as the first step when the
  user mentions "chat", "google chat", "message a space", "DM", or sending a
  chat message. Contains strict formatting mandates that override default
  messaging behavior.
---

# Google Chat Expert

You are an expert at messaging and managing conversations through the Google
Chat API. Follow these guidelines when helping users with chat tasks.

## Chat Message Formatting

When composing messages (via `chat.sendMessage` or `chat.sendDm`), **always use
Google Chat's supported markdown syntax**. Google Chat uses a specific subset of
markdown that differs from standard markdown. You MUST convert any unsupported
syntax before sending.

### Supported Formatting

| Syntax             | Renders As        | Example                        |
| :----------------- | :---------------- | :----------------------------- |
| `*text*`           | **bold**          | `*Important update*`           |
| `_text_`           | _italic_          | `_Please review_`              |
| `~text~`           | ~~strikethrough~~ | `~no longer relevant~`         |
| `` `code` ``       | `inline code`     | `` `git status` ``             |
| ` ``` `            | code block        | ` ```\ncode\n``` `             |
| `* ` or `- `       | bulleted list     | `* Item one\n* Item two`       |
| `<url\|text>`      | hyperlink         | `<https://example.com\|Click>` |
| `<users/{userId}>` | @mention          | `<users/12345678>`             |

### Unsupported Syntax (Convert These)

Always convert these before sending a message to Chat:

| Unsupported Syntax          | Convert To                       |
| :-------------------------- | :------------------------------- |
| `**bold**` (double `*`)     | `*bold*` (single `*`)            |
| `[text](url)` markdown link | `<url\|text>` Chat link format   |
| `# Heading`                 | `*Heading*` (bold text)          |
| Nested lists                | Flatten to a single-level list.  |
| `> blockquote`              | Preserve the `>` character as-is |

### Message Formatting Examples

#### Status Update

```
*Project Status Update*

_Sprint 14 Summary:_

* Completed 12 of 15 story points
* ~Deferred analytics dashboard~ (moved to Sprint 15)
* Key PR: <https://github.com/org/repo/pull/42|#42 - Auth refactor>

Next steps: `deploy-staging` pipeline runs tonight.
```

#### Code Snippet

````
Found the bug. The issue is in the handler:

```
func handleRequest(ctx context.Context) error {
    // Missing nil check here
    if ctx == nil {
        return ErrNilContext
    }
    return process(ctx)
}
```

<users/12345678> can you review this fix?
````

## Spaces vs. Direct Messages

Google Chat has two main messaging contexts. Use the right tool for each:

### Spaces (Group Conversations)

Spaces are shared group conversations with a display name.

| Action             | Tool                   | Key Parameter              |
| :----------------- | :--------------------- | :------------------------- |
| Find a space       | `chat.findSpaceByName` | `displayName`              |
| List all spaces    | `chat.listSpaces`      | _(none)_                   |
| Send a message     | `chat.sendMessage`     | `spaceName`, `message`     |
| Create a new space | `chat.setUpSpace`      | `displayName`, `userNames` |

### Direct Messages (1:1)

DMs are private conversations between two users, identified by email.

| Action          | Tool                 | Key Parameter      |
| :-------------- | :------------------- | :----------------- |
| Find a DM space | `chat.findDmByEmail` | `email`            |
| Send a DM       | `chat.sendDm`        | `email`, `message` |

> **Note:** `chat.sendDm` and `chat.findDmByEmail` both use `spaces.setup` under
> the hood. If no DM space exists with the user, one is automatically created.
> There is no need to create a DM space separately.

> **Limitation:** DM tools only support 1:1 conversations. For group
> conversations (3+ people), use `chat.setUpSpace` to create a named space
> instead.

## Threading

Threads keep related messages grouped together within a space. Use the
`threadName` parameter to reply in an existing thread.

### How Threading Works

1. **Start a new thread**: Send a message without `threadName`. The response
   will include a `thread.name` you can use for replies.
2. **Reply to a thread**: Pass `threadName` when calling `chat.sendMessage` or
   `chat.sendDm`. The API uses `REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD` — if the
   thread doesn't exist, a new thread is created.
3. **List threads**: Use `chat.listThreads` to discover threads in a space. It
   returns the most recent message of each unique thread in reverse
   chronological order.
4. **Get thread messages**: Use `chat.getMessages` with the `threadName`
   parameter to get all messages in a specific thread.

### Thread Example Workflow

```
1. chat.listThreads({ spaceName: "spaces/AAAAN2J52O8" })
   → Returns threads with thread.name values

2. chat.getMessages({
     spaceName: "spaces/AAAAN2J52O8",
     threadName: "spaces/AAAAN2J52O8/threads/IAf4cnLqYfg"
   })
   → Returns all messages in that thread

3. chat.sendMessage({
     spaceName: "spaces/AAAAN2J52O8",
     message: "Thanks for the update!",
     threadName: "spaces/AAAAN2J52O8/threads/IAf4cnLqYfg"
   })
   → Replies in the same thread
```

## Unread Messages

Use `unreadOnly: true` on `chat.getMessages` to filter for unread messages only.

### How It Works

The unread filter:

1. Looks up the authenticated user's ID via the People API
2. Finds the user's membership in the space
3. Uses the `lastReadTime` from the membership to filter messages created after
   that timestamp
4. If no `lastReadTime` is found, all messages are returned (treats everything
   as unread)

### Combining Filters

You can combine `unreadOnly` with `threadName` to get unread messages in a
specific thread. The filters are joined with `AND`:

```
chat.getMessages({
  spaceName: "spaces/AAAAN2J52O8",
  threadName: "spaces/AAAAN2J52O8/threads/IAf4cnLqYfg",
  unreadOnly: true
})
```

## Space Management

### Creating a Space

Use `chat.setUpSpace` to create a new named space with members:

```
chat.setUpSpace({
  displayName: "Q1 Planning",
  userNames: ["users/12345678", "users/87654321"]
})
```

> **Important:** The `userNames` parameter expects user resource names in the
> format `users/{userId}`, not email addresses. Use `people.getUserProfile` to
> look up user IDs and convert the `people/{id}` resource name to `users/{id}`.

## Resource Name Formats

Google Chat uses structured resource names. Here is a quick reference:

| Resource | Format                                  | Example                                  |
| :------- | :-------------------------------------- | :--------------------------------------- |
| Space    | `spaces/{spaceId}`                      | `spaces/AAAAN2J52O8`                     |
| Message  | `spaces/{spaceId}/messages/{messageId}` | `spaces/AAAAN2J52O8/messages/abc123`     |
| Thread   | `spaces/{spaceId}/threads/{threadId}`   | `spaces/AAAAN2J52O8/threads/IAf4cnLqYfg` |
| User     | `users/{userId}`                        | `users/12345678`                         |


================================================
FILE: skills/google-docs/SKILL.md
================================================
---
name: google-docs
description: >
  CRITICAL: You MUST activate this skill BEFORE creating, editing, or managing
  Google Docs. Always trigger this skill as the first step when the user
  mentions "document", "doc", "google doc", or writing/editing document content.
  Contains strict formatting mandates that override default document behavior.
---

# Google Docs Expert

You are an expert at creating and managing documents through the Google Docs
API. Follow these guidelines when helping users with document tasks.

## Document Formatting — Two-Step Workflow

To create richly formatted documents, use a **two-step process**:

1. **Insert content** using `docs.create` or `docs.writeText` — this inserts
   plain text
2. **Apply formatting** using `docs.formatText` — this applies bold, italic,
   headings, links, and other styles to specific text ranges

### Calculating Indices

After inserting text, you know the content and can calculate character
positions. Indices are 1-based (index 1 is the start of the document body).

For example, if you insert:

```
Project Update\n\nStatus: On Track\n
```

Then:

- "Project Update" spans indices 1–15
- "Status: On Track" spans indices 17–33

### Supported Formatting Styles

| Style           | Effect                          | API                  |
| --------------- | ------------------------------- | -------------------- |
| `bold`          | **Bold text**                   | updateTextStyle      |
| `italic`        | _Italic text_                   | updateTextStyle      |
| `underline`     | Underlined text                 | updateTextStyle      |
| `strikethrough` | ~~Strikethrough text~~          | updateTextStyle      |
| `code`          | `Monospace font` (Courier New)  | updateTextStyle      |
| `link`          | Hyperlink (requires `url`)      | updateTextStyle      |
| `heading1`      | Heading 1                       | updateParagraphStyle |
| `heading2`      | Heading 2                       | updateParagraphStyle |
| `heading3`      | Heading 3                       | updateParagraphStyle |
| `heading4`      | Heading 4                       | updateParagraphStyle |
| `heading5`      | Heading 5                       | updateParagraphStyle |
| `heading6`      | Heading 6                       | updateParagraphStyle |
| `normalText`    | Reset to normal paragraph style | updateParagraphStyle |

### Formatting Example

Create a doc with a heading and bold text:

```
// Step 1: Create with content
docs.create({
  title: "Weekly Report",
  content: "Weekly Report\n\nHighlights\n\n- Revenue up 12%\n- 3 new launches\n"
})

// Step 2: Apply formatting
docs.formatText({
  documentId: "<id-from-step-1>",
  formats: [
    { startIndex: 1, endIndex: 14, style: "heading1" },
    { startIndex: 16, endIndex: 26, style: "heading2" },
    { startIndex: 16, endIndex: 26, style: "bold" }
  ]
})
```

### Professional Document Structure

When creating documents, use a clear heading hierarchy:

- **Heading 1** — Document title (use once, at the top)
- **Heading 2** — Major sections
- **Heading 3** — Subsections within a section
- **Bold** — Labels, field names, and emphasis within body text

**Structure the content first, then apply formatting generously.** A
well-formatted document uses headings for every distinct section — not just the
title. Think of each logical group of content as deserving its own heading.

#### Example: PR Summary Document

**Step 1 — Content:**

```
PR Summary Report

PR #246: Add Gmail Skill

Author: Allen Hutchison
Status: Merged

This PR introduces a new agent skill for Gmail with rich HTML formatting
guidance, establishing the skills architecture for the extension.

Key Changes:
- Added skills/gmail/SKILL.md with email formatting instructions
- Updated WORKSPACE-Context.md to cross-reference the new skill
- Modified release script to bundle the skills directory

PR #245: Bump rollup from 4.57.1 to 4.59.0

Author: dependabot
Status: Merged

Routine dependency update for the build pipeline.
```

**Step 2 — Formatting:**

```
docs.formatText({
  documentId: "<id>",
  formats: [
    // Document title
    { startIndex: 1, endIndex: 18, style: "heading1" },
    // PR section headings
    { startIndex: 20, endIndex: 45, style: "heading2" },
    { startIndex: 287, endIndex: 325, style: "heading2" },
    // Field labels
    { startIndex: 47, endIndex: 54, style: "bold" },
    { startIndex: 73, endIndex: 80, style: "bold" },
    { startIndex: 89, endIndex: 101, style: "bold" },
    { startIndex: 327, endIndex: 334, style: "bold" },
    { startIndex: 347, endIndex: 354, style: "bold" },
  ]
})
```

### Formatting Best Practices

1. **Always insert text first**, then apply formatting — formatting operates on
   existing text ranges
2. **Calculate indices carefully** — count characters including newlines (`\n`)
3. **Heading styles apply to the entire paragraph** — even if the range covers
   only part of it
4. **Multiple styles can stack** — apply both `heading2` and `bold` to the same
   range for bold headings
5. **Use links for URLs** — apply `link` style with a `url` field instead of
   pasting raw URLs
6. **Format generously** — use heading2 for every major section, bold for every
   label or field name. A document with only a heading1 title and plain text
   body looks unprofessional

## Creating Documents

Use `docs.create` to create new documents:

- **Blank document**: Provide only a `title`
- **Document with content**: Provide `title` and `content` — the content is
  inserted into the document after creation
- **In a specific folder**: Add `folderName` to organize the document

```
docs.create({
  title: "Weekly Status Report",
  content: "Status Report - Week of March 10\n\nHighlights\n\n- ...",
  folderName: "Team Reports"
})
```

## Writing Text

Use `docs.writeText` to add text to an existing document:

- **Append to end** (default): `position: "end"` or omit position
- **Insert at beginning**: `position: "beginning"`
- **Insert at specific index**: `position: "5"` (numeric string)

```
docs.writeText({
  documentId: "doc-id",
  text: "New content\n",
  position: "end"
})
```

## Find and Replace

Use `docs.replaceText` to find all occurrences of a string and replace them.
This works across all tabs by default, or in a specific tab with `tabId`.

## Tab Management

Google Docs supports multiple tabs within a single document.

### Reading Tabs

- **Single tab**: `docs.getText` returns plain text directly
- **Multiple tabs**: Returns JSON array with `tabId`, `title`, `content`, and
  `index` for each tab
- **Specific tab**: Pass `tabId` to read only that tab
- **Nested tabs**: Child tabs are flattened and included in results

### Writing to Tabs

All write tools (`writeText`, `replaceText`, `formatText`) accept an optional
`tabId` parameter:

- **Without `tabId`**: Operates on the first tab (default)
- **With `tabId`**: Operates on the specified tab, including nested child tabs

## Document Organization

### Finding Documents

Use `drive.search` with a document MIME type filter to find Google Docs:

```
drive.search({
  query: "mimeType='application/vnd.google-apps.document' and name contains 'Weekly Report'"
})
```

For full-text search across document content, use `fullText contains` instead of
`name contains`.

### Moving Documents

Use `drive.moveFile` to move a document to a different folder. You can specify
the destination by folder ID or folder name.

## Comments & Suggestions

### Reading Comments

Use `drive.getComments` to retrieve all comments on a document:

- Returns comment threads with author, content, timestamp, and resolution status
- Includes **threaded replies** with author, content, timestamp, and action
  (e.g., `resolve`, `reopen`)
- Includes **quoted file content** showing what text the comment is anchored to

```
drive.getComments({ fileId: "doc-id" })
```

### Reading Suggestions

Use `docs.getSuggestions` to retrieve suggested edits from a document:

- **Insertions** — text proposed for addition (`suggestedInsertionIds`)
- **Deletions** — text proposed for removal (`suggestedDeletionIds`)
- **Style changes** — text formatting changes (bold, italic, etc.)
- **Paragraph style changes** — heading level changes (e.g., NORMAL_TEXT →
  HEADING_2)

Each suggestion includes the affected text, suggestion IDs, and start/end
indices.

```
docs.getSuggestions({ documentId: "doc-id" })
```

## ID Handling

- All tools accept Google Drive URLs directly — no manual ID extraction needed
- IDs and URLs are interchangeable in all `documentId` parameters


================================================
FILE: skills/google-sheets/SKILL.md
================================================
---
name: google-sheets
description: >
  Activate this skill when the user wants to find, read, or analyze Google
  Sheets spreadsheets. Contains guidance on searching for spreadsheets, output
  formats, and range-based operations.
---

# Google Sheets Expert

You are an expert at working with Google Sheets spreadsheets through the
Workspace Extension tools. Follow these guidelines when helping users with
spreadsheet tasks.

## Finding Spreadsheets

Use `drive.search` with a Sheets MIME type filter to find spreadsheets:

```
drive.search({
  query: "mimeType='application/vnd.google-apps.spreadsheet' and name contains 'Budget'"
})
```

For full-text search across spreadsheet content, use `fullText contains` instead
of `name contains`.

## Reading Data

### Full Spreadsheet

Use `sheets.getText` to read all sheets in a spreadsheet. Choose the output
format based on the use case:

- **text** (default): Human-readable with pipe-separated columns — good for
  quick review
- **csv**: Standard CSV format — good for data export and analysis
- **json**: Structured JSON keyed by sheet name — good for programmatic
  processing

### Specific Range

Use `sheets.getRange` with A1 notation to read a specific cell range:

```
sheets.getRange({
  spreadsheetId: "spreadsheet-id",
  range: "Sheet1!A1:D10"
})
```

### Metadata

Use `sheets.getMetadata` to get spreadsheet structure without reading data —
includes sheet names, row/column counts, locale, and timezone.

## ID Handling

- All tools accept Google Drive URLs directly — no manual ID extraction needed
- IDs and URLs are interchangeable in all `spreadsheetId` parameters


================================================
FILE: skills/google-slides/SKILL.md
================================================
---
name: google-slides
description: >
  Activate this skill when the user wants to find, read, or extract content from
  Google Slides presentations. Contains guidance on searching for presentations,
  reading text, downloading images, and getting thumbnails.
---

# Google Slides Expert

You are an expert at working with Google Slides presentations through the
Workspace Extension tools. Follow these guidelines when helping users with
presentation tasks.

## Finding Presentations

Use `drive.search` with a Slides MIME type filter to find presentations:

```
drive.search({
  query: "mimeType='application/vnd.google-apps.presentation' and name contains 'Quarterly Review'"
})
```

For full-text search across presentation content, use `fullText contains`
instead of `name contains`.

## Reading Content

### Text Extraction

Use `slides.getText` to extract all text content from a presentation. Text is
organized by slide with clear separators.

### Metadata

Use `slides.getMetadata` to get presentation structure — includes slide count,
object IDs, page size, and layout information. Slide object IDs from metadata
can be used with `slides.getSlideThumbnail`.

## Downloading Images

### All Images

Use `slides.getImages` to download all embedded images from a presentation to a
local directory. Requires an **absolute path** for the output directory.

### Slide Thumbnails

Use `slides.getSlideThumbnail` to download a thumbnail of a specific slide.
Requires the slide's `objectId` (from `slides.getMetadata` or `slides.getText`)
and an **absolute path** for the output file.

## ID Handling

- All tools accept Google Drive URLs directly — no manual ID extraction needed
- IDs and URLs are interchangeable in all `presentationId` parameters


================================================
FILE: tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "es2020",
    "module": "commonjs",
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["workspace-server/src/**/*"],
  "exclude": [
    "node_modules",
    "**/node_modules",
    "**/dist",
    "**/*.test.ts",
    "**/*.spec.ts"
  ]
}


================================================
FILE: workspace-server/.github/workflows/ci.yml
================================================
name: CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [18.x, 20.x]

    steps:
      - uses: actions/checkout@v4

      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
          cache-dependency-path: workspace-mcp-server/package-lock.json

      - name: Install dependencies
        run: npm ci
        working-directory: workspace-mcp-server

      - name: Run linter
        run: npm run lint --if-present
        working-directory: workspace-mcp-server

      - name: Run type checking
        run: npx tsc --noEmit
        working-directory: workspace-mcp-server

      - name: Run tests
        run: npm test
        working-directory: workspace-mcp-server

      - name: Generate coverage report
        run: npm run test:coverage
        working-directory: workspace-mcp-server

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v4
        with:
          directory: ./workspace-mcp-server/coverage
          flags: unittests
          name: codecov-umbrella
          fail_ci_if_error: false

  build:
    runs-on: ubuntu-latest
    needs: test

    steps:
      - uses: actions/checkout@v4

      - name: Use Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20.x'
          cache: 'npm'
          cache-dependency-path: workspace-mcp-server/package-lock.json

      - name: Install dependencies
        run: npm ci
        working-directory: workspace-mcp-server

      - name: Build
        run: npm run build
        working-directory: workspace-mcp-server

      - name: Upload build artifacts
        uses: actions/upload-artifact@v4
        with:
          name: dist
          path: workspace-mcp-server/dist/

  security:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Run security audit
        run: npm audit --audit-level=moderate
        working-directory: workspace-mcp-server
        continue-on-error: true

      - name: Check for known vulnerabilities
        run: npx audit-ci --moderate
        working-directory: workspace-mcp-server
        continue-on-error: true


================================================
FILE: workspace-server/.github/workflows/release.yml
================================================
name: Release

on:
  push:
    tags:
      - 'v*'

jobs:
  release:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Use Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20.x'
          cache: 'npm'
          cache-dependency-path: workspace-mcp-server/package-lock.json

      - name: Install dependencies
        run: npm ci
        working-directory: workspace-mcp-server

      - name: Run tests
        run: npm test
        working-directory: workspace-mcp-server

      - name: Build
        run: npm run build
        working-directory: workspace-mcp-server

      - name: Create Release
        id: create_release
        uses: actions/create-release@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag_name: ${{ github.ref }}
          release_name: Release ${{ github.ref }}
          draft: false
          prerelease: false

      - name: Upload Release Asset
        uses: actions/upload-release-asset@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          upload_url: ${{ steps.create_release.outputs.upload_url }}
          asset_path: ./workspace-mcp-server/dist/index.js
          asset_name: workspace-mcp-server.js
          asset_content_type: application/javascript


================================================
FILE: workspace-server/WORKSPACE-Context.md
================================================
# Google Workspace Extension - Behavioral Guide

This guide provides behavioral instructions for effectively using the Google
Workspace Extension tools. For detailed parameter documentation, refer to the
tool descriptions in the extension itself.

## 🎯 Core Principles

### 1. User Context First

**Always establish user context at the beginning of interactions:**

- Use `people.getMe()` to understand who the user is
- Use `time.getTimeZone()` to get the user's local timezone
- Apply this context throughout all interactions
- All time-based operations should respect the user's timezone

### 2. Safety and Transparency

**Never execute write operations without explicit confirmation:**

- Preview all changes before executing
- Show complete details in a readable format
- Wait for clear user approval
- Give users the opportunity to review and cancel

### 3. Smart Tool Usage

**Choose the right approach for each task:**

- Tools automatically handle URL-to-ID conversion - don't extract IDs manually
- Batch related operations when possible
- Use pagination for large result sets
- Apply appropriate formats based on the use case

## 📋 Output Formatting Standards

### Lists and Search Results

Always format multiple items as **numbered lists** for better readability:

✅ **Correct:**

```
Found 3 documents:
1. Budget Report 2024
2. Q3 Sales Presentation
3. Team Meeting Notes
```

❌ **Incorrect:**

```
Found 3 documents:
- Budget Report 2024
- Q3 Sales Presentation
- Team Meeting Notes
```

### Write Operation Previews

Before any write operation, show a clear preview:

```
I'll create this calendar event:

Title: Team Standup
Date: January 15, 2025
Time: 10:00 AM - 10:30 AM (EST)
Attendees: team@example.com

Should I create this event?
```

## 🔄 Multi-Tool Workflows

### Creating and Organizing Documents

When creating documents in specific folders:

1. Create the document with `docs.create` (blank)
2. Move it to the target folder with `drive.moveFile`
3. Confirm successful completion

To find Google Docs, Sheets, or Slides, use `drive.search` with a MIME type
filter rather than searching by name alone. Example MIME type queries:

- Docs:
  `mimeType='application/vnd.google-apps.document' and name contains 'query'`
- Sheets:
  `mimeType='application/vnd.google-apps.spreadsheet' and name contains 'query'`
- Slides:
  `mimeType='application/vnd.google-apps.presentation' and name contains 'query'`

## 🚫 Common Pitfalls to Avoid

### Don't Do This:

- ❌ Use `extractIdFromUrl` when other tools accept URLs
- ❌ Assume timezone without checking
- ❌ Execute writes without preview and confirmation
- ❌ Create files unless explicitly requested
- ❌ Duplicate parameter documentation from tool descriptions
- ❌ Use relative paths for file downloads (e.g., `downloads/file.txt`)

### Do This Instead:

- ✅ Pass URLs directly to tools that accept them
- ✅ Get user timezone at session start
- ✅ Preview all changes and wait for approval
- ✅ Only create what's requested
- ✅ Focus on behavioral guidance and best practices
- ✅ Always use **absolute paths** for file downloads (e.g.,
  `/Users/me/Downloads/file.txt`)

## 🔍 Error Handling Patterns

### Authentication Errors

- If any tool returns `{"error":"invalid_request"}`, it likely indicates an
  expired or invalid session.
- **Action:** Call `auth.clear` to reset credentials and force a re-login.
- Inform the user that you are resetting authentication due to an error.

### Graceful Degradation

- If a folder doesn't exist, offer to create it
- If search returns no results, suggest alternatives
- If permissions are insufficient, explain clearly

### Validation Before Action

- Verify file/folder existence before moving
- Check calendar availability before scheduling
- Validate email addresses before sending

## ⚡ Performance Optimization

### Batch Operations

- Group related API calls when possible
- Use field masks to request only needed data
- Implement pagination for large datasets

### Caching Strategy

- Reuse user context throughout session
- Cache frequently accessed metadata
- Minimize redundant API calls

## 📝 Session Management

### Beginning of Session

1. Get user profile with `people.getMe()`
2. Get timezone with `time.getTimeZone()`
3. Establish any relevant context

### During Interaction

- Maintain context awareness
- Apply user preferences consistently
- Handle follow-up questions efficiently

### End of Session

- Confirm all requested tasks completed
- Provide summary if multiple operations performed
- Ensure no pending confirmations

## 🎨 Service-Specific Nuances

### Google Docs

- See the **Google Docs skill** for detailed guidance on document content
  formatting, creation, editing, tab management, and document organization.

### Google Sheets

- See the **Google Sheets skill** for detailed guidance on finding spreadsheets,
  output format selection, and range-based operations.

### Google Slides

- See the **Google Slides skill** for detailed guidance on finding
  presentations, text extraction, image downloads, and slide thumbnails.

### Google Calendar

- See the **Google Calendar skill** for detailed guidance on timezone handling,
  meeting queries, event management, responding to invitations, and scheduling.

### Gmail

- See the **Gmail skill** for detailed guidance on composing rich HTML emails,
  search syntax, label management, attachments, and threading.

### Google Chat

- See the **Google Chat skill** for detailed guidance on formatting messages,
  spaces vs. DMs, threading, unread filtering, and space management.

Remember: This guide focuses on **how to think** about using these tools
effectively. For specific parameter details, refer to the tool descriptions
themselves.


================================================
FILE: workspace-server/esbuild.auth-utils.js
================================================
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

const esbuild = require('esbuild');
const path = require('node:path');

async function buildAuthUtils() {
  try {
    await esbuild.build({
      entryPoints: ['src/auth/token-storage/oauth-credential-storage.ts'],
      bundle: true,
      platform: 'node',
      target: 'node20',
      outfile: 'dist/auth-utils.js',
      minify: true,
      sourcemap: true,
      external: [
        'keytar', // keytar is a native module and should not be bundled
      ],
      format: 'cjs',
      logLevel: 'info',
    });

    console.log('Auth Utils build completed successfully!');
  } catch (error) {
    console.error('Auth Utils build failed:', error);
    process.exit(1);
  }
}

buildAuthUtils();


================================================
FILE: workspace-server/esbuild.config.js
================================================
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

const esbuild = require('esbuild');
const path = require('node:path');
const fs = require('node:fs');

async function build() {
  try {
    await esbuild.build({
      entryPoints: ['src/index.ts'],
      bundle: true,
      platform: 'node',
      target: 'node16',
      outfile: 'dist/index.js',
      minify: true,
      sourcemap: true,
      // Replace 'open' package with our wrapper
      alias: {
        open: path.resolve(__dirname, 'src/utils/open-wrapper.ts'),
      },
      // External packages that shouldn't be bundled
      external: [],
      // Add a loader for .node files
      loader: {
        '.node': 'file',
      },
      // Make sure CommonJS modules work properly
      format: 'cjs',
      logLevel: 'info',
    });

    console.log('Build completed successfully!');
  } catch (error) {
    console.error('Build failed:', error);
    process.exit(1);
  }
}

build();


================================================
FILE: workspace-server/esbuild.headless-login.js
================================================
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

const esbuild = require('esbuild');

async function buildHeadlessLogin() {
  try {
    await esbuild.build({
      entryPoints: ['src/cli/headless-login.ts'],
      bundle: true,
      platform: 'node',
      target: 'node20',
      outfile: 'dist/headless-login.js',
      minify: true,
      sourcemap: true,
      external: [
        'keytar', // keytar is a native module and should not be bundled
      ],
      format: 'cjs',
      logLevel: 'info',
    });

    console.log('Headless Login build completed successfully!');
  } catch (error) {
    console.error('Headless Login build failed:', error);
    process.exit(1);
  }
}

buildHeadlessLogin();


================================================
FILE: workspace-server/jest.config.js
================================================
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

/** @type {import('jest').Config} */
module.exports = {
  // This workspace's tests are configured in the root jest.config.js
  // as part of the 'projects' array. This file is kept for backwards
  // compatibility and workspace-specific overrides if needed.
};


================================================
FILE: workspace-server/package.json
================================================
{
  "name": "workspace-server",
  "version": "0.0.8",
  "description": "",
  "main": "dist/index.js",
  "scripts": {
    "test": "cd .. && node --max-old-space-size=4096 node_modules/jest/bin/jest.js --runInBand --verbose",
    "test:watch": "cd .. && jest --watch",
    "test:coverage": "cd .. && node --max-old-space-size=4096 node_modules/jest/bin/jest.js --coverage",
    "test:ci": "cd .. && node --max-old-space-size=4096 node_modules/jest/bin/jest.js --ci --coverage --maxWorkers=2",
    "start": "ts-node src/index.ts",
    "build": "node esbuild.config.js && node esbuild.headless-login.js",
    "build:auth-utils": "node esbuild.auth-utils.js",
    "build:headless-login": "node esbuild.headless-login.js"
  },
  "keywords": [],
  "author": "Allen Hutchison",
  "license": "Apache-2.0",
  "type": "commonjs",
  "devDependencies": {
    "esbuild": "^0.28.0"
  }
}


================================================
FILE: workspace-server/src/__tests__/auth/AuthManager.test.ts
================================================
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { AuthManager } from '../../auth/AuthManager';
import { OAuthCredentialStorage } from '../../auth/token-storage/oauth-credential-storage';
import { google } from 'googleapis';

// Mock dependencies
jest.mock('../../auth/token-storage/oauth-credential-storage');
jest.mock('googleapis');
jest.mock('../../utils/logger');
jest.mock('../../utils/secure-browser-launcher');

// Mock fetch globally for refreshToken tests
global.fetch = jest.fn();

describe('AuthManager', () => {
  let authManager: AuthManager;
  let mockOAuth2Client: any;

  beforeEach(() => {
    jest.clearAllMocks();

    // Setup mock OAuth2 client
    mockOAuth2Client = {
      setCredentials: jest.fn().mockImplementation((creds) => {
        mockOAuth2Client.credentials = creds;
      }),
      generateAuthUrl: jest.fn(),
      on: jest.fn(),
      refreshAccessToken: jest.fn(),
      credentials: {},
    };

    (google.auth.OAuth2 as unknown as jest.Mock).mockReturnValue(
      mockOAuth2Client,
    );

    authManager = new AuthManager(['scope1']);
  });

  it('should set up tokens event listener on client creation', async () => {
    (OAuthCredentialStorage.loadCredentials as jest.Mock).mockResolvedValue({
      access_token: 'old_token',
      refresh_token: 'old_refresh',
      scope: 'scope1',
    });

    await authManager.getAuthenticatedClient();

    // Verify 'on' was called for 'tokens'
    expect(mockOAuth2Client.on).toHaveBeenCalledWith(
      'tokens',
      expect.any(Function),
    );
  });

  it('should save credentials when tokens event is emitted', async () => {
    (OAuthCredentialStorage.loadCredentials as jest.Mock).mockResolvedValue({
      access_token: 'old_token',
      refresh_token: 'old_refresh',
      scope: 'scope1',
    });

    await authManager.getAuthenticatedClient();

    // Get the registered callback
    const tokensCallback = mockOAuth2Client.on.mock.calls.find(
      (call: any[]) => call[0] === 'tokens',
    )[1];
    expect(tokensCallback).toBeDefined();

    // Simulate tokens event
    const newTokens = {
      access_token: 'new_token',
      expiry_date: 123456789,
    };

    await tokensCallback(newTokens);

    // Verify saveCredentials was called with merged tokens
    // New tokens take precedence, but refresh_token is preserved from old credentials
    expect(OAuthCredentialStorage.saveCredentials).toHaveBeenCalledWith({
      access_token: 'new_token',
      refresh_token: 'old_refresh', // Preserved from old credentials
      expiry_date: 123456789,
      // Note: scope is NOT preserved because newTokens didn't include it
    });
  });

  it('should preserve refresh token during manual refresh if not returned', async () => {
    // Setup initial state with a refresh token
    (OAuthCredentialStorage.loadCredentials as jest.Mock).mockResolvedValue({
      access_token: 'old_token',
      refresh_token: 'old_refresh_token',
      scope: 'scope1',
    });

    // Initialize client to populate this.client
    await authManager.getAuthenticatedClient();

    // Mock fetch to simulate cloud function returning new tokens without refresh_token
    (global.fetch as jest.Mock).mockResolvedValue({
      ok: true,
      json: async () => ({
        access_token: 'new_access_token',
        expiry_date: 999999999,
      }),
    });

    await authManager.refreshToken();

    // Verify saveCredentials was called with BOTH new access token AND old refresh token
    expect(OAuthCredentialStorage.saveCredentials).toHaveBeenCalledWith(
      expect.objectContaining({
        access_token: 'new_access_token',
        refresh_token: 'old_refresh_token',
      }),
    );
  });

  it('should preserve refresh token when refreshAccessToken mutates credentials in-place', async () => {
    // Setup initial state with a refresh token
    (OAuthCredentialStorage.loadCredentials as jest.Mock).mockResolvedValue({
      access_token: 'old_token',
      refresh_token: 'old_refresh_token',
      scope: 'scope1',
    });

    // Initialize client to populate this.client
    await authManager.getAuthenticatedClient();

    // Mock fetch to simulate cloud function returning new tokens without refresh_token
    (global.fetch as jest.Mock).mockResolvedValue({
      ok: true,
      json: async () => ({
        access_token: 'new_access_token',
        expiry_date: 999999999,
      }),
    });

    await authManager.refreshToken();

    // This test verifies that the refresh_token is preserved even when
    // the cloud function doesn't return it in the response
    expect(OAuthCredentialStorage.saveCredentials).toHaveBeenCalledWith(
      expect.objectContaining({
        access_token: 'new_access_token',
        refresh_token: 'old_refresh_token',
      }),
    );
  });

  it('should preserve refresh token in tokens event handler', async () => {
    // Setup initial state with a refresh token in storage
    (OAuthCredentialStorage.loadCredentials as jest.Mock).mockResolvedValue({
      access_token: 'old_token',
      refresh_token: 'stored_refresh_token',
      scope: 'scope1',
    });

    await authManager.getAuthenticatedClient();

    // Get the registered callback
    const tokensCallback = mockOAuth2Client.on.mock.calls.find(
      (call: any[]) => call[0] === 'tokens',
    )[1];

    // Simulate automatic refresh that doesn't include refresh_token
    const newTokens = {
      access_token: 'auto_refreshed_token',
      expiry_date: 999999999,
      // Note: no refresh_token
    };

    await tokensCallback(newTokens);

    // Verify saveCredentials was called with BOTH new access token AND stored refresh token
    expect(OAuthCredentialStorage.saveCredentials).toHaveBeenCalledWith({
      access_token: 'auto_refreshed_token',
      expiry_date: 999999999,
      refresh_token: 'stored_refresh_token',
    });
  });

  it('should proactively refresh expired tokens before returning client', async () => {
    // Setup: Load credentials with expired token
    const expiredTime = Date.now() - 1000; // 1 second ago
    (OAuthCredentialStorage.loadCredentials as jest.Mock).mockResolvedValue({
      access_token: 'expired_token',
      refresh_token: 'valid_refresh',
      expiry_date: expiredTime,
      scope: 'scope1',
    });

    // Mock fetch to simulate cloud function returning fresh tokens
    (global.fetch as jest.Mock).mockResolvedValue({
      ok: true,
      json: async () => ({
        access_token: 'fresh_token',
        expiry_date: Date.now() + 3600000,
      }),
    });

    // First call: load expired credentials from storage, should trigger proactive refresh
    const firstClient = await authManager.getAuthenticatedClient();
    expect(firstClient).toBeDefined();

    // Verify fetch was called to refresh the token
    expect(global.fetch).toHaveBeenCalledWith(
      'https://google-workspace-extension.geminicli.com/refreshToken',
      expect.objectContaining({
        method: 'POST',
        body: expect.stringContaining('valid_refresh'),
      }),
    );

    // Verify new token was saved with preserved refresh_token
    expect(OAuthCredentialStorage.saveCredentials).toHaveBeenCalledWith(
      expect.objectContaining({
        access_token: 'fresh_token',
        refresh_token: 'valid_refresh',
      }),
    );
  });

  it('should proactively refresh tokens expiring within buffer (5 minutes)', async () => {
    // Setup: Load credentials with token expiring in 4 minutes (within 5 min buffer)
    const TEST_EXPIRY_WITHIN_BUFFER = 4 * 60 * 1000;
    const expiresIn4Minutes = Date.now() + TEST_EXPIRY_WITHIN_BUFFER;
    (OAuthCredentialStorage.loadCredentials as jest.Mock).mockResolvedValue({
      access_token: 'soon_expiring_token',
      refresh_token: 'valid_refresh',
      expiry_date: expiresIn4Minutes,
      scope: 'scope1',
    });

    // Mock fetch to simulate cloud function returning fresh tokens
    (global.fetch as jest.Mock).mockResolvedValue({
      ok: true,
      json: async () => ({
        access_token: 'fresh_token',
        expiry_date: Date.now() + 60 * 60 * 1000,
      }),
    });

    // Call getAuthenticatedClient
    const client = await authManager.getAuthenticatedClient();
    expect(client).toBeDefined();

    // Verify fetch was called to refresh the token because it was within buffer
    expect(global.fetch).toHaveBeenCalledWith(
      'https://google-workspace-extension.geminicli.com/refreshToken',
      expect.objectContaining({
        method: 'POST',
        body: expect.stringContaining('valid_refresh'),
      }),
    );
  });
});


================================================
FILE: workspace-server/src/__tests__/auth/token-storage/base-token-storage.test.ts
================================================
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { describe, it, expect, beforeEach } from '@jest/globals';
import { BaseTokenStorage } from '../../../auth/token-storage/base-token-storage';
import type {
  OAuthCredentials,
  OAuthToken,
} from '../../../auth/token-storage/types';

class TestTokenStorage extends BaseTokenStorage {
  private storage = new Map<string, OAuthCredentials>();

  async getCredentials(serverName: string): Promise<OAuthCredentials | null> {
    return this.storage.get(serverName) || null;
  }

  async setCredentials(credentials: OAuthCredentials): Promise<void> {
    this.validateCredentials(credentials);
    this.storage.set(credentials.serverName, credentials);
  }

  async deleteCredentials(serverName: string): Promise<void> {
    this.storage.delete(serverName);
  }

  async listServers(): Promise<string[]> {
    return Array.from(this.storage.keys());
  }

  async getAllCredentials(): Promise<Map<string, OAuthCredentials>> {
    return new Map(this.storage);
  }

  async clearAll(): Promise<void> {
    this.storage.clear();
  }

  override validateCredentials(credentials: OAuthCredentials): void {
    super.validateCredentials(credentials);
  }

  override sanitizeServerName(serverName: string): string {
    return super.sanitizeServerName(serverName);
  }
}

describe('BaseTokenStorage', () => {
  let storage: TestTokenStorage;

  beforeEach(() => {
    storage = new TestTokenStorage('gemini-cli-mcp-oauth');
  });

  describe('validateCredentials', () => {
    it('should validate valid credentials with access token', () => {
      const credentials: OAuthCredentials = {
        serverName: 'test-server',
        token: {
          accessToken: 'access-token',
          tokenType: 'Bearer',
        },
        updatedAt: Date.now(),
      };

      expect(() => storage.validateCredentials(credentials)).not.toThrow();
    });

    it('should validate valid credentials with refresh token', () => {
      const credentials: OAuthCredentials = {
        serverName: 'test-server',
        token: {
          refreshToken: 'refresh-token',
          tokenType: 'Bearer',
        },
        updatedAt: Date.now(),
      };

      expect(() => storage.validateCredentials(credentials)).not.toThrow();
    });

    it('should throw for missing server name', () => {
      const credentials = {
        serverName: '',
        token: {
          accessToken: 'access-token',
          tokenType: 'Bearer',
        },
        updatedAt: Date.now(),
      } as OAuthCredentials;

      expect(() => storage.validateCredentials(credentials)).toThrow(
        'Server name is required',
      );
    });

    it('should throw for missing token', () => {
      const credentials = {
        serverName: 'test-server',
        token: null as unknown as OAuthToken,
        updatedAt: Date.now(),
      } as OAuthCredentials;

      expect(() => storage.validateCredentials(credentials)).toThrow(
        'Token is required',
      );
    });

    it('should throw for missing access token and refresh token', () => {
      const credentials = {
        serverName: 'test-server',
        token: {
          accessToken: '',
          tokenType: 'Bearer',
        },
        updatedAt: Date.now(),
      } as OAuthCredentials;

      expect(() => storage.validateCredentials(credentials)).toThrow(
        'Access token or refresh token is required',
      );
    });

    it('should throw for missing token type', () => {
      const credentials = {
        serverName: 'test-server',
        token: {
          accessToken: 'access-token',
          tokenType: '',
        },
        updatedAt: Date.now(),
      } as OAuthCredentials;

      expect(() => storage.validateCredentials(credentials)).toThrow(
        'Token type is required',
      );
    });
  });

  describe('sanitizeServerName', () => {
    it('should keep valid characters', () => {
      expect(storage.sanitizeServerName('test-server.example_123')).toBe(
        'test-server.example_123',
      );
    });

    it('should replace invalid characters with underscore', () => {
      expect(storage.sanitizeServerName('test@server#example')).toBe(
        'test_server_example',
      );
    });

    it('should handle special characters', () => {
      expect(storage.sanitizeServerName('test server/example:123')).toBe(
        'test_server_example_123',
      );
    });
  });
});


================================================
FILE: workspace-server/src/__tests__/auth/token-storage/file-token-storage.test.ts
================================================
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import {
  describe,
  it,
  expect,
  beforeEach,
  afterEach,
  jest,
} from '@jest/globals';
import * as crypto from 'node:crypto';
import { promises as fs } from 'node:fs';
import * as path from 'node:path';
import { FileTokenStorage } from '../../../auth/token-storage/file-token-storage';
import type { OAuthCredentials } from '../../../auth/token-storage/types';
import {
  ENCRYPTED_TOKEN_PATH,
  ENCRYPTION_MASTER_KEY_PATH,
} from '../../../utils/paths';

jest.mock('node:fs', () => ({
  promises: {
    readFile: jest.fn(),
    writeFile: jest.fn(),
    unlink: jest.fn(),
    mkdir: jest.fn(),
  },
  existsSync: jest.fn(() => true),
}));

jest.mock('node:os', () => ({
  default: {
    homedir: jest.fn(() => '/home/test'),
    hostname: jest.fn(() => 'test-host'),
    userInfo: jest.fn(() => ({ username: 'test-user' })),
  },
  homedir: jest.fn(() => '/home/test'),
  hostname: jest.fn(() => 'test-host'),
  userInfo: jest.fn(() => ({ username: 'test-user' })),
}));

describe('FileTokenStorage', () => {
  let storage: FileTokenStorage;
  const mockFs = fs as unknown as {
    readFile: ReturnType<typeof jest.fn>;
    writeFile: ReturnType<typeof jest.fn>;
    unlink: ReturnType<typeof jest.fn>;
    mkdir: ReturnType<typeof jest.fn>;
  };

  const existingCredentials: OAuthCredentials = {
    serverName: 'existing-server',
    token: {
      accessToken: 'existing-token',
      tokenType: 'Bearer',
    },
    updatedAt: Date.now() - 10000,
  };

  afterEach(() => {
    jest.clearAllMocks();
  });

  describe('when master key does not exist', () => {
    it('should create a new master key', async () => {
      const error = new Error('File not found');
      (error as NodeJS.ErrnoException).code = 'ENOENT';
      mockFs.readFile.mockRejectedValue(error);
      storage = await FileTokenStorage.create('test-storage');

      expect(mockFs.readFile).toHaveBeenCalledWith(ENCRYPTION_MASTER_KEY_PATH);
      expect(mockFs.writeFile).toHaveBeenCalledWith(
        ENCRYPTION_MASTER_KEY_PATH,
        expect.any(Buffer),
        { mode: 0o600 },
      );
    });
  });

  describe('when master key exists', () => {
    it('should load the master key without creating a new one', async () => {
      const masterKey = crypto.randomBytes(32);
      mockFs.readFile.mockResolvedValue(masterKey);
      storage = await FileTokenStorage.create('test-storage');
      expect(mockFs.readFile).toHaveBeenCalledWith(ENCRYPTION_MASTER_KEY_PATH);
      expect(mockFs.writeFile).not.toHaveBeenCalled();
    });
  });

  describe('getCredentials', () => {
    beforeEach(async () => {
      // All tests assume a master key exists.
      const masterKey = crypto.randomBytes(32);
      mockFs.readFile.mockResolvedValue(masterKey);
      storage = await FileTokenStorage.create('test-storage');
    });

    it('should return null when file does not exist', async () => {
      mockFs.readFile.mockRejectedValue({ code: 'ENOENT' });

      const result = await storage.getCredentials('test-server');
      expect(result).toBeNull();
    });

    it('should return credentials even if access token is expired', async () => {
      const credentials: OAuthCredentials = {
        serverName: 'test-server',
        token: {
          accessToken: 'access-token',
          tokenType: 'Bearer',
          expiresAt: Date.now() - 3600000,
        },
        updatedAt: Date.now(),
      };

      const encryptedData = (storage as any).encrypt(
        JSON.stringify({ 'test-server': credentials }),
      );
      mockFs.readFile.mockResolvedValue(encryptedData);

      const result = await storage.getCredentials('test-server');
      expect(result).toEqual(credentials);
    });

    it('should return credentials for valid tokens', async () => {
      const credentials: OAuthCredentials = {
        serverName: 'test-server',
        token: {
          accessToken: 'access-token',
          tokenType: 'Bearer',
          expiresAt: Date.now() + 3600000,
        },
        updatedAt: Date.now(),
      };

      const encryptedData = (storage as any).encrypt(
        JSON.stringify({ 'test-server': credentials }),
      );
      mockFs.readFile.mockResolvedValue(encryptedData);

      const result = await storage.getCredentials('test-server');
      expect(result).toEqual(credentials);
    });

    it('should return null for corrupted files', async () => {
      mockFs.readFile.mockResolvedValue('corrupted-data');

      const result = await storage.getCredentials('test-server');
      expect(result).toBeNull();
    });
  });

  describe('setCredentials', () => {
    beforeEach(async () => {
      // All tests assume a master key exists.
      const masterKey = crypto.randomBytes(32);
      mockFs.readFile.mockResolvedValue(masterKey);
      storage = await FileTokenStorage.create('test-storage');
    });
    it('should save credentials with encryption', async () => {
      const encryptedData = (storage as any).encrypt(
        JSON.stringify({ 'existing-server': existingCredentials }),
      );
      mockFs.readFile.mockResolvedValue(encryptedData);
      mockFs.mkdir.mockResolvedValue(undefined);
      mockFs.writeFile.mockResolvedValue(undefined);

      const credentials: OAuthCredentials = {
        serverName: 'test-server',
        token: {
          accessToken: 'access-token',
          tokenType: 'Bearer',
        },
        updatedAt: Date.now(),
      };

      await storage.setCredentials(credentials);

      expect(mockFs.mkdir).toHaveBeenCalledWith(
        path.dirname(ENCRYPTED_TOKEN_PATH),
        { recursive: true, mode: 0o700 },
      );
      expect(mockFs.writeFile).toHaveBeenCalled();

      const writeCall = mockFs.writeFile.mock.calls[0];
      expect(writeCall[0]).toBe(ENCRYPTED_TOKEN_PATH);
      expect(writeCall[1]).toMatch(/^[0-9a-f]+:[0-9a-f]+:[0-9a-f]+$/);
      expect(writeCall[2]).toEqual({ mode: 0o600 });
    });

    it('should update existing credentials', async () => {
      const encryptedData = (storage as any).encrypt(
        JSON.stringify({ 'existing-server': existingCredentials }),
      );
      mockFs.readFile.mockResolvedValue(encryptedData);
      mockFs.writeFile.mockResolvedValue(undefined);

      const newCredentials: OAuthCredentials = {
        serverName: 'test-server',
        token: {
          accessToken: 'new-token',
          tokenType: 'Bearer',
        },
        updatedAt: Date.now(),
      };

      await storage.setCredentials(newCredentials);

      expect(mockFs.writeFile).toHaveBeenCalled();
      const writeCall = mockFs.writeFile.mock.calls[0];
      const decrypted = (storage as any).decrypt(writeCall[1]);
      const saved = JSON.parse(decrypted);

      expect(saved['existing-server']).toEqual(existingCredentials);
      expect(saved['test-server'].token.accessToken).toBe('new-token');
    });
  });

  describe('deleteCredentials', () => {
    beforeEach(async () => {
      // All tests assume a master key exists.
      const masterKey = crypto.randomBytes(32);
      mockFs.readFile.mockResolvedValue(masterKey);
      storage = await FileTokenStorage.create('test-storage');
    });
    it('should throw when credentials do not exist', async () => {
      mockFs.readFile.mockRejectedValue({ code: 'ENOENT' });

      await expect(storage.deleteCredentials('test-server')).rejects.toThrow(
        'No credentials found for test-server',
      );
    });

    it('should delete file when last credential is removed', async () => {
      const credentials: OAuthCredentials = {
        serverName: 'test-server',
        token: {
          accessToken: 'access-token',
          tokenType: 'Bearer',
        },
        updatedAt: Date.now(),
      };

      const encryptedData = (storage as any).encrypt(
        JSON.stringify({ 'test-server': credentials }),
      );
      mockFs.readFile.mockResolvedValue(encryptedData);
      mockFs.unlink.mockResolvedValue(undefined);

      await storage.deleteCredentials('test-server');

      expect(mockFs.unlink).toHaveBeenCalledWith(ENCRYPTED_TOKEN_PATH);
    });

    it('should update file when other credentials remain', async () => {
      const credentials1: OAuthCredentia
Download .txt
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
Download .txt
SYMBOL INDEX (245 symbols across 45 files)

FILE: cloud_function/index.js
  constant CLIENT_ID (line 17) | const CLIENT_ID = process.env.CLIENT_ID;
  constant SECRET_NAME (line 18) | const SECRET_NAME = process.env.SECRET_NAME;
  constant REDIRECT_URI (line 19) | const REDIRECT_URI = process.env.REDIRECT_URI;
  constant KEYCHAIN_SERVICE_NAME (line 22) | const KEYCHAIN_SERVICE_NAME = 'gemini-cli-workspace-oauth';
  constant KEYCHAIN_ACCOUNT_NAME (line 23) | const KEYCHAIN_ACCOUNT_NAME = 'main-account';
  function getClientSecret (line 32) | async function getClientSecret() {
  function handleCallback (line 51) | async function handleCallback(req, res) {
  function handleRefreshToken (line 284) | async function handleRefreshToken(req, res) {

FILE: scripts/auth-utils.js
  function clearAuth (line 11) | async function clearAuth() {
  function expireToken (line 21) | async function expireToken() {
  function showStatus (line 40) | async function showStatus() {
  function login (line 77) | async function login() {
  function showHelp (line 89) | function showHelp() {
  function main (line 110) | async function main() {

FILE: scripts/clean.js
  constant RMRF (line 11) | const RMRF = { recursive: true, force: true };
  function rmrfSyncVerbose (line 13) | function rmrfSyncVerbose(path) {

FILE: scripts/start.js
  function runCommand (line 10) | function runCommand(command, args, options) {
  function main (line 42) | async function main() {

FILE: scripts/utils/dependencies.js
  function getDependencies (line 16) | function getDependencies(rootDir, pkgName) {
  function getTransitiveDependencies (line 31) | function getTransitiveDependencies(rootDir, startPkgs) {

FILE: workspace-server/esbuild.auth-utils.js
  function buildAuthUtils (line 10) | async function buildAuthUtils() {

FILE: workspace-server/esbuild.config.js
  function build (line 11) | async function build() {

FILE: workspace-server/esbuild.headless-login.js
  function buildHeadlessLogin (line 9) | async function buildHeadlessLogin() {

FILE: workspace-server/src/__tests__/auth/token-storage/base-token-storage.test.ts
  class TestTokenStorage (line 14) | class TestTokenStorage extends BaseTokenStorage {
    method getCredentials (line 17) | async getCredentials(serverName: string): Promise<OAuthCredentials | n...
    method setCredentials (line 21) | async setCredentials(credentials: OAuthCredentials): Promise<void> {
    method deleteCredentials (line 26) | async deleteCredentials(serverName: string): Promise<void> {
    method listServers (line 30) | async listServers(): Promise<string[]> {
    method getAllCredentials (line 34) | async getAllCredentials(): Promise<Map<string, OAuthCredentials>> {
    method clearAll (line 38) | async clearAll(): Promise<void> {
    method validateCredentials (line 42) | override validateCredentials(credentials: OAuthCredentials): void {
    method sanitizeServerName (line 46) | override sanitizeServerName(serverName: string): string {

FILE: workspace-server/src/__tests__/auth/token-storage/hybrid-token-storage.test.ts
  constant KEYCHAIN_TOKEN_STORAGE_PATH (line 21) | const KEYCHAIN_TOKEN_STORAGE_PATH =
  constant FILE_TOKEN_STORAGE_PATH (line 23) | const FILE_TOKEN_STORAGE_PATH =
  constant HYBRID_TOKEN_STORAGE_PATH (line 25) | const HYBRID_TOKEN_STORAGE_PATH =
  type MockStorage (line 28) | interface MockStorage {

FILE: workspace-server/src/__tests__/services/CalendarValidation.test.ts
  function getZodIssueMessages (line 14) | function getZodIssueMessages(fn: () => void): string[] {

FILE: workspace-server/src/__tests__/utils/logger.test.ts
  function setupLogger (line 26) | async function setupLogger(appendFileMock?: any) {

FILE: workspace-server/src/__tests__/utils/secure-browser-launcher.test.ts
  function simulateSuccess (line 38) | function simulateSuccess() {
  function simulateFailure (line 44) | function simulateFailure(error = new Error('Command failed')) {

FILE: workspace-server/src/auth/AuthManager.ts
  constant CLIENT_ID (line 19) | const CLIENT_ID = config.clientId;
  constant CLOUD_FUNCTION_URL (line 20) | const CLOUD_FUNCTION_URL = config.cloudFunctionUrl;
  constant TOKEN_EXPIRY_BUFFER_MS (line 21) | const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000;
  type OauthWebLogin (line 28) | interface OauthWebLogin {
  class AuthManager (line 33) | class AuthManager {
    method constructor (line 38) | constructor(scopes: string[]) {
    method setOnStatusUpdate (line 42) | public setOnStatusUpdate(callback: (message: string) => void) {
    method isTokenExpiringSoon (line 46) | private isTokenExpiringSoon(credentials: Auth.Credentials): boolean {
    method loadCachedCredentials (line 53) | private async loadCachedCredentials(
    method getAuthenticatedClient (line 84) | public async getAuthenticatedClient(): Promise<Auth.OAuth2Client> {
    method clearAuth (line 214) | public async clearAuth(): Promise<void> {
    method refreshToken (line 221) | public async refreshToken(): Promise<void> {
    method getAvailablePort (line 273) | private async getAvailablePort(): Promise<number> {
    method authWithWeb (line 304) | private async authWithWeb(client: Auth.OAuth2Client): Promise<OauthWeb...

FILE: workspace-server/src/auth/scopes.ts
  constant SCOPES (line 17) | const SCOPES: string[] = resolveFeatures(

FILE: workspace-server/src/auth/token-storage/base-token-storage.ts
  method constructor (line 12) | constructor(serviceName: string) {
  method validateCredentials (line 23) | protected validateCredentials(credentials: OAuthCredentials): void {
  method sanitizeServerName (line 38) | protected sanitizeServerName(serverName: string): string {

FILE: workspace-server/src/auth/token-storage/file-token-storage.ts
  class FileTokenStorage (line 19) | class FileTokenStorage extends BaseTokenStorage {
    method constructor (line 24) | private constructor(serviceName: string, masterKey: Buffer) {
    method create (line 31) | static async create(serviceName: string): Promise<FileTokenStorage> {
    method loadMasterKey (line 36) | private static async loadMasterKey(): Promise<Buffer> {
    method deriveEncryptionKey (line 51) | private deriveEncryptionKey(): Buffer {
    method encrypt (line 58) | private encrypt(text: string): string {
    method decrypt (line 70) | private decrypt(encryptedData: string): string {
    method ensureDirectoryExists (line 93) | private async ensureDirectoryExists(): Promise<void> {
    method loadTokens (line 98) | private async loadTokens(): Promise<Map<string, OAuthCredentials>> {
    method saveTokens (line 123) | private async saveTokens(
    method getCredentials (line 135) | async getCredentials(serverName: string): Promise<OAuthCredentials | n...
    method setCredentials (line 146) | async setCredentials(credentials: OAuthCredentials): Promise<void> {
    method deleteCredentials (line 159) | async deleteCredentials(serverName: string): Promise<void> {
    method listServers (line 182) | async listServers(): Promise<string[]> {
    method getAllCredentials (line 187) | async getAllCredentials(): Promise<Map<string, OAuthCredentials>> {
    method clearAll (line 203) | async clearAll(): Promise<void> {

FILE: workspace-server/src/auth/token-storage/hybrid-token-storage.ts
  constant FORCE_FILE_STORAGE_ENV_VAR (line 12) | const FORCE_FILE_STORAGE_ENV_VAR = 'GEMINI_CLI_WORKSPACE_FORCE_FILE_STOR...
  class HybridTokenStorage (line 14) | class HybridTokenStorage extends BaseTokenStorage {
    method constructor (line 19) | constructor(serviceName: string) {
    method initializeStorage (line 23) | private async initializeStorage(): Promise<TokenStorage> {
    method getStorage (line 52) | private async getStorage(): Promise<TokenStorage> {
    method getCredentials (line 66) | async getCredentials(serverName: string): Promise<OAuthCredentials | n...
    method setCredentials (line 71) | async setCredentials(credentials: OAuthCredentials): Promise<void> {
    method deleteCredentials (line 76) | async deleteCredentials(serverName: string): Promise<void> {
    method listServers (line 81) | async listServers(): Promise<string[]> {
    method getAllCredentials (line 86) | async getAllCredentials(): Promise<Map<string, OAuthCredentials>> {
    method clearAll (line 91) | async clearAll(): Promise<void> {
    method getStorageType (line 96) | async getStorageType(): Promise<TokenStorageType> {

FILE: workspace-server/src/auth/token-storage/keychain-token-storage.ts
  type Keytar (line 11) | interface Keytar {
  constant KEYCHAIN_TEST_PREFIX (line 24) | const KEYCHAIN_TEST_PREFIX = '__keychain_test__';
  class KeychainTokenStorage (line 26) | class KeychainTokenStorage extends BaseTokenStorage {
    method getKeytar (line 31) | async getKeytar(): Promise<Keytar | null> {
    method getCredentials (line 50) | async getCredentials(serverName: string): Promise<OAuthCredentials | n...
    method setCredentials (line 79) | async setCredentials(credentials: OAuthCredentials): Promise<void> {
    method deleteCredentials (line 101) | async deleteCredentials(serverName: string): Promise<void> {
    method listServers (line 122) | async listServers(): Promise<string[]> {
    method getAllCredentials (line 143) | async getAllCredentials(): Promise<Map<string, OAuthCredentials>> {
    method clearAll (line 178) | async clearAll(): Promise<void> {
    method checkKeychainAvailability (line 212) | async checkKeychainAvailability(): Promise<boolean> {
    method isAvailable (line 243) | async isAvailable(): Promise<boolean> {

FILE: workspace-server/src/auth/token-storage/oauth-credential-storage.ts
  constant KEYCHAIN_SERVICE_NAME (line 11) | const KEYCHAIN_SERVICE_NAME = 'gemini-cli-workspace-oauth';
  constant MAIN_ACCOUNT_KEY (line 12) | const MAIN_ACCOUNT_KEY = 'main-account';
  class OAuthCredentialStorage (line 14) | class OAuthCredentialStorage {
    method loadCredentials (line 22) | static async loadCredentials(): Promise<Credentials | null> {
    method saveCredentials (line 53) | static async saveCredentials(credentials: Credentials): Promise<void> {
    method clearCredentials (line 73) | static async clearCredentials(): Promise<void> {

FILE: workspace-server/src/auth/token-storage/types.ts
  type OAuthToken (line 10) | interface OAuthToken {
  type OAuthCredentials (line 21) | interface OAuthCredentials {
  type TokenStorageType (line 30) | enum TokenStorageType {
  type TokenStorage (line 35) | interface TokenStorage {

FILE: workspace-server/src/cli/headless-login.ts
  constant CLIENT_ID (line 32) | const CLIENT_ID = config.clientId;
  constant CLOUD_FUNCTION_URL (line 33) | const CLOUD_FUNCTION_URL = config.cloudFunctionUrl;
  type CredentialsJson (line 35) | interface CredentialsJson {
  constant TTY_PATH (line 43) | const TTY_PATH = os.platform() === 'win32' ? '\\\\.\\CON' : '/dev/tty';
  function openTtyRead (line 50) | function openTtyRead(): fs.ReadStream {
  function openTtyWrite (line 57) | function openTtyWrite(): fs.WriteStream {
  function readCredentialsFromTty (line 65) | function readCredentialsFromTty(): Promise<string> {
  function validateCredentials (line 129) | function validateCredentials(
  function generateOAuthUrl (line 147) | function generateOAuthUrl(): string {
  function main (line 167) | async function main() {

FILE: workspace-server/src/features/feature-config.ts
  constant SCOPE_PREFIX (line 20) | const SCOPE_PREFIX = 'https://www.googleapis.com/auth/';
  function scopes (line 22) | function scopes(...names: string[]): string[] {
  type ServiceName (line 26) | type ServiceName =
  type FeatureGroup (line 38) | interface FeatureGroup {
  function featureGroupKey (line 54) | function featureGroupKey(fg: FeatureGroup): string {
  constant FEATURE_GROUPS (line 58) | const FEATURE_GROUPS: readonly FeatureGroup[] = [
  function getAllPossibleScopes (line 270) | function getAllPossibleScopes(): string[] {

FILE: workspace-server/src/features/feature-resolver.ts
  type ResolvedFeatures (line 32) | interface ResolvedFeatures {
  type Override (line 39) | interface Override {
  function parseOverrides (line 50) | function parseOverrides(raw: string): Override[] {
  constant GROUP_INDEX (line 83) | const GROUP_INDEX: ReadonlyMap<string, FeatureGroup> = new Map(
  constant TOOL_INDEX (line 88) | const TOOL_INDEX: ReadonlyMap<string, string> = new Map(
  function resolveFeatures (line 101) | function resolveFeatures(

FILE: workspace-server/src/index.ts
  function main (line 104) | async function main() {

FILE: workspace-server/src/services/CalendarService.ts
  type EventAttachment (line 22) | interface EventAttachment {
  type CalendarEventType (line 28) | type CalendarEventType =
  type ListEventsEventType (line 34) | type ListEventsEventType = CalendarEventType | 'birthday' | 'fromGmail';
  type CreateEventInput (line 36) | interface CreateEventInput {
  type ListEventsInput (line 69) | interface ListEventsInput {
  type GetEventInput (line 77) | interface GetEventInput {
  type DeleteEventInput (line 82) | interface DeleteEventInput {
  type UpdateEventInput (line 87) | interface UpdateEventInput {
  type RespondToEventInput (line 99) | interface RespondToEventInput {
  type FindFreeTimeInput (line 107) | interface FindFreeTimeInput {
  class CalendarService (line 114) | class CalendarService {
    method constructor (line 117) | constructor(private authManager: any) {}
    method applyMeetAndAttachments (line 125) | private applyMeetAndAttachments(
    method createValidationErrorResponse (line 154) | private createValidationErrorResponse(error: unknown) {
    method extractErrorMessage (line 194) | private extractErrorMessage(error: unknown): string {
    method getCalendar (line 249) | private async getCalendar(): Promise<calendar_v3.Calendar> {
    method getPrimaryCalendarId (line 257) | private async getPrimaryCalendarId(): Promise<string> {

FILE: workspace-server/src/services/CalendarValidation.ts
  type EventDateInput (line 15) | type EventDateInput = {
  type WorkingLocationValidationInput (line 20) | type WorkingLocationValidationInput = {
  type CompleteEventValidationInput (line 26) | type CompleteEventValidationInput = {
  function createIssue (line 47) | function createIssue(path: (string | number)[], message: string): z.ZodE...
  function validateExclusiveDateField (line 57) | function validateExclusiveDateField(
  function validateOptionalExclusiveDateField (line 72) | function validateOptionalExclusiveDateField(
  function validateDateFieldFormats (line 91) | function validateDateFieldFormats(
  function validateWorkingLocationProperties (line 103) | function validateWorkingLocationProperties(
  function addDays (line 134) | function addDays(date: string, days: number): string {
  function validateWorkingLocationDuration (line 140) | function validateWorkingLocationDuration(
  function validateCompleteEventInput (line 163) | function validateCompleteEventInput(input: CompleteEventValidationInput)...
  function validateCreateEventInput (line 193) | function validateCreateEventInput(input: CreateEventInput): void {
  function validateUpdateEventInput (line 204) | function validateUpdateEventInput(input: UpdateEventInput): void {

FILE: workspace-server/src/services/ChatService.ts
  type GetMessagesParams (line 12) | interface GetMessagesParams {
  class ChatService (line 21) | class ChatService {
    method constructor (line 22) | constructor(private authManager: AuthManager) {}
    method getChatClient (line 24) | private async getChatClient(): Promise<chat_v1.Chat> {
    method getPeopleClient (line 30) | private async getPeopleClient(): Promise<people_v1.People> {
    method _setupDmSpace (line 36) | private async _setupDmSpace(email: string): Promise<chat_v1.Schema$Spa...

FILE: workspace-server/src/services/DocsService.ts
  constant TABS_FIELD_MASK (line 17) | const TABS_FIELD_MASK =
  type BaseDocsSuggestion (line 20) | interface BaseDocsSuggestion {
  type DocsInsertionSuggestion (line 26) | interface DocsInsertionSuggestion extends BaseDocsSuggestion {
  type DocsDeletionSuggestion (line 31) | interface DocsDeletionSuggestion extends BaseDocsSuggestion {
  type DocsStyleChangeSuggestion (line 36) | interface DocsStyleChangeSuggestion extends BaseDocsSuggestion {
  type DocsParagraphStyleChangeSuggestion (line 42) | interface DocsParagraphStyleChangeSuggestion extends BaseDocsSuggestion {
  type DocsSuggestion (line 48) | type DocsSuggestion =
  class DocsService (line 54) | class DocsService {
    method _flattenTabs (line 59) | private _flattenTabs(tabs: docs_v1.Schema$Tab[]): docs_v1.Schema$Tab[] {
    method constructor (line 66) | constructor(private authManager: AuthManager) {}
    method getDocsClient (line 68) | private async getDocsClient(): Promise<docs_v1.Docs> {
    method _extractSuggestions (line 125) | private _extractSuggestions(
    method _getParagraphText (line 204) | private _getParagraphText(
    method _readStructuralElement (line 670) | private _readStructuralElement(
    method _renderPersonChip (line 702) | private _renderPersonChip(props: docs_v1.Schema$PersonProperties): str...
    method _renderRichLinkChip (line 710) | private _renderRichLinkChip(
    method _renderDateChip (line 720) | private _renderDateChip(props: docs_v1.Schema$DateElementProperties): ...
    method _generateReplacementRequests (line 816) | private _generateReplacementRequests(
    method _getFullDocumentText (line 865) | private _getFullDocumentText(

FILE: workspace-server/src/services/DriveService.ts
  constant MIN_DRIVE_ID_LENGTH (line 17) | const MIN_DRIVE_ID_LENGTH = 25;
  constant URL_PATTERNS (line 19) | const URL_PATTERNS = [
  class DriveService (line 29) | class DriveService {
    method constructor (line 30) | constructor(private authManager: AuthManager) {}
    method getDriveClient (line 32) | private async getDriveClient(): Promise<drive_v3.Drive> {
    method handleError (line 38) | private handleError(

FILE: workspace-server/src/services/GmailService.ts
  type SendEmailParams (line 22) | type SendEmailParams = {
  type CreateDraftParams (line 31) | type CreateDraftParams = SendEmailParams & {
  type GmailAttachment (line 35) | interface GmailAttachment {
  class GmailService (line 42) | class GmailService {
    method constructor (line 43) | constructor(private authManager: AuthManager) {}
    method getGmailClient (line 45) | private async getGmailClient(): Promise<gmail_v1.Gmail> {
    method handleError (line 54) | private handleError(error: unknown, context: string) {
    method extractAttachmentsAndBody (line 710) | private extractAttachmentsAndBody(

FILE: workspace-server/src/services/PeopleService.ts
  class PeopleService (line 12) | class PeopleService {
    method constructor (line 13) | constructor(private authManager: AuthManager) {}
    method getPeopleClient (line 15) | private async getPeopleClient(): Promise<people_v1.People> {

FILE: workspace-server/src/services/SheetsService.ts
  class SheetsService (line 13) | class SheetsService {
    method constructor (line 14) | constructor(private authManager: AuthManager) {}
    method getSheetsClient (line 16) | private async getSheetsClient(): Promise<sheets_v4.Sheets> {

FILE: workspace-server/src/services/SlidesService.ts
  class SlidesService (line 16) | class SlidesService {
    method constructor (line 17) | constructor(private authManager: AuthManager) {}
    method getSlidesClient (line 19) | private async getSlidesClient(): Promise<slides_v1.Slides> {
    method extractTextFromTextContent (line 111) | private extractTextFromTextContent(
    method downloadToLocal (line 184) | private async downloadToLocal(url: string, localPath: string) {

FILE: workspace-server/src/services/TimeService.ts
  class TimeService (line 9) | class TimeService {
    method constructor (line 10) | constructor() {
    method handleErrors (line 14) | private async handleErrors<T>(
    method getTimeContext (line 42) | private getTimeContext() {

FILE: workspace-server/src/utils/DriveQueryBuilder.ts
  function escapeQueryString (line 16) | function escapeQueryString(str: string): string {

FILE: workspace-server/src/utils/IdUtils.ts
  constant DOC_ID_REGEX (line 9) | const DOC_ID_REGEX = /\/d\/([a-zA-Z0-9-_]+)/;
  function extractDocId (line 17) | function extractDocId(url: string): string | undefined {

FILE: workspace-server/src/utils/MimeHelper.ts
  class MimeHelper (line 10) | class MimeHelper {
    method createMimeMessage (line 14) | public static createMimeMessage({
    method createMimeMessageWithAttachments (line 98) | public static createMimeMessageWithAttachments({
    method decodeBase64Url (line 205) | public static decodeBase64Url(encoded: string): string {

FILE: workspace-server/src/utils/config.ts
  type WorkspaceConfig (line 9) | interface WorkspaceConfig {
  constant DEFAULT_CONFIG (line 14) | const DEFAULT_CONFIG: WorkspaceConfig = {
  function loadConfig (line 24) | function loadConfig(): WorkspaceConfig {

FILE: workspace-server/src/utils/constants.ts
  constant GMAIL_SEARCH_MAX_RESULTS (line 7) | const GMAIL_SEARCH_MAX_RESULTS = 100;
  constant GMAIL_BATCH_MODIFY_MAX_IDS (line 8) | const GMAIL_BATCH_MODIFY_MAX_IDS = 1000;
  constant GMAIL_NO_LABEL_CHANGES_MESSAGE (line 9) | const GMAIL_NO_LABEL_CHANGES_MESSAGE =

FILE: workspace-server/src/utils/logger.ts
  function ensureLogDirectoryExists (line 13) | async function ensureLogDirectoryExists() {
  function setLoggingEnabled (line 27) | function setLoggingEnabled(enabled: boolean) {
  function logToFile (line 31) | function logToFile(message: string) {

FILE: workspace-server/src/utils/paths.ts
  function findProjectRoot (line 10) | function findProjectRoot(): string {
  constant PROJECT_ROOT (line 24) | const PROJECT_ROOT = findProjectRoot();
  constant ENCRYPTED_TOKEN_PATH (line 25) | const ENCRYPTED_TOKEN_PATH = path.join(
  constant ENCRYPTION_MASTER_KEY_PATH (line 29) | const ENCRYPTION_MASTER_KEY_PATH = path.join(

FILE: workspace-server/src/utils/secure-browser-launcher.ts
  function withTimeout (line 11) | function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
  function validateUrl (line 28) | function validateUrl(url: string): void {
  function openBrowserSecurely (line 61) | async function openBrowserSecurely(
  function shouldLaunchBrowser (line 195) | function shouldLaunchBrowser(): boolean {

FILE: workspace-server/src/utils/tool-normalization.ts
  function applyToolNameNormalization (line 18) | function applyToolNameNormalization(

FILE: workspace-server/src/utils/validation.ts
  function createValidator (line 110) | function createValidator<T>(
  function extractDocumentId (line 154) | function extractDocumentId(urlOrId: string): string {
  class ValidationError (line 172) | class ValidationError extends Error {
    method constructor (line 173) | constructor(
Condensed preview — 124 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (833K chars).
[
  {
    "path": ".gemini/skills/code-reviewer/SKILL.md",
    "chars": 4187,
    "preview": "---\nname: code-reviewer\ndescription:\n  Use this skill to review code. It supports both local changes (staged or\n  workin"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 1024,
    "preview": "version: 2\nupdates:\n  - package-ecosystem: 'npm'\n    directory: '/'\n    schedule:\n      interval: 'weekly'\n    open-pull"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 2454,
    "preview": "name: CI\n\non:\n  push:\n    branches: [main, develop]\n  pull_request:\n    branches: [main]\n\njobs:\n  verify:\n    runs-on: u"
  },
  {
    "path": ".github/workflows/deploy-docs.yml",
    "chars": 1218,
    "preview": "name: Deploy Docs to GitHub Pages\n\non:\n  push:\n    branches:\n      - main\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n   "
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 1068,
    "preview": "name: Release\n\non:\n  push:\n    tags:\n      - 'v*'\n\njobs:\n  release:\n    strategy:\n      matrix:\n        include:\n       "
  },
  {
    "path": ".github/workflows/weekly-preview.yml",
    "chars": 1921,
    "preview": "name: Weekly Preview Release\n\non:\n  schedule:\n    # Every Monday at 09:00 UTC\n    - cron: '0 9 * * 1'\n  workflow_dispatc"
  },
  {
    "path": ".gitignore",
    "chars": 400,
    "preview": "# macOS\n.DS_Store\n\n# logs\nlogs\n\n# Dependencies\nnode_modules/\n\n# Build outputs\ndist/\n\n# Environment files\n.env\n.env.local"
  },
  {
    "path": ".prettierignore",
    "chars": 78,
    "preview": "node_modules\ndist\ncoverage\n.vitepress/cache\n.vitepress/dist\npackage-lock.json\n"
  },
  {
    "path": ".prettierrc.json",
    "chars": 224,
    "preview": "{\n  \"semi\": true,\n  \"trailingComma\": \"all\",\n  \"singleQuote\": true,\n  \"printWidth\": 80,\n  \"tabWidth\": 2,\n  \"overrides\": ["
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 4021,
    "preview": "# How to Contribute\n\nWe would love to accept your patches and contributions to this project.\n\n## Before you begin\n\n### S"
  },
  {
    "path": "GEMINI.md",
    "chars": 1196,
    "preview": "This is a Gemini extension that provides tools for interacting with Google\nWorkspace services like Google Docs.\n\n### Bui"
  },
  {
    "path": "LICENSE",
    "chars": 11357,
    "preview": "\n                                 Apache License\n                           Version 2.0, January 2004\n                  "
  },
  {
    "path": "README.md",
    "chars": 3880,
    "preview": "# Google Workspace Extension for Gemini CLI\n\n[![Build Status](https://github.com/gemini-cli-extensions/workspace/actions"
  },
  {
    "path": "SECURITY.md",
    "chars": 328,
    "preview": "# Reporting Security Issues\n\nTo report a security issue, please use [https://g.co/vulnz](https://g.co/vulnz).\nWe use g.c"
  },
  {
    "path": "cloud_function/index.js",
    "chars": 13827,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n// Import required packages\ncon"
  },
  {
    "path": "cloud_function/package.json",
    "chars": 219,
    "preview": "{\n  \"name\": \"oauth-handler\",\n  \"version\": \"1.0.0\",\n  \"main\": \"index.js\",\n  \"dependencies\": {\n    \"@google-cloud/function"
  },
  {
    "path": "commands/calendar/clear-schedule.toml",
    "chars": 1689,
    "preview": "description = \"Clear all events for a specific date or range by deleting or declining them\"\nprompt = \"\"\"\nPlease help me "
  },
  {
    "path": "commands/calendar/get-schedule.toml",
    "chars": 901,
    "preview": "description = \"Show your schedule for today, or the date specified\"\nprompt = \"\"\"\nPlease show me my schedule for today. T"
  },
  {
    "path": "commands/drive/search.toml",
    "chars": 619,
    "preview": "description = \"Searches Google Drive for files matching a query.\"\nprompt = \"\"\"\nYou are tasked with searching Google Driv"
  },
  {
    "path": "commands/gmail/search.toml",
    "chars": 826,
    "preview": "description = \"Searches for emails in Gmail matching a query.\"\nprompt = \"\"\"\nYou are tasked with searching Gmail for emai"
  },
  {
    "path": "docs/.vitepress/config.mts",
    "chars": 1213,
    "preview": "import { defineConfig } from 'vitepress';\n\n// https://vitepress.dev/reference/site-config\nexport default defineConfig({\n"
  },
  {
    "path": "docs/GCP-RECREATION.md",
    "chars": 2485,
    "preview": "# Recreating the GCP Project\n\nThis guide provides step-by-step instructions to recreate the Google Cloud\nPlatform (GCP) "
  },
  {
    "path": "docs/development.md",
    "chars": 8492,
    "preview": "# Development\n\nThis document provides instructions for developing the Google Workspace\nextension.\n\n## Development Setup "
  },
  {
    "path": "docs/feature-configuration.md",
    "chars": 7133,
    "preview": "# Feature Configuration\n\nThe extension provides a feature configuration system that lets you control\nwhich services and "
  },
  {
    "path": "docs/index.md",
    "chars": 5118,
    "preview": "# Google Workspace Extension Documentation\n\nThis document provides an overview of the Google Workspace extension for Gem"
  },
  {
    "path": "docs/release.md",
    "chars": 1812,
    "preview": "# Release Process\n\nThis project uses GitHub Actions to automate the release process.\n\n## Prerequisites\n\n- [GitHub CLI](h"
  },
  {
    "path": "docs/release_notes.md",
    "chars": 6457,
    "preview": "# Release Notes\n\n## 0.0.8 (2026-05-01)\n\n### New Features\n\n- **Google Calendar**: Added support for `eventType` (Out of O"
  },
  {
    "path": "eslint.config.js",
    "chars": 2903,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nconst tseslint = require('@type"
  },
  {
    "path": "gemini-extension.json",
    "chars": 271,
    "preview": "{\n  \"name\": \"google-workspace\",\n  \"version\": \"0.0.8\",\n  \"contextFileName\": \"workspace-server${/}WORKSPACE-Context.md\",\n "
  },
  {
    "path": "jest.config.js",
    "chars": 1480,
    "preview": "/** @type {import('jest').Config} */\nmodule.exports = {\n  preset: 'ts-jest',\n  testEnvironment: 'node',\n  projects: [\n  "
  },
  {
    "path": "package.json",
    "chars": 2749,
    "preview": "{\n  \"name\": \"gemini-workspace-extension\",\n  \"version\": \"0.0.8\",\n  \"description\": \"Google Workspace Server Extension\",\n  "
  },
  {
    "path": "scripts/auth-utils.js",
    "chars": 3632,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nconst {\n  OAuthCredentialStorag"
  },
  {
    "path": "scripts/clean.js",
    "chars": 829,
    "preview": "/**\n * @license\n * Copyright 2026 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nconst { rmSync, readFileSync } "
  },
  {
    "path": "scripts/list-deps.js",
    "chars": 687,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nconst path = require('node:path"
  },
  {
    "path": "scripts/print-scopes.ts",
    "chars": 489,
    "preview": "/**\n * @license\n * Copyright 2026 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Prints the OAuth scopes "
  },
  {
    "path": "scripts/release.js",
    "chars": 4685,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nconst fs = require('node:fs');\n"
  },
  {
    "path": "scripts/set-version.js",
    "chars": 1679,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nconst fs = require('node:fs');\n"
  },
  {
    "path": "scripts/setup-gcp.sh",
    "chars": 8665,
    "preview": "#!/bin/bash\n\n# GCP Setup Script for Google Workspace Extension\n# This script is idempotent — it can be safely re-run wit"
  },
  {
    "path": "scripts/start.js",
    "chars": 1484,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nconst { spawn } = require('node"
  },
  {
    "path": "scripts/tsconfig.json",
    "chars": 142,
    "preview": "{\n  \"extends\": \"../tsconfig.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"..\"\n  },\n  \"include\": [\"**/*.ts\", \"../workspac"
  },
  {
    "path": "scripts/utils/dependencies.js",
    "chars": 1501,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nconst fs = require('node:fs');\n"
  },
  {
    "path": "skills/gmail/SKILL.md",
    "chars": 8204,
    "preview": "---\nname: gmail\ndescription: >\n  CRITICAL: You MUST activate this skill BEFORE composing, sending, drafting, or\n  search"
  },
  {
    "path": "skills/google-calendar/SKILL.md",
    "chars": 14257,
    "preview": "---\nname: google-calendar\ndescription: >\n  CRITICAL: You MUST activate this skill BEFORE creating, querying, or managing"
  },
  {
    "path": "skills/google-chat/SKILL.md",
    "chars": 7355,
    "preview": "---\nname: google-chat\ndescription: >\n  CRITICAL: You MUST activate this skill BEFORE sending, reading, or managing\n  Goo"
  },
  {
    "path": "skills/google-docs/SKILL.md",
    "chars": 8584,
    "preview": "---\nname: google-docs\ndescription: >\n  CRITICAL: You MUST activate this skill BEFORE creating, editing, or managing\n  Go"
  },
  {
    "path": "skills/google-sheets/SKILL.md",
    "chars": 1635,
    "preview": "---\nname: google-sheets\ndescription: >\n  Activate this skill when the user wants to find, read, or analyze Google\n  Shee"
  },
  {
    "path": "skills/google-slides/SKILL.md",
    "chars": 1752,
    "preview": "---\nname: google-slides\ndescription: >\n  Activate this skill when the user wants to find, read, or extract content from\n"
  },
  {
    "path": "tsconfig.json",
    "chars": 511,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"es2020\",\n    \"module\": \"commonjs\",\n    \"esModuleInterop\": true,\n    \"allowSynthe"
  },
  {
    "path": "workspace-server/.github/workflows/ci.yml",
    "chars": 2349,
    "preview": "name: CI\n\non:\n  push:\n    branches: [main, develop]\n  pull_request:\n    branches: [main]\n\njobs:\n  test:\n    runs-on: ubu"
  },
  {
    "path": "workspace-server/.github/workflows/release.yml",
    "chars": 1342,
    "preview": "name: Release\n\non:\n  push:\n    tags:\n      - 'v*'\n\njobs:\n  release:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses:"
  },
  {
    "path": "workspace-server/WORKSPACE-Context.md",
    "chars": 5733,
    "preview": "# Google Workspace Extension - Behavioral Guide\n\nThis guide provides behavioral instructions for effectively using the G"
  },
  {
    "path": "workspace-server/esbuild.auth-utils.js",
    "chars": 787,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nconst esbuild = require('esbuil"
  },
  {
    "path": "workspace-server/esbuild.config.js",
    "chars": 987,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nconst esbuild = require('esbuil"
  },
  {
    "path": "workspace-server/esbuild.headless-login.js",
    "chars": 747,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nconst esbuild = require('esbuil"
  },
  {
    "path": "workspace-server/jest.config.js",
    "chars": 351,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/** @type {import('jest').Confi"
  },
  {
    "path": "workspace-server/package.json",
    "chars": 873,
    "preview": "{\n  \"name\": \"workspace-server\",\n  \"version\": \"0.0.8\",\n  \"description\": \"\",\n  \"main\": \"dist/index.js\",\n  \"scripts\": {\n   "
  },
  {
    "path": "workspace-server/src/__tests__/auth/AuthManager.test.ts",
    "chars": 8624,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { AuthManager } from '.."
  },
  {
    "path": "workspace-server/src/__tests__/auth/token-storage/base-token-storage.test.ts",
    "chars": 4449,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { describe, it, expect, "
  },
  {
    "path": "workspace-server/src/__tests__/auth/token-storage/file-token-storage.test.ts",
    "chars": 12551,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport {\n  describe,\n  it,\n  ex"
  },
  {
    "path": "workspace-server/src/__tests__/auth/token-storage/hybrid-token-storage.test.ts",
    "chars": 8539,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport {\n  describe,\n  it,\n  ex"
  },
  {
    "path": "workspace-server/src/__tests__/auth/token-storage/keychain-token-storage.test.ts",
    "chars": 12275,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport {\n  describe,\n  it,\n  ex"
  },
  {
    "path": "workspace-server/src/__tests__/auth/token-storage/oauth-credential-storage.test.ts",
    "chars": 3595,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { describe, it, expect, "
  },
  {
    "path": "workspace-server/src/__tests__/features/feature-config.test.ts",
    "chars": 4819,
    "preview": "/**\n * @license\n * Copyright 2026 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { execSync } from 'node:"
  },
  {
    "path": "workspace-server/src/__tests__/features/feature-resolver.test.ts",
    "chars": 8572,
    "preview": "/**\n * @license\n * Copyright 2026 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { describe, it, expect, "
  },
  {
    "path": "workspace-server/src/__tests__/mocks/wasm.js",
    "chars": 110,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nmodule.exports = {};\n"
  },
  {
    "path": "workspace-server/src/__tests__/services/CalendarService.test.ts",
    "chars": 72035,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport {\n  describe,\n  it,\n  ex"
  },
  {
    "path": "workspace-server/src/__tests__/services/CalendarValidation.test.ts",
    "chars": 6202,
    "preview": "/**\n * @license\n * Copyright 2026 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { describe, it, expect }"
  },
  {
    "path": "workspace-server/src/__tests__/services/ChatService.test.ts",
    "chars": 20224,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport {\n  describe,\n  it,\n  ex"
  },
  {
    "path": "workspace-server/src/__tests__/services/DocsService.comments.test.ts",
    "chars": 11874,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport {\n  describe,\n  it,\n  ex"
  },
  {
    "path": "workspace-server/src/__tests__/services/DocsService.test.ts",
    "chars": 32539,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport {\n  describe,\n  it,\n  ex"
  },
  {
    "path": "workspace-server/src/__tests__/services/DriveService.test.ts",
    "chars": 42316,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport {\n  describe,\n  it,\n  ex"
  },
  {
    "path": "workspace-server/src/__tests__/services/GmailService.test.ts",
    "chars": 32909,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport {\n  describe,\n  it,\n  ex"
  },
  {
    "path": "workspace-server/src/__tests__/services/PeopleService.test.ts",
    "chars": 8819,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport {\n  describe,\n  it,\n  ex"
  },
  {
    "path": "workspace-server/src/__tests__/services/SheetsService.test.ts",
    "chars": 10417,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport {\n  describe,\n  it,\n  ex"
  },
  {
    "path": "workspace-server/src/__tests__/services/SlidesService.test.ts",
    "chars": 12153,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport {\n  describe,\n  it,\n  ex"
  },
  {
    "path": "workspace-server/src/__tests__/services/TimeService.test.ts",
    "chars": 1805,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { TimeService } from '.."
  },
  {
    "path": "workspace-server/src/__tests__/setup.ts",
    "chars": 748,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n// Test setup file for Jest\n// "
  },
  {
    "path": "workspace-server/src/__tests__/tool-normalization.test.ts",
    "chars": 1345,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { McpServer } from '@mod"
  },
  {
    "path": "workspace-server/src/__tests__/utils/DriveQueryBuilder.test.ts",
    "chars": 992,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { describe, it, expect }"
  },
  {
    "path": "workspace-server/src/__tests__/utils/IdUtils.test.ts",
    "chars": 4461,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { describe, it, expect }"
  },
  {
    "path": "workspace-server/src/__tests__/utils/MimeHelper.test.ts",
    "chars": 13486,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { describe, it, expect }"
  },
  {
    "path": "workspace-server/src/__tests__/utils/config.test.ts",
    "chars": 1820,
    "preview": "/**\n * @license\n * Copyright 2026 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport {\n  describe,\n  it,\n  ex"
  },
  {
    "path": "workspace-server/src/__tests__/utils/logger.test.ts",
    "chars": 7118,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport {\n  describe,\n  it,\n  ex"
  },
  {
    "path": "workspace-server/src/__tests__/utils/paths.test.ts",
    "chars": 1020,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport path from 'node:path';\ni"
  },
  {
    "path": "workspace-server/src/__tests__/utils/secure-browser-launcher.test.ts",
    "chars": 9271,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport {\n  describe,\n  it,\n  ex"
  },
  {
    "path": "workspace-server/src/__tests__/utils/validation.test.ts",
    "chars": 6384,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { describe, it, expect }"
  },
  {
    "path": "workspace-server/src/auth/AuthManager.ts",
    "chars": 14177,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { google, Auth } from 'g"
  },
  {
    "path": "workspace-server/src/auth/scopes.ts",
    "chars": 532,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { resolveFeatures } from"
  },
  {
    "path": "workspace-server/src/auth/token-storage/base-token-storage.ts",
    "chars": 1325,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { OAuthCredentials, Toke"
  },
  {
    "path": "workspace-server/src/auth/token-storage/file-token-storage.ts",
    "chars": 6139,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { promises as fs } from "
  },
  {
    "path": "workspace-server/src/auth/token-storage/hybrid-token-storage.ts",
    "chars": 3057,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { BaseTokenStorage } fro"
  },
  {
    "path": "workspace-server/src/auth/token-storage/index.ts",
    "chars": 230,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nexport * from './types';\nexport"
  },
  {
    "path": "workspace-server/src/auth/token-storage/keychain-token-storage.ts",
    "chars": 7041,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport * as crypto from 'node:c"
  },
  {
    "path": "workspace-server/src/auth/token-storage/oauth-credential-storage.ts",
    "chars": 2234,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { type Credentials } fro"
  },
  {
    "path": "workspace-server/src/auth/token-storage/types.ts",
    "chars": 942,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Interface for OAuth toke"
  },
  {
    "path": "workspace-server/src/cli/headless-login.ts",
    "chars": 6531,
    "preview": "#!/usr/bin/env node\n\n/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Hea"
  },
  {
    "path": "workspace-server/src/features/feature-config.ts",
    "chars": 6273,
    "preview": "/**\n * @license\n * Copyright 2026 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Feature Configuration Re"
  },
  {
    "path": "workspace-server/src/features/feature-resolver.ts",
    "chars": 5635,
    "preview": "/**\n * @license\n * Copyright 2026 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Feature Resolver\n *\n * R"
  },
  {
    "path": "workspace-server/src/features/index.ts",
    "chars": 370,
    "preview": "/**\n * @license\n * Copyright 2026 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nexport {\n  FEATURE_GROUPS,\n  fe"
  },
  {
    "path": "workspace-server/src/index.ts",
    "chars": 44085,
    "preview": "#!/usr/bin/env node\n\n/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { M"
  },
  {
    "path": "workspace-server/src/services/CalendarService.ts",
    "chars": 29696,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport crypto from 'node:crypto"
  },
  {
    "path": "workspace-server/src/services/CalendarValidation.ts",
    "chars": 5751,
    "preview": "/**\n * @license\n * Copyright 2026 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { z } from 'zod';\nimport"
  },
  {
    "path": "workspace-server/src/services/ChatService.ts",
    "chars": 15411,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { google, chat_v1, peopl"
  },
  {
    "path": "workspace-server/src/services/DocsService.ts",
    "chars": 24493,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { google, docs_v1 } from"
  },
  {
    "path": "workspace-server/src/services/DriveService.ts",
    "chars": 18257,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { google, drive_v3 } fro"
  },
  {
    "path": "workspace-server/src/services/GmailService.ts",
    "chars": 19675,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { google, gmail_v1 } fro"
  },
  {
    "path": "workspace-server/src/services/PeopleService.ts",
    "chars": 5738,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { google, people_v1 } fr"
  },
  {
    "path": "workspace-server/src/services/SheetsService.ts",
    "chars": 7546,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { google, sheets_v4 } fr"
  },
  {
    "path": "workspace-server/src/services/SlidesService.ts",
    "chars": 10682,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { google, slides_v1 } fr"
  },
  {
    "path": "workspace-server/src/services/TimeService.ts",
    "chars": 1972,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { logToFile } from '../u"
  },
  {
    "path": "workspace-server/src/utils/DriveQueryBuilder.ts",
    "chars": 413,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Utility for escaping Goo"
  },
  {
    "path": "workspace-server/src/utils/GaxiosConfig.ts",
    "chars": 884,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { GaxiosOptions } from '"
  },
  {
    "path": "workspace-server/src/utils/IdUtils.ts",
    "chars": 907,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { logToFile } from './lo"
  },
  {
    "path": "workspace-server/src/utils/MimeHelper.ts",
    "chars": 5194,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Helper class for creatin"
  },
  {
    "path": "workspace-server/src/utils/config.ts",
    "chars": 1103,
    "preview": "/**\n * @license\n * Copyright 2026 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { logToFile } from './lo"
  },
  {
    "path": "workspace-server/src/utils/constants.ts",
    "chars": 292,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nexport const GMAIL_SEARCH_MAX_R"
  },
  {
    "path": "workspace-server/src/utils/logger.ts",
    "chars": 1119,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport * as fs from 'node:fs/pr"
  },
  {
    "path": "workspace-server/src/utils/open-wrapper.ts",
    "chars": 1470,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * This module acts as a dr"
  },
  {
    "path": "workspace-server/src/utils/paths.ts",
    "chars": 811,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport path from 'node:path';\ni"
  },
  {
    "path": "workspace-server/src/utils/secure-browser-launcher.ts",
    "chars": 6935,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { execFile, ExecFileOpti"
  },
  {
    "path": "workspace-server/src/utils/tool-normalization.ts",
    "chars": 1306,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { McpServer } from '@mod"
  },
  {
    "path": "workspace-server/src/utils/validation.ts",
    "chars": 4599,
    "preview": "/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { z } from 'zod';\n\n/**\n "
  },
  {
    "path": "workspace-server/tsconfig.json",
    "chars": 217,
    "preview": "{\n  \"extends\": \"../tsconfig.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  },\n  \"include"
  },
  {
    "path": "workspace-server/tsconfig.test.json",
    "chars": 166,
    "preview": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"strict\": false,\n    \"noImplicitAny\": false\n  },\n  \"include"
  }
]

About this extraction

This page contains the full source code of the gemini-cli-extensions/workspace GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 124 files (776.8 KB), approximately 185.3k tokens, and a symbol index with 245 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!