# llms-full (private-aware)
> Built from GitHub files and website pages. Large files may be truncated.

--- docs/cli/tutorials/skills-getting-started.md ---
# Getting Started with Agent Skills

Agent Skills allow you to extend Gemini CLI with specialized expertise. This
tutorial will guide you through creating your first skill, enabling it, and
using it in a session.

## 1. Enable Agent Skills

Agent Skills are currently an experimental feature and must be enabled in your
settings.

### Via the interactive UI

1.  Start a Gemini CLI session by running `gemini`.
2.  Type `/settings` to open the interactive settings dialog.
3.  Search for "Skills".
4.  Toggle **Agent Skills** to `true`.
5.  Press `Esc` to save and exit. You may need to restart the CLI for the
    changes to take effect.

### Via `settings.json`

Alternatively, you can manually edit your global settings file at
`~/.gemini/settings.json` (create it if it doesn't exist):

```json
{
  "experimental": {
    "skills": true
  }
}
```

## 2. Create Your First Skill

A skill is a directory containing a `SKILL.md` file. Let's create an **API
Auditor** skill that helps you verify if local or remote endpoints are
responding correctly.

1.  **Create the skill directory structure:**

    ```bash
    mkdir -p .gemini/skills/api-auditor/scripts
    ```

2.  **Create the `SKILL.md` file:** Create a file at
    `.gemini/skills/api-auditor/SKILL.md` with the following content:

    ```markdown
    ---
    name: api-auditor
    description:
      Expertise in auditing and testing API endpoints. Use when the user asks to
      "check", "test", or "audit" a URL or API.
    ---

    # API Auditor Instructions

    You act as a QA engineer specialized in API reliability. When this skill is
    active, you MUST:

    1.  **Audit**: Use the bundled `scripts/audit.js` utility to check the
        status of the provided URL.
    2.  **Report**: Analyze the output (status codes, latency) and explain any
        failures in plain English.
    3.  **Secure**: Remind the user if they are testing a sensitive endpoint
        without an `https://` protocol.
    ```

3.  **Create the bundled Node.js script:** Create a file at
    `.gemini/skills/api-auditor/scripts/audit.js`. This script will be used by
    the agent to perform the actual check:

    ```javascript
    // .gemini/skills/api-auditor/scripts/audit.js
    const url = process.argv[2];

    if (!url) {
      console.error('Usage: node audit.js <url>');
      process.exit(1);
    }

    console.log(`Auditing ${url}...`);
    fetch(url, { method: 'HEAD' })
      .then((r) => console.log(`Result: Success (Status ${r.status})`))
      .catch((e) => console.error(`Result: Failed (${e.message})`));
    ```

## 3. Verify the Skill is Discovered

Use the `/skills` slash command (or `gemini skills list` from your terminal) to
see if Gemini CLI has found your new skill.

In a Gemini CLI session:

```
/skills list
```

You should see `api-auditor` in the list of available skills.

## 4. Use the Skill in a Chat

Now, let's see the skill in action. Start a new session and ask a question about
an endpoint.

**User:** "Can you audit http://geminili.com"

Gemini will recognize the request matches the `api-auditor` description and will
ask for your permission to activate it.

**Model:** (After calling `activate_skill`) "I've activated the **api-auditor**
skill. I'll run the audit script now..."

Gemini will then use the `run_shell_command` tool to execute your bundled Node
script:

`node .gemini/skills/api-auditor/scripts/audit.js http://geminili.com`

## Next Steps

- Explore [Agent Skills Authoring Guide](../skills.md#creating-a-skill) to learn
  about more advanced skill features.
- Learn how to share skills via [Extensions](../../extensions/index.md).


## Links discovered
- [Agent Skills Authoring Guide](https://github.com/google-gemini/gemini-cli/blob/main/docs/cli/skills.md#creating-a-skill)
- [Extensions](https://github.com/google-gemini/gemini-cli/blob/main/docs/extensions/index.md)

--- integration-tests/extensions-install.test.ts ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { describe, expect, it, beforeEach, afterEach } from 'vitest';
import { TestRig } from './test-helper.js';
import { writeFileSync } from 'node:fs';
import { join } from 'node:path';

const extension = `{
  "name": "test-extension-install",
  "version": "0.0.1"
}`;

const extensionUpdate = `{
  "name": "test-extension-install",
  "version": "0.0.2"
}`;

describe('extension install', () => {
  let rig: TestRig;

  beforeEach(() => {
    rig = new TestRig();
  });

  afterEach(async () => await rig.cleanup());

  it('installs a local extension, verifies a command, and updates it', async () => {
    rig.setup('extension install test');
    const testServerPath = join(rig.testDir!, 'gemini-extension.json');
    writeFileSync(testServerPath, extension);
    try {
      const result = await rig.runCommand(
        ['extensions', 'install', `${rig.testDir!}`],
        { stdin: 'y\n' },
      );
      expect(result).toContain('test-extension-install');

      const listResult = await rig.runCommand(['extensions', 'list']);
      expect(listResult).toContain('test-extension-install');
      writeFileSync(testServerPath, extensionUpdate);
      const updateResult = await rig.runCommand([
        'extensions',
        'update',
        `test-extension-install`,
      ]);
      expect(updateResult).toContain('0.0.2');
    } finally {
      await rig.runCommand([
        'extensions',
        'uninstall',
        'test-extension-install',
      ]);
    }
  });
});


--- docs/extensions/getting-started-extensions.md ---
# Getting started with Gemini CLI extensions

This guide will walk you through creating your first Gemini CLI extension.
You'll learn how to set up a new extension, add a custom tool via an MCP server,
create a custom command, and provide context to the model with a `GEMINI.md`
file.

## Prerequisites

Before you start, make sure you have the Gemini CLI installed and a basic
understanding of Node.js and TypeScript.

## Step 1: Create a new extension

The easiest way to start is by using one of the built-in templates. We'll use
the `mcp-server` example as our foundation.

Run the following command to create a new directory called `my-first-extension`
with the template files:

```bash
gemini extensions new my-first-extension mcp-server
```

This will create a new directory with the following structure:

```
my-first-extension/
├── example.ts
├── gemini-extension.json
├── package.json
└── tsconfig.json
```

## Step 2: Understand the extension files

Let's look at the key files in your new extension.

### `gemini-extension.json`

This is the manifest file for your extension. It tells Gemini CLI how to load
and use your extension.

```json
{
  "name": "my-first-extension",
  "version": "1.0.0",
  "mcpServers": {
    "nodeServer": {
      "command": "node",
      "args": ["${extensionPath}${/}dist${/}example.js"],
      "cwd": "${extensionPath}"
    }
  }
}
```

- `name`: The unique name for your extension.
- `version`: The version of your extension.
- `mcpServers`: This section defines one or more Model Context Protocol (MCP)
  servers. MCP servers are how you can add new tools for the model to use.
  - `command`, `args`, `cwd`: These fields specify how to start your server.
    Notice the use of the `${extensionPath}` variable, which Gemini CLI replaces
    with the absolute path to your extension's installation directory. This
    allows your extension to work regardless of where it's installed.

### `example.ts`

This file contains the source code for your MCP server. It's a simple Node.js
server that uses the `@modelcontextprotocol/sdk`.

```typescript
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';

const server = new McpServer({
  name: 'prompt-server',
  version: '1.0.0',
});

// Registers a new tool named 'fetch_posts'
server.registerTool(
  'fetch_posts',
  {
    description: 'Fetches a list of posts from a public API.',
    inputSchema: z.object({}).shape,
  },
  async () => {
    const apiResponse = await fetch(
      'https://jsonplaceholder.typicode.com/posts',
    );
    const posts = await apiResponse.json();
    const response = { posts: posts.slice(0, 5) };
    return {
      content: [
        {
          type: 'text',
          text: JSON.stringify(response),
        },
      ],
    };
  },
);

// ... (prompt registration omitted for brevity)

const transport = new StdioServerTransport();
await server.connect(transport);
```

This server defines a single tool called `fetch_posts` that fetches data from a
public API.

### `package.json` and `tsconfig.json`

These are standard configuration files for a TypeScript project. The
`package.json` file defines dependencies and a `build` script, and
`tsconfig.json` configures the TypeScript compiler.

## Step 3: Build and link your extension

Before you can use the extension, you need to compile the TypeScript code and
link the extension to your Gemini CLI installation for local development.

1.  **Install dependencies:**

    ```bash
    cd my-first-extension
    npm install
    ```

2.  **Build the server:**

    ```bash
    npm run build
    ```

    This will compile `example.ts` into `dist/example.js`, which is the file
    referenced in your `gemini-extension.json`.

3.  **Link the extension:**

    The `link` command creates a symbolic link from the Gemini CLI extensions
    directory to your development directory. This means any changes you make
    will be reflected immediately without needing to reinstall.

    ```bash
    gemini extensions link .
    ```

Now, restart your Gemini CLI session. The new `fetch_posts` tool will be
available. You can test it by asking: "fetch posts".

## Step 4: Add a custom command

Custom commands provide a way to create shortcuts for complex prompts. Let's add
a command that searches for a pattern in your code.

1.  Create a `commands` directory and a subdirectory for your command group:

    ```bash
    mkdir -p commands/fs
    ```

2.  Create a file named `commands/fs/grep-code.toml`:

    ```toml
    prompt = """
    Please summarize the findings for the pattern `{{args}}`.

    Search Results:
    !{grep -r {{args}} .}
    """
    ```

    This command, `/fs:grep-code`, will take an argument, run the `grep` shell
    command with it, and pipe the results into a prompt for summarization.

After saving the file, restart the Gemini CLI. You can now run
`/fs:grep-code "some pattern"` to use your new command.

## Step 5: Add a custom `GEMINI.md`

You can provide persistent context to the model by adding a `GEMINI.md` file to
your extension. This is useful for giving the model instructions on how to
behave or information about your extension's tools. Note that you may not always
need this for extensions built to expose commands and prompts.

1.  Create a file named `GEMINI.md` in the root of your extension directory:

    ```markdown
    # My First Extension Instructions

    You are an expert developer assistant. When the user asks you to fetch
    posts, use the `fetch_posts` tool. Be concise in your responses.
    ```

2.  Update your `gemini-extension.json` to tell the CLI to load this file:

    ```json
    {
      "name": "my-first-extension",
      "version": "1.0.0",
      "contextFileName": "GEMINI.md",
      "mcpServers": {
        "nodeServer": {
          "command": "node",
          "args": ["${extensionPath}${/}dist${/}example.js"],
          "cwd": "${extensionPath}"
        }
      }
    }
    ```

Restart the CLI again. The model will now have the context from your `GEMINI.md`
file in every session where the extension is active.

## Step 6: Releasing your extension

Once you are happy with your extension, you can share it with others. The two
primary ways of releasing extensions are via a Git repository or through GitHub
Releases. Using a public Git repository is the simplest method.

For detailed instructions on both methods, please refer to the
[Extension Releasing Guide](./extension-releasing.md).

## Conclusion

You've successfully created a Gemini CLI extension! You learned how to:

- Bootstrap a new extension from a template.
- Add custom tools with an MCP server.
- Create convenient custom commands.
- Provide persistent context to the model.
- Link your extension for local development.

From here, you can explore more advanced features and build powerful new
capabilities into the Gemini CLI.


## Links discovered
- [Extension Releasing Guide](https://github.com/google-gemini/gemini-cli/blob/main/docs/extensions/extension-releasing.md)

--- docs/get-started/installation.md ---
# Gemini CLI installation, execution, and deployment

Install and run Gemini CLI. This document provides an overview of Gemini CLI's
installation methods and deployment architecture.

## How to install and/or run Gemini CLI

There are several ways to run Gemini CLI. The recommended option depends on how
you intend to use Gemini CLI.

- As a standard installation. This is the most straightforward method of using
  Gemini CLI.
- In a sandbox. This method offers increased security and isolation.
- From the source. This is recommended for contributors to the project.

### 1. Standard installation (recommended for standard users)

This is the recommended way for end-users to install Gemini CLI. It involves
downloading the Gemini CLI package from the NPM registry.

- **Global install:**

  ```bash
  npm install -g @google/gemini-cli
  ```

  Then, run the CLI from anywhere:

  ```bash
  gemini
  ```

- **NPX execution:**

  ```bash
  # Execute the latest version from NPM without a global install
  npx @google/gemini-cli
  ```

### 2. Run in a sandbox (Docker/Podman)

For security and isolation, Gemini CLI can be run inside a container. This is
the default way that the CLI executes tools that might have side effects.

- **Directly from the registry:** You can run the published sandbox image
  directly. This is useful for environments where you only have Docker and want
  to run the CLI.
  ```bash
  # Run the published sandbox image
  docker run --rm -it us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.1.1
  ```
- **Using the `--sandbox` flag:** If you have Gemini CLI installed locally
  (using the standard installation described above), you can instruct it to run
  inside the sandbox container.
  ```bash
  gemini --sandbox -y -p "your prompt here"
  ```

### 3. Run from source (recommended for Gemini CLI contributors)

Contributors to the project will want to run the CLI directly from the source
code.

- **Development mode:** This method provides hot-reloading and is useful for
  active development.
  ```bash
  # From the root of the repository
  npm run start
  ```
- **Production-like mode (linked package):** This method simulates a global
  installation by linking your local package. It's useful for testing a local
  build in a production workflow.

  ```bash
  # Link the local cli package to your global node_modules
  npm link packages/cli

  # Now you can run your local version using the `gemini` command
  gemini
  ```

---

### 4. Running the latest Gemini CLI commit from GitHub

You can run the most recently committed version of Gemini CLI directly from the
GitHub repository. This is useful for testing features still in development.

```bash
# Execute the CLI directly from the main branch on GitHub
npx https://github.com/google-gemini/gemini-cli
```

## Deployment architecture

The execution methods described above are made possible by the following
architectural components and processes:

**NPM packages**

Gemini CLI project is a monorepo that publishes two core packages to the NPM
registry:

- `@google/gemini-cli-core`: The backend, handling logic and tool execution.
- `@google/gemini-cli`: The user-facing frontend.

These packages are used when performing the standard installation and when
running Gemini CLI from the source.

**Build and packaging processes**

There are two distinct build processes used, depending on the distribution
channel:

- **NPM publication:** For publishing to the NPM registry, the TypeScript source
  code in `@google/gemini-cli-core` and `@google/gemini-cli` is transpiled into
  standard JavaScript using the TypeScript Compiler (`tsc`). The resulting
  `dist/` directory is what gets published in the NPM package. This is a
  standard approach for TypeScript libraries.

- **GitHub `npx` execution:** When running the latest version of Gemini CLI
  directly from GitHub, a different process is triggered by the `prepare` script
  in `package.json`. This script uses `esbuild` to bundle the entire application
  and its dependencies into a single, self-contained JavaScript file. This
  bundle is created on-the-fly on the user's machine and is not checked into the
  repository.

**Docker sandbox image**

The Docker-based execution method is supported by the `gemini-cli-sandbox`
container image. This image is published to a container registry and contains a
pre-installed, global version of Gemini CLI.

## Release process

The release process is automated through GitHub Actions. The release workflow
performs the following actions:

1.  Build the NPM packages using `tsc`.
2.  Publish the NPM packages to the artifact registry.
3.  Create GitHub releases with bundled assets.


--- docs/cli/uninstall.md ---
# Uninstalling the CLI

Your uninstall method depends on how you ran the CLI. Follow the instructions
for either npx or a global npm installation.

## Method 1: Using npx

npx runs packages from a temporary cache without a permanent installation. To
"uninstall" the CLI, you must clear this cache, which will remove gemini-cli and
any other packages previously executed with npx.

The npx cache is a directory named `_npx` inside your main npm cache folder. You
can find your npm cache path by running `npm config get cache`.

**For macOS / Linux**

```bash
# The path is typically ~/.npm/_npx
rm -rf "$(npm config get cache)/_npx"
```

**For Windows**

_Command Prompt_

```cmd
:: The path is typically %LocalAppData%\npm-cache\_npx
rmdir /s /q "%LocalAppData%\npm-cache\_npx"
```

_PowerShell_

```powershell
# The path is typically $env:LocalAppData\npm-cache\_npx
Remove-Item -Path (Join-Path $env:LocalAppData "npm-cache\_npx") -Recurse -Force
```

## Method 2: Using npm (global install)

If you installed the CLI globally (e.g., `npm install -g @google/gemini-cli`),
use the `npm uninstall` command with the `-g` flag to remove it.

```bash
npm uninstall -g @google/gemini-cli
```

This command completely removes the package from your system.


--- packages/core/src/ide/ide-installer.ts ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import * as child_process from 'node:child_process';
import * as process from 'node:process';
import * as path from 'node:path';
import * as fs from 'node:fs';
import { IDE_DEFINITIONS, type IdeInfo } from './detect-ide.js';
import { GEMINI_CLI_COMPANION_EXTENSION_NAME } from './constants.js';
import { homedir } from '../utils/paths.js';

export interface IdeInstaller {
  install(): Promise<InstallResult>;
}

export interface InstallResult {
  success: boolean;
  message: string;
}

async function findCommand(
  command: string,
  platform: NodeJS.Platform = process.platform,
): Promise<string | null> {
  // 1. Check PATH first.
  try {
    if (platform === 'win32') {
      const result = child_process
        .execSync(`where.exe ${command}`)
        .toString()
        .trim();
      // `where.exe` can return multiple paths. Return the first one.
      const firstPath = result.split(/\r?\n/)[0];
      if (firstPath) {
        return firstPath;
      }
    } else {
      child_process.execSync(`command -v ${command}`, {
        stdio: 'ignore',
      });
      return command;
    }
  } catch {
    // Not in PATH, continue to check common locations.
  }

  // 2. Check common installation locations.
  const locations: string[] = [];
  const homeDir = homedir();

  if (command === 'code' || command === 'code.cmd') {
    if (platform === 'darwin') {
      // macOS
      locations.push(
        '/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code',
        path.join(homeDir, 'Library/Application Support/Code/bin/code'),
      );
    } else if (platform === 'linux') {
      // Linux
      locations.push(
        '/usr/share/code/bin/code',
        '/snap/bin/code',
        path.join(homeDir, '.local/share/code/bin/code'),
      );
    } else if (platform === 'win32') {
      // Windows
      locations.push(
        path.join(
          process.env['ProgramFiles'] || 'C:\\Program Files',
          'Microsoft VS Code',
          'bin',
          'code.cmd',
        ),
        path.join(
          homeDir,
          'AppData',
          'Local',
          'Programs',
          'Microsoft VS Code',
          'bin',
          'code.cmd',
        ),
      );
    }
  }

  for (const location of locations) {
    if (fs.existsSync(location)) {
      return location;
    }
  }

  return null;
}

class VsCodeInstaller implements IdeInstaller {
  private vsCodeCommand: Promise<string | null>;

  constructor(
    readonly ideInfo: IdeInfo,
    readonly platform = process.platform,
  ) {
    const command = platform === 'win32' ? 'code.cmd' : 'code';
    this.vsCodeCommand = findCommand(command, platform);
  }

  async install(): Promise<InstallResult> {
    const commandPath = await this.vsCodeCommand;
    if (!commandPath) {
      return {
        success: false,
        message: `${this.ideInfo.displayName} CLI not found. Please ensure 'code' is in your system's PATH. For help, see https://code.visualstudio.com/docs/configure/command-line#_code-is-not-recognized-as-an-internal-or-external-command. You can also install the '${GEMINI_CLI_COMPANION_EXTENSION_NAME}' extension manually from the VS Code marketplace.`,
      };
    }

    try {
      const result = child_process.spawnSync(
        commandPath,
        [
          '--install-extension',
          'google.gemini-cli-vscode-ide-companion',
          '--force',
        ],
        { stdio: 'pipe', shell: this.platform === 'win32' },
      );

      if (result.status !== 0) {
        throw new Error(
          `Failed to install extension: ${result.stderr?.toString()}`,
        );
      }

      return {
        success: true,
        message: `${this.ideInfo.displayName} companion extension was installed successfully.`,
      };
    } catch (_error) {
      return {
        success: false,
        message: `Failed to install ${this.ideInfo.displayName} companion extension. Please try installing '${GEMINI_CLI_COMPANION_EXTENSION_NAME}' manually from the ${this.ideInfo.displayName} extension marketplace.`,
      };
    }
  }
}

class AntigravityInstaller implements IdeInstaller {
  constructor(
    readonly ideInfo: IdeInfo,
    readonly platform = process.platform,
  ) {}

  async install(): Promise<InstallResult> {
    const command = process.env['ANTIGRAVITY_CLI_ALIAS'];
    if (!command) {
      return {
        success: false,
        message: 'ANTIGRAVITY_CLI_ALIAS environment variable not set.',
      };
    }

    const commandPath = await findCommand(command, this.platform);
    if (!commandPath) {
      return {
        success: false,
        message: `${command} not found. Please ensure it is in your system's PATH.`,
      };
    }

    try {
      const result = child_process.spawnSync(
        commandPath,
        [
          '--install-extension',
          'google.gemini-cli-vscode-ide-companion',
          '--force',
        ],
        { stdio: 'pipe', shell: this.platform === 'win32' },
      );

      if (result.status !== 0) {
        throw new Error(
          `Failed to install extension: ${result.stderr?.toString()}`,
        );
      }

      return {
        success: true,
        message: `${this.ideInfo.displayName} companion extension was installed successfully.`,
      };
    } catch (_error) {
      return {
        success: false,
        message: `Failed to install ${this.ideInfo.displayName} companion extension. Please try installing '${GEMINI_CLI_COMPANION_EXTENSION_NAME}' manually from the ${this.ideInfo.displayName} extension marketplace.`,
      };
    }
  }
}

export function getIdeInstaller(
  ide: IdeInfo,
  platform = process.platform,
): IdeInstaller | null {
  switch (ide.name) {
    case IDE_DEFINITIONS.vscode.name:
    case IDE_DEFINITIONS.firebasestudio.name:
      return new VsCodeInstaller(ide, platform);
    case IDE_DEFINITIONS.antigravity.name:
      return new AntigravityInstaller(ide, platform);
    default:
      return null;
  }
}


--- packages/core/src/ide/ide-installer.test.ts ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { vi } from 'vitest';

vi.mock('node:child_process', async (importOriginal) => {
  const actual = await importOriginal();
  return {
    ...(actual as object),
    execSync: vi.fn(),
    spawnSync: vi.fn(() => ({ status: 0 })),
  };
});
vi.mock('node:fs');
vi.mock('node:os');
vi.mock('../utils/paths.js', async (importOriginal) => {
  const actual = await importOriginal<typeof import('../utils/paths.js')>();
  return {
    ...actual,
    homedir: vi.fn(),
  };
});

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { getIdeInstaller } from './ide-installer.js';
import * as child_process from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { IDE_DEFINITIONS, type IdeInfo } from './detect-ide.js';
import { homedir as pathsHomedir } from '../utils/paths.js';

describe('ide-installer', () => {
  const HOME_DIR = '/home/user';

  beforeEach(() => {
    vi.spyOn(os, 'homedir').mockReturnValue(HOME_DIR);
    vi.mocked(pathsHomedir).mockReturnValue(HOME_DIR);
  });

  afterEach(() => {
    vi.restoreAllMocks();
  });

  describe('getIdeInstaller', () => {
    it.each([
      { ide: IDE_DEFINITIONS.vscode },
      { ide: IDE_DEFINITIONS.firebasestudio },
    ])('returns a VsCodeInstaller for "$ide.name"', ({ ide }) => {
      const installer = getIdeInstaller(ide);

      expect(installer).not.toBeNull();
      expect(installer?.install).toEqual(expect.any(Function));
    });

    it('returns an AntigravityInstaller for "antigravity"', () => {
      const installer = getIdeInstaller(IDE_DEFINITIONS.antigravity);

      expect(installer).not.toBeNull();
      expect(installer?.install).toEqual(expect.any(Function));
    });
  });

  describe('VsCodeInstaller', () => {
    function setup({
      ide = IDE_DEFINITIONS.vscode,
      existsResult = false,
      execSync = () => '',
      platform = 'linux' as NodeJS.Platform,
    }: {
      ide?: IdeInfo;
      existsResult?: boolean;
      execSync?: () => string;
      platform?: NodeJS.Platform;
    } = {}) {
      vi.spyOn(child_process, 'execSync').mockImplementation(execSync);
      vi.spyOn(fs, 'existsSync').mockReturnValue(existsResult);
      const installer = getIdeInstaller(ide, platform)!;

      return { installer };
    }

    describe('install', () => {
      it.each([
        {
          platform: 'win32' as NodeJS.Platform,
          expectedLookupPaths: [
            path.join('C:\\Program Files', 'Microsoft VS Code/bin/code.cmd'),
            path.join(
              HOME_DIR,
              '/AppData/Local/Programs/Microsoft VS Code/bin/code.cmd',
            ),
          ],
        },
        {
          platform: 'darwin' as NodeJS.Platform,
          expectedLookupPaths: [
            '/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code',
            path.join(HOME_DIR, 'Library/Application Support/Code/bin/code'),
          ],
        },
        {
          platform: 'linux' as NodeJS.Platform,
          expectedLookupPaths: ['/usr/share/code/bin/code'],
        },
      ])(
        'identifies the path to code cli on platform: $platform',
        async ({ platform, expectedLookupPaths }) => {
          const { installer } = setup({
            platform,
            execSync: () => {
              throw new Error('Command not found'); // `code` is not in PATH
            },
          });
          await installer.install();
          for (const [idx, path] of expectedLookupPaths.entries()) {
            expect(fs.existsSync).toHaveBeenNthCalledWith(idx + 1, path);
          }
        },
      );

      it('installs the extension using code cli', async () => {
        const { installer } = setup({
          platform: 'linux',
        });
        await installer.install();
        expect(child_process.spawnSync).toHaveBeenCalledWith(
          'code',
          [
            '--install-extension',
            'google.gemini-cli-vscode-ide-companion',
            '--force',
          ],
          { stdio: 'pipe', shell: false },
        );
      });

      it('installs the extension using code cli on windows', async () => {
        const { installer } = setup({
          platform: 'win32',
          execSync: () => 'C:\\Program Files\\Microsoft VS Code\\bin\\code.cmd',
        });
        await installer.install();
        expect(child_process.spawnSync).toHaveBeenCalledWith(
          'C:\\Program Files\\Microsoft VS Code\\bin\\code.cmd',
          [
            '--install-extension',
            'google.gemini-cli-vscode-ide-companion',
            '--force',
          ],
          { stdio: 'pipe', shell: true },
        );
      });

      it.each([
        {
          ide: IDE_DEFINITIONS.vscode,
          expectedMessage:
            'VS Code companion extension was installed successfully',
        },
        {
          ide: IDE_DEFINITIONS.firebasestudio,
          expectedMessage:
            'Firebase Studio companion extension was installed successfully',
        },
      ])(
        'returns that the cli was installed successfully',
        async ({ ide, expectedMessage }) => {
          const { installer } = setup({ ide });
          const result = await installer.install();
          expect(result.success).toBe(true);
          expect(result.message).toContain(expectedMessage);
        },
      );

      it.each([
        {
          ide: IDE_DEFINITIONS.vscode,
          expectedErr: 'VS Code CLI not found',
        },
        {
          ide: IDE_DEFINITIONS.firebasestudio,
          expectedErr: 'Firebase Studio CLI not found',
        },
      ])(
        'should return a failure message if $ide is not installed',
        async ({ ide, expectedErr }) => {
          const { installer } = setup({
            ide,
            execSync: () => {
              throw new Error('Command not found');
            },
            existsResult: false,
          });
          const result = await installer.install();
          expect(result.success).toBe(false);
          expect(result.message).toContain(expectedErr);
        },
      );
    });
  });
});

describe('AntigravityInstaller', () => {
  function setup({
    execSync = () => '',
    platform = 'linux' as NodeJS.Platform,
  }: {
    execSync?: () => string;
    platform?: NodeJS.Platform;
  } = {}) {
    vi.spyOn(child_process, 'execSync').mockImplementation(execSync);
    const installer = getIdeInstaller(IDE_DEFINITIONS.antigravity, platform)!;

    return { installer };
  }

  it('installs the extension using the alias', async () => {
    vi.stubEnv('ANTIGRAVITY_CLI_ALIAS', 'agy');
    const { installer } = setup({});
    const result = await installer.install();

    expect(result.success).toBe(true);
    expect(child_process.spawnSync).toHaveBeenCalledWith(
      'agy',
      [
        '--install-extension',
        'google.gemini-cli-vscode-ide-companion',
        '--force',
      ],
      { stdio: 'pipe', shell: false },
    );
  });

  it('returns a failure message if the alias is not set', async () => {
    vi.stubEnv('ANTIGRAVITY_CLI_ALIAS', '');
    const { installer } = setup({});
    const result = await installer.install();

    expect(result.success).toBe(false);
    expect(result.message).toContain(
      'ANTIGRAVITY_CLI_ALIAS environment variable not set',
    );
  });

  it('returns a failure message if the command is not found', async () => {
    vi.stubEnv('ANTIGRAVITY_CLI_ALIAS', 'not-a-command');
    const { installer } = setup({
      execSync: () => {
        throw new Error('Command not found');
      },
    });
    const result = await installer.install();

    expect(result.success).toBe(false);
    expect(result.message).toContain('not-a-command not found');
  });
});


--- packages/cli/src/utils/installationInfo.ts ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { debugLogger, isGitRepository } from '@google/gemini-cli-core';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as childProcess from 'node:child_process';
import process from 'node:process';

export const isDevelopment = process.env['NODE_ENV'] === 'development';

export enum PackageManager {
  NPM = 'npm',
  YARN = 'yarn',
  PNPM = 'pnpm',
  PNPX = 'pnpx',
  BUN = 'bun',
  BUNX = 'bunx',
  HOMEBREW = 'homebrew',
  NPX = 'npx',
  UNKNOWN = 'unknown',
}

export interface InstallationInfo {
  packageManager: PackageManager;
  isGlobal: boolean;
  updateCommand?: string;
  updateMessage?: string;
}

export function getInstallationInfo(
  projectRoot: string,
  isAutoUpdateDisabled: boolean,
): InstallationInfo {
  const cliPath = process.argv[1];
  if (!cliPath) {
    return { packageManager: PackageManager.UNKNOWN, isGlobal: false };
  }

  try {
    // Normalize path separators to forward slashes for consistent matching.
    const realPath = fs.realpathSync(cliPath).replace(/\\/g, '/');
    const normalizedProjectRoot = projectRoot?.replace(/\\/g, '/');
    const isGit = isGitRepository(process.cwd());

    // Check for local git clone first
    if (
      isGit &&
      normalizedProjectRoot &&
      realPath.startsWith(normalizedProjectRoot) &&
      !realPath.includes('/node_modules/')
    ) {
      return {
        packageManager: PackageManager.UNKNOWN, // Not managed by a package manager in this sense
        isGlobal: false,
        updateMessage:
          'Running from a local git clone. Please update with "git pull".',
      };
    }

    // Check for npx/pnpx
    if (realPath.includes('/.npm/_npx') || realPath.includes('/npm/_npx')) {
      return {
        packageManager: PackageManager.NPX,
        isGlobal: false,
        updateMessage: 'Running via npx, update not applicable.',
      };
    }
    if (realPath.includes('/.pnpm/_pnpx')) {
      return {
        packageManager: PackageManager.PNPX,
        isGlobal: false,
        updateMessage: 'Running via pnpx, update not applicable.',
      };
    }

    // Check for Homebrew
    if (process.platform === 'darwin') {
      try {
        // The package name in homebrew is gemini-cli
        childProcess.execSync('brew list -1 | grep -q "^gemini-cli$"', {
          stdio: 'ignore',
        });
        return {
          packageManager: PackageManager.HOMEBREW,
          isGlobal: true,
          updateMessage:
            'Installed via Homebrew. Please update with "brew upgrade gemini-cli".',
        };
      } catch (_error) {
        // Brew is not installed or gemini-cli is not installed via brew.
        // Continue to the next check.
      }
    }

    // Check for pnpm
    if (realPath.includes('/.pnpm/global')) {
      const updateCommand = 'pnpm add -g @google/gemini-cli@latest';
      return {
        packageManager: PackageManager.PNPM,
        isGlobal: true,
        updateCommand,
        updateMessage: isAutoUpdateDisabled
          ? `Please run ${updateCommand} to update`
          : 'Installed with pnpm. Attempting to automatically update now...',
      };
    }

    // Check for yarn
    if (realPath.includes('/.yarn/global')) {
      const updateCommand = 'yarn global add @google/gemini-cli@latest';
      return {
        packageManager: PackageManager.YARN,
        isGlobal: true,
        updateCommand,
        updateMessage: isAutoUpdateDisabled
          ? `Please run ${updateCommand} to update`
          : 'Installed with yarn. Attempting to automatically update now...',
      };
    }

    // Check for bun
    if (realPath.includes('/.bun/install/cache')) {
      return {
        packageManager: PackageManager.BUNX,
        isGlobal: false,
        updateMessage: 'Running via bunx, update not applicable.',
      };
    }
    if (realPath.includes('/.bun/bin')) {
      const updateCommand = 'bun add -g @google/gemini-cli@latest';
      return {
        packageManager: PackageManager.BUN,
        isGlobal: true,
        updateCommand,
        updateMessage: isAutoUpdateDisabled
          ? `Please run ${updateCommand} to update`
          : 'Installed with bun. Attempting to automatically update now...',
      };
    }

    // Check for local install
    if (
      normalizedProjectRoot &&
      realPath.startsWith(`${normalizedProjectRoot}/node_modules`)
    ) {
      let pm = PackageManager.NPM;
      if (fs.existsSync(path.join(projectRoot, 'yarn.lock'))) {
        pm = PackageManager.YARN;
      } else if (fs.existsSync(path.join(projectRoot, 'pnpm-lock.yaml'))) {
        pm = PackageManager.PNPM;
      } else if (fs.existsSync(path.join(projectRoot, 'bun.lockb'))) {
        pm = PackageManager.BUN;
      }
      return {
        packageManager: pm,
        isGlobal: false,
        updateMessage:
          "Locally installed. Please update via your project's package.json.",
      };
    }

    // Assume global npm
    const updateCommand = 'npm install -g @google/gemini-cli@latest';
    return {
      packageManager: PackageManager.NPM,
      isGlobal: true,
      updateCommand,
      updateMessage: isAutoUpdateDisabled
        ? `Please run ${updateCommand} to update`
        : 'Installed with npm. Attempting to automatically update now...',
    };
  } catch (error) {
    debugLogger.log(error);
    return { packageManager: PackageManager.UNKNOWN, isGlobal: false };
  }
}


--- packages/cli/src/utils/installationInfo.test.ts ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { getInstallationInfo, PackageManager } from './installationInfo.js';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as childProcess from 'node:child_process';
import { isGitRepository, debugLogger } from '@google/gemini-cli-core';

vi.mock('@google/gemini-cli-core', async (importOriginal) => {
  const actual =
    await importOriginal<typeof import('@google/gemini-cli-core')>();
  return {
    ...actual,
    isGitRepository: vi.fn(),
  };
});

vi.mock('fs', async (importOriginal) => {
  const actualFs = await importOriginal<typeof fs>();
  return {
    ...actualFs,
    realpathSync: vi.fn(),
    existsSync: vi.fn(),
  };
});

vi.mock('child_process', async (importOriginal) => {
  const actual = await importOriginal<typeof import('child_process')>();
  return {
    ...actual,
    execSync: vi.fn(),
  };
});

const mockedIsGitRepository = vi.mocked(isGitRepository);
const mockedRealPathSync = vi.mocked(fs.realpathSync);
const mockedExistsSync = vi.mocked(fs.existsSync);
const mockedExecSync = vi.mocked(childProcess.execSync);

describe('getInstallationInfo', () => {
  const projectRoot = '/path/to/project';
  let originalArgv: string[];

  beforeEach(() => {
    vi.resetAllMocks();
    originalArgv = [...process.argv];
    // Mock process.cwd() for isGitRepository
    vi.spyOn(process, 'cwd').mockReturnValue(projectRoot);
    vi.spyOn(debugLogger, 'log').mockImplementation(() => {});
  });

  afterEach(() => {
    process.argv = originalArgv;
  });

  it('should return UNKNOWN when cliPath is not available', () => {
    process.argv[1] = '';
    const info = getInstallationInfo(projectRoot, false);
    expect(info.packageManager).toBe(PackageManager.UNKNOWN);
  });

  it('should return UNKNOWN and log error if realpathSync fails', () => {
    process.argv[1] = '/path/to/cli';
    const error = new Error('realpath failed');
    mockedRealPathSync.mockImplementation(() => {
      throw error;
    });

    const info = getInstallationInfo(projectRoot, false);

    expect(info.packageManager).toBe(PackageManager.UNKNOWN);
    expect(debugLogger.log).toHaveBeenCalledWith(error);
  });

  it('should detect running from a local git clone', () => {
    process.argv[1] = `${projectRoot}/packages/cli/dist/index.js`;
    mockedRealPathSync.mockReturnValue(
      `${projectRoot}/packages/cli/dist/index.js`,
    );
    mockedIsGitRepository.mockReturnValue(true);

    const info = getInstallationInfo(projectRoot, false);

    expect(info.packageManager).toBe(PackageManager.UNKNOWN);
    expect(info.isGlobal).toBe(false);
    expect(info.updateMessage).toBe(
      'Running from a local git clone. Please update with "git pull".',
    );
  });

  it('should detect running via npx', () => {
    const npxPath = `/Users/test/.npm/_npx/12345/bin/gemini`;
    process.argv[1] = npxPath;
    mockedRealPathSync.mockReturnValue(npxPath);

    const info = getInstallationInfo(projectRoot, false);

    expect(info.packageManager).toBe(PackageManager.NPX);
    expect(info.isGlobal).toBe(false);
    expect(info.updateMessage).toBe('Running via npx, update not applicable.');
  });

  it('should detect running via pnpx', () => {
    const pnpxPath = `/Users/test/.pnpm/_pnpx/12345/bin/gemini`;
    process.argv[1] = pnpxPath;
    mockedRealPathSync.mockReturnValue(pnpxPath);

    const info = getInstallationInfo(projectRoot, false);

    expect(info.packageManager).toBe(PackageManager.PNPX);
    expect(info.isGlobal).toBe(false);
    expect(info.updateMessage).toBe('Running via pnpx, update not applicable.');
  });

  it('should detect running via bunx', () => {
    const bunxPath = `/Users/test/.bun/install/cache/12345/bin/gemini`;
    process.argv[1] = bunxPath;
    mockedRealPathSync.mockReturnValue(bunxPath);
    mockedExecSync.mockImplementation(() => {
      throw new Error('Command failed');
    });

    const info = getInstallationInfo(projectRoot, false);

    expect(info.packageManager).toBe(PackageManager.BUNX);
    expect(info.isGlobal).toBe(false);
    expect(info.updateMessage).toBe('Running via bunx, update not applicable.');
  });

  it('should detect Homebrew installation via execSync', () => {
    Object.defineProperty(process, 'platform', {
      value: 'darwin',
    });
    const cliPath = '/usr/local/bin/gemini';
    process.argv[1] = cliPath;
    mockedRealPathSync.mockReturnValue(cliPath);
    mockedExecSync.mockReturnValue(Buffer.from('gemini-cli')); // Simulate successful command

    const info = getInstallationInfo(projectRoot, false);

    expect(mockedExecSync).toHaveBeenCalledWith(
      'brew list -1 | grep -q "^gemini-cli$"',
      { stdio: 'ignore' },
    );
    expect(info.packageManager).toBe(PackageManager.HOMEBREW);
    expect(info.isGlobal).toBe(true);
    expect(info.updateMessage).toBe(
      'Installed via Homebrew. Please update with "brew upgrade gemini-cli".',
    );
  });

  it('should fall through if brew command fails', () => {
    Object.defineProperty(process, 'platform', {
      value: 'darwin',
    });
    const cliPath = '/usr/local/bin/gemini';
    process.argv[1] = cliPath;
    mockedRealPathSync.mockReturnValue(cliPath);
    mockedExecSync.mockImplementation(() => {
      throw new Error('Command failed');
    });

    const info = getInstallationInfo(projectRoot, false);

    expect(mockedExecSync).toHaveBeenCalledWith(
      'brew list -1 | grep -q "^gemini-cli$"',
      { stdio: 'ignore' },
    );
    // Should fall back to default global npm
    expect(info.packageManager).toBe(PackageManager.NPM);
    expect(info.isGlobal).toBe(true);
  });

  it('should detect global pnpm installation', () => {
    const pnpmPath = `/Users/test/.pnpm/global/5/node_modules/.pnpm/some-hash/node_modules/@google/gemini-cli/dist/index.js`;
    process.argv[1] = pnpmPath;
    mockedRealPathSync.mockReturnValue(pnpmPath);
    mockedExecSync.mockImplementation(() => {
      throw new Error('Command failed');
    });

    const info = getInstallationInfo(projectRoot, false);
    expect(info.packageManager).toBe(PackageManager.PNPM);
    expect(info.isGlobal).toBe(true);
    expect(info.updateCommand).toBe('pnpm add -g @google/gemini-cli@latest');
    expect(info.updateMessage).toContain('Attempting to automatically update');

    const infoDisabled = getInstallationInfo(projectRoot, true);
    expect(infoDisabled.updateMessage).toContain('Please run pnpm add');
  });

  it('should detect global yarn installation', () => {
    const yarnPath = `/Users/test/.yarn/global/node_modules/@google/gemini-cli/dist/index.js`;
    process.argv[1] = yarnPath;
    mockedRealPathSync.mockReturnValue(yarnPath);
    mockedExecSync.mockImplementation(() => {
      throw new Error('Command failed');
    });

    const info = getInstallationInfo(projectRoot, false);
    expect(info.packageManager).toBe(PackageManager.YARN);
    expect(info.isGlobal).toBe(true);
    expect(info.updateCommand).toBe(
      'yarn global add @google/gemini-cli@latest',
    );
    expect(info.updateMessage).toContain('Attempting to automatically update');

    const infoDisabled = getInstallationInfo(projectRoot, true);
    expect(infoDisabled.updateMessage).toContain('Please run yarn global add');
  });

  it('should detect global bun installation', () => {
    const bunPath = `/Users/test/.bun/bin/gemini`;
    process.argv[1] = bunPath;
    mockedRealPathSync.mockReturnValue(bunPath);
    mockedExecSync.mockImplementation(() => {
      throw new Error('Command failed');
    });

    const info = getInstallationInfo(projectRoot, false);
    expect(info.packageManager).toBe(PackageManager.BUN);
    expect(info.isGlobal).toBe(true);
    expect(info.updateCommand).toBe('bun add -g @google/gemini-cli@latest');
    expect(info.updateMessage).toContain('Attempting to automatically update');

    const infoDisabled = getInstallationInfo(projectRoot, true);
    expect(infoDisabled.updateMessage).toContain('Please run bun add');
  });

  it('should detect local installation and identify yarn from lockfile', () => {
    const localPath = `${projectRoot}/node_modules/.bin/gemini`;
    process.argv[1] = localPath;
    mockedRealPathSync.mockReturnValue(localPath);
    mockedExecSync.mockImplementation(() => {
      throw new Error('Command failed');
    });
    mockedExistsSync.mockImplementation(
      (p) => p === path.join(projectRoot, 'yarn.lock'),
    );

    const info = getInstallationInfo(projectRoot, false);

    expect(info.packageManager).toBe(PackageManager.YARN);
    expect(info.isGlobal).toBe(false);
    expect(info.updateMessage).toContain('Locally installed');
  });

  it('should detect local installation and identify pnpm from lockfile', () => {
    const localPath = `${projectRoot}/node_modules/.bin/gemini`;
    process.argv[1] = localPath;
    mockedRealPathSync.mockReturnValue(localPath);
    mockedExecSync.mockImplementation(() => {
      throw new Error('Command failed');
    });
    mockedExistsSync.mockImplementation(
      (p) => p === path.join(projectRoot, 'pnpm-lock.yaml'),
    );

    const info = getInstallationInfo(projectRoot, false);

    expect(info.packageManager).toBe(PackageManager.PNPM);
    expect(info.isGlobal).toBe(false);
  });

  it('should detect local installation and identify bun from lockfile', () => {
    const localPath = `${projectRoot}/node_modules/.bin/gemini`;
    process.argv[1] = localPath;
    mockedRealPathSync.mockReturnValue(localPath);
    mockedExecSync.mockImplementation(() => {
      throw new Error('Command failed');
    });
    mockedExistsSync.mockImplementation(
      (p) => p === path.join(projectRoot, 'bun.lockb'),
    );

    const info = getInstallationInfo(projectRoot, false);

    expect(info.packageManager).toBe(PackageManager.BUN);
    expect(info.isGlobal).toBe(false);
  });

  it('should default to local npm installation if no lockfile is found', () => {
    const localPath = `${projectRoot}/node_modules/.bin/gemini`;
    process.argv[1] = localPath;
    mockedRealPathSync.mockReturnValue(localPath);
    mockedExecSync.mockImplementation(() => {
      throw new Error('Command failed');
    });
    mockedExistsSync.mockReturnValue(false); // No lockfiles

    const info = getInstallationInfo(projectRoot, false);

    expect(info.packageManager).toBe(PackageManager.NPM);
    expect(info.isGlobal).toBe(false);
  });

  it('should default to global npm installation for unrecognized paths', () => {
    const globalPath = `/usr/local/bin/gemini`;
    process.argv[1] = globalPath;
    mockedRealPathSync.mockReturnValue(globalPath);
    mockedExecSync.mockImplementation(() => {
      throw new Error('Command failed');
    });

    const info = getInstallationInfo(projectRoot, false);
    expect(info.packageManager).toBe(PackageManager.NPM);
    expect(info.isGlobal).toBe(true);
    expect(info.updateCommand).toBe('npm install -g @google/gemini-cli@latest');
    expect(info.updateMessage).toContain('Attempting to automatically update');

    const infoDisabled = getInstallationInfo(projectRoot, true);
    expect(infoDisabled.updateMessage).toContain('Please run npm install');
  });
});


--- packages/core/src/utils/installationManager.ts ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import * as fs from 'node:fs';
import { randomUUID } from 'node:crypto';
import * as path from 'node:path';
import { Storage } from '../config/storage.js';
import { debugLogger } from './debugLogger.js';

export class InstallationManager {
  private getInstallationIdPath(): string {
    return Storage.getInstallationIdPath();
  }

  private readInstallationIdFromFile(): string | null {
    const installationIdFile = this.getInstallationIdPath();
    if (fs.existsSync(installationIdFile)) {
      const installationid = fs
        .readFileSync(installationIdFile, 'utf-8')
        .trim();
      return installationid || null;
    }
    return null;
  }

  private writeInstallationIdToFile(installationId: string) {
    const installationIdFile = this.getInstallationIdPath();
    const dir = path.dirname(installationIdFile);
    fs.mkdirSync(dir, { recursive: true });
    fs.writeFileSync(installationIdFile, installationId, 'utf-8');
  }

  /**
   * Retrieves the installation ID from a file, creating it if it doesn't exist.
   * This ID is used for unique user installation tracking.
   * @returns A UUID string for the user.
   */
  getInstallationId(): string {
    try {
      let installationId = this.readInstallationIdFromFile();

      if (!installationId) {
        installationId = randomUUID();
        this.writeInstallationIdToFile(installationId);
      }

      return installationId;
    } catch (error) {
      debugLogger.warn(
        'Error accessing installation ID file, generating ephemeral ID:',
        error,
      );
      return '123456789';
    }
  }
}


--- packages/cli/src/commands/extensions/examples/mcp-server/example.ts ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';

const server = new McpServer({
  name: 'prompt-server',
  version: '1.0.0',
});

server.registerTool(
  'fetch_posts',
  {
    description: 'Fetches a list of posts from a public API.',
    inputSchema: z.object({}).shape,
  },
  async () => {
    const apiResponse = await fetch(
      'https://jsonplaceholder.typicode.com/posts',
    );
    const posts = await apiResponse.json();
    const response = { posts: posts.slice(0, 5) };
    return {
      content: [
        {
          type: 'text',
          text: JSON.stringify(response),
        },
      ],
    };
  },
);

server.registerPrompt(
  'poem-writer',
  {
    title: 'Poem Writer',
    description: 'Write a nice haiku',
    argsSchema: { title: z.string(), mood: z.string().optional() },
  },
  ({ title, mood }) => ({
    messages: [
      {
        role: 'user',
        content: {
          type: 'text',
          text: `Write a haiku${mood ? ` with the mood ${mood}` : ''} called ${title}. Note that a haiku is 5 syllables followed by 7 syllables followed by 5 syllables `,
        },
      },
    ],
  }),
);

const transport = new StdioServerTransport();
await server.connect(transport);


--- packages/cli/src/commands/extensions/examples/mcp-server/example.test.ts ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';

// Mock the MCP server and transport
const mockRegisterTool = vi.fn();
const mockRegisterPrompt = vi.fn();
const mockConnect = vi.fn();

vi.mock('@modelcontextprotocol/sdk/server/mcp.js', () => ({
  McpServer: vi.fn().mockImplementation(() => ({
    registerTool: mockRegisterTool,
    registerPrompt: mockRegisterPrompt,
    connect: mockConnect,
  })),
}));

vi.mock('@modelcontextprotocol/sdk/server/stdio.js', () => ({
  StdioServerTransport: vi.fn(),
}));

describe('MCP Server Example', () => {
  beforeEach(async () => {
    // Dynamically import the server setup after mocks are in place
    await import('./example.js');
  });

  afterEach(() => {
    vi.clearAllMocks();
    vi.resetModules();
  });

  it('should create an McpServer with the correct name and version', () => {
    expect(McpServer).toHaveBeenCalledWith({
      name: 'prompt-server',
      version: '1.0.0',
    });
  });

  it('should register the "fetch_posts" tool', () => {
    expect(mockRegisterTool).toHaveBeenCalledWith(
      'fetch_posts',
      {
        description: 'Fetches a list of posts from a public API.',
        inputSchema: z.object({}).shape,
      },
      expect.any(Function),
    );
  });

  it('should register the "poem-writer" prompt', () => {
    expect(mockRegisterPrompt).toHaveBeenCalledWith(
      'poem-writer',
      {
        title: 'Poem Writer',
        description: 'Write a nice haiku',
        argsSchema: expect.any(Object),
      },
      expect.any(Function),
    );
  });

  it('should connect the server to an StdioServerTransport', () => {
    expect(StdioServerTransport).toHaveBeenCalled();
    expect(mockConnect).toHaveBeenCalledWith(expect.any(StdioServerTransport));
  });

  describe('fetch_posts tool implementation', () => {
    it('should fetch posts and return a formatted response', async () => {
      const mockPosts = [
        { id: 1, title: 'Post 1' },
        { id: 2, title: 'Post 2' },
      ];
      global.fetch = vi.fn().mockResolvedValue({
        json: vi.fn().mockResolvedValue(mockPosts),
      });

      const toolFn = mockRegisterTool.mock.calls[0][2];
      const result = await toolFn();

      expect(global.fetch).toHaveBeenCalledWith(
        'https://jsonplaceholder.typicode.com/posts',
      );
      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: JSON.stringify({ posts: mockPosts }),
          },
        ],
      });
    });
  });

  describe('poem-writer prompt implementation', () => {
    it('should generate a prompt with a title', () => {
      const promptFn = mockRegisterPrompt.mock.calls[0][2];
      const result = promptFn({ title: 'My Poem' });
      expect(result).toEqual({
        messages: [
          {
            role: 'user',
            content: {
              type: 'text',
              text: 'Write a haiku called My Poem. Note that a haiku is 5 syllables followed by 7 syllables followed by 5 syllables ',
            },
          },
        ],
      });
    });

    it('should generate a prompt with a title and mood', () => {
      const promptFn = mockRegisterPrompt.mock.calls[0][2];
      const result = promptFn({ title: 'My Poem', mood: 'sad' });
      expect(result).toEqual({
        messages: [
          {
            role: 'user',
            content: {
              type: 'text',
              text: 'Write a haiku with the mood sad called My Poem. Note that a haiku is 5 syllables followed by 7 syllables followed by 5 syllables ',
            },
          },
        ],
      });
    });
  });
});


--- packages/cli/src/commands/extensions/examples/context/GEMINI.md ---
# Ink Library Screen Reader Guidance

When building custom components, it's important to keep accessibility in mind.
While Ink provides the building blocks, ensuring your components are accessible
will make your CLIs usable by a wider audience.

## General Principles

Provide screen reader-friendly output: Use the useIsScreenReaderEnabled hook to
detect if a screen reader is active. You can then render a more descriptive
output for screen reader users. Leverage ARIA props: For components that have a
specific role (e.g., a checkbox or a button), use the aria-role, aria-state, and
aria-label props on <Box> and <Text> to provide semantic meaning to screen
readers.


--- packages/core/src/core/apiKeyCredentialStorage.ts ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { HybridTokenStorage } from '../mcp/token-storage/hybrid-token-storage.js';
import type { OAuthCredentials } from '../mcp/token-storage/types.js';
import { debugLogger } from '../utils/debugLogger.js';

const KEYCHAIN_SERVICE_NAME = 'gemini-cli-api-key';
const DEFAULT_API_KEY_ENTRY = 'default-api-key';

const storage = new HybridTokenStorage(KEYCHAIN_SERVICE_NAME);

/**
 * Load cached API key
 */
export async function loadApiKey(): Promise<string | null> {
  try {
    const credentials = await storage.getCredentials(DEFAULT_API_KEY_ENTRY);

    if (credentials?.token?.accessToken) {
      return credentials.token.accessToken;
    }

    return null;
  } catch (error: unknown) {
    // Log other errors but don't crash, just return null so user can re-enter key
    debugLogger.error('Failed to load API key from storage:', error);
    return null;
  }
}

/**
 * Save API key
 */
export async function saveApiKey(
  apiKey: string | null | undefined,
): Promise<void> {
  if (!apiKey || apiKey.trim() === '') {
    try {
      await storage.deleteCredentials(DEFAULT_API_KEY_ENTRY);
    } catch (error: unknown) {
      // Ignore errors when deleting, as it might not exist
      debugLogger.warn('Failed to delete API key from storage:', error);
    }
    return;
  }

  // Wrap API key in OAuthCredentials format as required by HybridTokenStorage
  const credentials: OAuthCredentials = {
    serverName: DEFAULT_API_KEY_ENTRY,
    token: {
      accessToken: apiKey,
      tokenType: 'ApiKey',
    },
    updatedAt: Date.now(),
  };

  await storage.setCredentials(credentials);
}

/**
 * Clear cached API key
 */
export async function clearApiKey(): Promise<void> {
  try {
    await storage.deleteCredentials(DEFAULT_API_KEY_ENTRY);
  } catch (error: unknown) {
    debugLogger.error('Failed to clear API key from storage:', error);
  }
}


--- packages/core/src/core/apiKeyCredentialStorage.test.ts ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { describe, it, expect, vi, beforeEach } from 'vitest';
import {
  loadApiKey,
  saveApiKey,
  clearApiKey,
} from './apiKeyCredentialStorage.js';

const getCredentialsMock = vi.hoisted(() => vi.fn());
const setCredentialsMock = vi.hoisted(() => vi.fn());
const deleteCredentialsMock = vi.hoisted(() => vi.fn());

vi.mock('../mcp/token-storage/hybrid-token-storage.js', () => ({
  HybridTokenStorage: vi.fn().mockImplementation(() => ({
    getCredentials: getCredentialsMock,
    setCredentials: setCredentialsMock,
    deleteCredentials: deleteCredentialsMock,
  })),
}));

describe('ApiKeyCredentialStorage', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('should load an API key', async () => {
    getCredentialsMock.mockResolvedValue({
      serverName: 'default-api-key',
      token: {
        accessToken: 'test-key',
        tokenType: 'ApiKey',
      },
      updatedAt: Date.now(),
    });

    const apiKey = await loadApiKey();
    expect(apiKey).toBe('test-key');
    expect(getCredentialsMock).toHaveBeenCalledWith('default-api-key');
  });

  it('should return null if no API key is stored', async () => {
    getCredentialsMock.mockResolvedValue(null);
    const apiKey = await loadApiKey();
    expect(apiKey).toBeNull();
    expect(getCredentialsMock).toHaveBeenCalledWith('default-api-key');
  });

  it('should save an API key', async () => {
    await saveApiKey('new-key');
    expect(setCredentialsMock).toHaveBeenCalledWith(
      expect.objectContaining({
        serverName: 'default-api-key',
        token: expect.objectContaining({
          accessToken: 'new-key',
          tokenType: 'ApiKey',
        }),
      }),
    );
  });

  it('should clear an API key when saving empty key', async () => {
    await saveApiKey('');
    expect(deleteCredentialsMock).toHaveBeenCalledWith('default-api-key');
    expect(setCredentialsMock).not.toHaveBeenCalled();
  });

  it('should clear an API key when saving null key', async () => {
    await saveApiKey(null);
    expect(deleteCredentialsMock).toHaveBeenCalledWith('default-api-key');
    expect(setCredentialsMock).not.toHaveBeenCalled();
  });

  it('should clear an API key', async () => {
    await clearApiKey();
    expect(deleteCredentialsMock).toHaveBeenCalledWith('default-api-key');
  });

  it('should not throw when clearing an API key fails', async () => {
    deleteCredentialsMock.mockRejectedValueOnce(new Error('Failed to delete'));
    await expect(saveApiKey('')).resolves.not.toThrow();
    expect(deleteCredentialsMock).toHaveBeenCalledWith('default-api-key');
  });
});


--- packages/core/src/availability/errorClassification.ts ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import {
  TerminalQuotaError,
  RetryableQuotaError,
} from '../utils/googleQuotaErrors.js';
import { ModelNotFoundError } from '../utils/httpErrors.js';
import type { FailureKind } from './modelPolicy.js';

export function classifyFailureKind(error: unknown): FailureKind {
  if (error instanceof TerminalQuotaError) {
    return 'terminal';
  }
  if (error instanceof RetryableQuotaError) {
    return 'transient';
  }
  if (error instanceof ModelNotFoundError) {
    return 'not_found';
  }
  return 'unknown';
}


--- packages/core/src/telemetry/sdk.ts ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import {
  DiagLogLevel,
  diag,
  trace,
  context,
  metrics,
  propagation,
} from '@opentelemetry/api';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-grpc';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-grpc';
import { OTLPTraceExporter as OTLPTraceExporterHttp } from '@opentelemetry/exporter-trace-otlp-http';
import { OTLPLogExporter as OTLPLogExporterHttp } from '@opentelemetry/exporter-logs-otlp-http';
import { OTLPMetricExporter as OTLPMetricExporterHttp } from '@opentelemetry/exporter-metrics-otlp-http';
import { CompressionAlgorithm } from '@opentelemetry/otlp-exporter-base';
import { NodeSDK } from '@opentelemetry/sdk-node';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
import { resourceFromAttributes } from '@opentelemetry/resources';
import {
  BatchSpanProcessor,
  ConsoleSpanExporter,
} from '@opentelemetry/sdk-trace-node';
import {
  BatchLogRecordProcessor,
  ConsoleLogRecordExporter,
} from '@opentelemetry/sdk-logs';
import {
  ConsoleMetricExporter,
  PeriodicExportingMetricReader,
} from '@opentelemetry/sdk-metrics';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
import type { JWTInput } from 'google-auth-library';
import type { Config } from '../config/config.js';
import { SERVICE_NAME } from './constants.js';
import { initializeMetrics } from './metrics.js';
import { ClearcutLogger } from './clearcut-logger/clearcut-logger.js';
import {
  FileLogExporter,
  FileMetricExporter,
  FileSpanExporter,
} from './file-exporters.js';
import {
  GcpTraceExporter,
  GcpMetricExporter,
  GcpLogExporter,
} from './gcp-exporters.js';
import { TelemetryTarget } from './index.js';
import { debugLogger } from '../utils/debugLogger.js';
import { authEvents } from '../code_assist/oauth2.js';

// For troubleshooting, set the log level to DiagLogLevel.DEBUG
class DiagLoggerAdapter {
  error(message: string, ...args: unknown[]): void {
    debugLogger.error(message, ...args);
  }

  warn(message: string, ...args: unknown[]): void {
    debugLogger.warn(message, ...args);
  }

  info(message: string, ...args: unknown[]): void {
    debugLogger.log(message, ...args);
  }

  debug(message: string, ...args: unknown[]): void {
    debugLogger.debug(message, ...args);
  }

  verbose(message: string, ...args: unknown[]): void {
    debugLogger.debug(message, ...args);
  }
}

diag.setLogger(new DiagLoggerAdapter(), DiagLogLevel.INFO);

let sdk: NodeSDK | undefined;
let spanProcessor: BatchSpanProcessor | undefined;
let logRecordProcessor: BatchLogRecordProcessor | undefined;
let telemetryInitialized = false;
let callbackRegistered = false;
let authListener: ((newCredentials: JWTInput) => Promise<void>) | undefined =
  undefined;
const telemetryBuffer: Array<() => void | Promise<void>> = [];
let activeTelemetryEmail: string | undefined;

export function isTelemetrySdkInitialized(): boolean {
  return telemetryInitialized;
}

export function bufferTelemetryEvent(fn: () => void | Promise<void>): void {
  if (telemetryInitialized) {
    // eslint-disable-next-line @typescript-eslint/no-floating-promises
    fn();
  } else {
    telemetryBuffer.push(fn);
  }
}

async function flushTelemetryBuffer(): Promise<void> {
  if (!telemetryInitialized) return;
  while (telemetryBuffer.length > 0) {
    const fn = telemetryBuffer.shift();
    if (fn) {
      try {
        await fn();
      } catch (e) {
        debugLogger.error('Error executing buffered telemetry event', e);
      }
    }
  }
}

function parseOtlpEndpoint(
  otlpEndpointSetting: string | undefined,
  protocol: 'grpc' | 'http',
): string | undefined {
  if (!otlpEndpointSetting) {
    return undefined;
  }
  // Trim leading/trailing quotes that might come from env variables
  const trimmedEndpoint = otlpEndpointSetting.replace(/^["']|["']$/g, '');

  try {
    const url = new URL(trimmedEndpoint);
    if (protocol === 'grpc') {
      // OTLP gRPC exporters expect an endpoint in the format scheme://host:port
      // The `origin` property provides this, stripping any path, query, or hash.
      return url.origin;
    }
    // For http, use the full href.
    return url.href;
  } catch (error) {
    diag.error('Invalid OTLP endpoint URL provided:', trimmedEndpoint, error);
    return undefined;
  }
}

export async function initializeTelemetry(
  config: Config,
  credentials?: JWTInput,
): Promise<void> {
  if (!config.getTelemetryEnabled()) {
    return;
  }

  if (telemetryInitialized) {
    if (
      credentials?.client_email &&
      activeTelemetryEmail &&
      credentials.client_email !== activeTelemetryEmail
    ) {
      const message = `Telemetry credentials have changed (from ${activeTelemetryEmail} to ${credentials.client_email}), but telemetry cannot be re-initialized in this process. Please restart the CLI to use the new account for telemetry.`;
      debugLogger.error(message);
    }
    return;
  }

  if (config.getTelemetryUseCollector() && config.getTelemetryUseCliAuth()) {
    debugLogger.error(
      'Telemetry configuration error: "useCollector" and "useCliAuth" cannot both be true. ' +
        'CLI authentication is only supported with in-process exporters. ' +
        'Disabling telemetry.',
    );
    return;
  }

  // If using CLI auth and no credentials provided, defer initialization
  if (config.getTelemetryUseCliAuth() && !credentials) {
    // Register a callback to initialize telemetry when the user logs in.
    // This is done only once.
    if (!callbackRegistered) {
      callbackRegistered = true;
      authListener = async (newCredentials: JWTInput) => {
        if (config.getTelemetryEnabled() && config.getTelemetryUseCliAuth()) {
          debugLogger.log('Telemetry reinit with credentials.');
          await initializeTelemetry(config, newCredentials);
        }
      };
      authEvents.on('post_auth', authListener);
    }
    debugLogger.log(
      'CLI auth is requested but no credentials, deferring telemetry initialization.',
    );
    return;
  }

  const resource = resourceFromAttributes({
    [SemanticResourceAttributes.SERVICE_NAME]: SERVICE_NAME,
    [SemanticResourceAttributes.SERVICE_VERSION]: process.version,
    'session.id': config.getSessionId(),
  });

  const otlpEndpoint = config.getTelemetryOtlpEndpoint();
  const otlpProtocol = config.getTelemetryOtlpProtocol();
  const telemetryTarget = config.getTelemetryTarget();
  const useCollector = config.getTelemetryUseCollector();

  const parsedEndpoint = parseOtlpEndpoint(otlpEndpoint, otlpProtocol);
  const telemetryOutfile = config.getTelemetryOutfile();
  const useOtlp = !!parsedEndpoint && !telemetryOutfile;

  const gcpProjectId =
    process.env['OTLP_GOOGLE_CLOUD_PROJECT'] ||
    process.env['GOOGLE_CLOUD_PROJECT'];
  const useDirectGcpExport =
    telemetryTarget === TelemetryTarget.GCP && !useCollector;

  let spanExporter:
    | OTLPTraceExporter
    | OTLPTraceExporterHttp
    | GcpTraceExporter
    | FileSpanExporter
    | ConsoleSpanExporter;
  let logExporter:
    | OTLPLogExporter
    | OTLPLogExporterHttp
    | GcpLogExporter
    | FileLogExporter
    | ConsoleLogRecordExporter;
  let metricReader: PeriodicExportingMetricReader;

  if (useDirectGcpExport) {
    debugLogger.log(
      'Creating GCP exporters with projectId:',
      gcpProjectId,
      'using',
      credentials ? 'provided credentials' : 'ADC',
    );
    spanExporter = new GcpTraceExporter(gcpProjectId, credentials);
    logExporter = new GcpLogExporter(gcpProjectId, credentials);
    metricReader = new PeriodicExportingMetricReader({
      exporter: new GcpMetricExporter(gcpProjectId, credentials),
      exportIntervalMillis: 30000,
    });
  } else if (useOtlp) {
    if (otlpProtocol === 'http') {
      spanExporter = new OTLPTraceExporterHttp({
        url: parsedEndpoint,
      });
      logExporter = new OTLPLogExporterHttp({
        url: parsedEndpoint,
      });
      metricReader = new PeriodicExportingMetricReader({
        exporter: new OTLPMetricExporterHttp({
          url: parsedEndpoint,
        }),
        exportIntervalMillis: 10000,
      });
    } else {
      // grpc
      spanExporter = new OTLPTraceExporter({
        url: parsedEndpoint,
        compression: CompressionAlgorithm.GZIP,
      });
      logExporter = new OTLPLogExporter({
        url: parsedEndpoint,
        compression: CompressionAlgorithm.GZIP,
      });
      metricReader = new PeriodicExportingMetricReader({
        exporter: new OTLPMetricExporter({
          url: parsedEndpoint,
          compression: CompressionAlgorithm.GZIP,
        }),
        exportIntervalMillis: 10000,
      });
    }
  } else if (telemetryOutfile) {
    spanExporter = new FileSpanExporter(telemetryOutfile);
    logExporter = new FileLogExporter(telemetryOutfile);
    metricReader = new PeriodicExportingMetricReader({
      exporter: new FileMetricExporter(telemetryOutfile),
      exportIntervalMillis: 10000,
    });
  } else {
    spanExporter = new ConsoleSpanExporter();
    logExporter = new ConsoleLogRecordExporter();
    metricReader = new PeriodicExportingMetricReader({
      exporter: new ConsoleMetricExporter(),
      exportIntervalMillis: 10000,
    });
  }

  // Store processor references for manual flushing
  spanProcessor = new BatchSpanProcessor(spanExporter);
  logRecordProcessor = new BatchLogRecordProcessor(logExporter);

  sdk = new NodeSDK({
    resource,
    spanProcessors: [spanProcessor],
    logRecordProcessors: [logRecordProcessor],
    metricReader,
    instrumentations: [new HttpInstrumentation()],
  });

  try {
    sdk.start();
    if (config.getDebugMode()) {
      debugLogger.log('OpenTelemetry SDK started successfully.');
    }
    telemetryInitialized = true;
    activeTelemetryEmail = credentials?.client_email;
    initializeMetrics(config);
    void flushTelemetryBuffer();
  } catch (error) {
    debugLogger.error('Error starting OpenTelemetry SDK:', error);
  }

  // Note: We don't use process.on('exit') here because that callback is synchronous
  // and won't wait for the async shutdownTelemetry() to complete.
  // Instead, telemetry shutdown is handled in runExitCleanup() in cleanup.ts
  process.on('SIGTERM', () => {
    // eslint-disable-next-line @typescript-eslint/no-floating-promises
    shutdownTelemetry(config);
  });
  process.on('SIGINT', () => {
    // eslint-disable-next-line @typescript-eslint/no-floating-promises
    shutdownTelemetry(config);
  });
}

/**
 * Force flush all pending telemetry data to disk.
 * This is useful for ensuring telemetry is written before critical operations like /clear.
 */
export async function flushTelemetry(config: Config): Promise<void> {
  if (!telemetryInitialized || !spanProcessor || !logRecordProcessor) {
    return;
  }
  try {
    // Force flush all pending telemetry to disk
    await Promise.all([
      spanProcessor.forceFlush(),
      logRecordProcessor.forceFlush(),
    ]);
    if (config.getDebugMode()) {
      debugLogger.log('OpenTelemetry SDK flushed successfully.');
    }
  } catch (error) {
    debugLogger.error('Error flushing SDK:', error);
  }
}

export async function shutdownTelemetry(
  config: Config,
  fromProcessExit = true,
): Promise<void> {
  if (!telemetryInitialized || !sdk) {
    return;
  }
  try {
    ClearcutLogger.getInstance()?.shutdown();
    await sdk.shutdown();
    if (config.getDebugMode() && fromProcessExit) {
      debugLogger.log('OpenTelemetry SDK shut down successfully.');
    }
  } catch (error) {
    debugLogger.error('Error shutting down SDK:', error);
  } finally {
    telemetryInitialized = false;
    sdk = undefined;
    // Fully reset the global APIs to allow for re-initialization.
    // This is primarily for testing environments where the SDK is started
    // and stopped multiple times in the same process.
    trace.disable();
    context.disable();
    metrics.disable();
    propagation.disable();
    diag.disable();
    if (authListener) {
      authEvents.off('post_auth', authListener);
      authListener = undefined;
    }
    callbackRegistered = false;
    activeTelemetryEmail = undefined;
  }
}


--- packages/core/src/telemetry/sdk.test.ts ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import type { Config } from '../config/config.js';
import {
  initializeTelemetry,
  shutdownTelemetry,
  bufferTelemetryEvent,
} from './sdk.js';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-grpc';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-grpc';
import { OTLPTraceExporter as OTLPTraceExporterHttp } from '@opentelemetry/exporter-trace-otlp-http';
import { OTLPLogExporter as OTLPLogExporterHttp } from '@opentelemetry/exporter-logs-otlp-http';
import { OTLPMetricExporter as OTLPMetricExporterHttp } from '@opentelemetry/exporter-metrics-otlp-http';
import { ConsoleSpanExporter } from '@opentelemetry/sdk-trace-node';
import { NodeSDK } from '@opentelemetry/sdk-node';
import { GoogleAuth, type JWTInput } from 'google-auth-library';
import {
  GcpTraceExporter,
  GcpLogExporter,
  GcpMetricExporter,
} from './gcp-exporters.js';
import { TelemetryTarget } from './index.js';

import * as os from 'node:os';
import * as path from 'node:path';

import { authEvents } from '../code_assist/oauth2.js';
import { debugLogger } from '../utils/debugLogger.js';

vi.mock('@opentelemetry/exporter-trace-otlp-grpc');
vi.mock('@opentelemetry/exporter-logs-otlp-grpc');
vi.mock('@opentelemetry/exporter-metrics-otlp-grpc');
vi.mock('@opentelemetry/exporter-trace-otlp-http');
vi.mock('@opentelemetry/exporter-logs-otlp-http');
vi.mock('@opentelemetry/exporter-metrics-otlp-http');
vi.mock('@opentelemetry/sdk-trace-node');
vi.mock('@opentelemetry/sdk-node');
vi.mock('./gcp-exporters.js');
vi.mock('google-auth-library');
vi.mock('../utils/debugLogger.js', () => ({
  debugLogger: {
    log: vi.fn(),
    error: vi.fn(),
    warn: vi.fn(),
    debug: vi.fn(),
  },
}));

describe('Telemetry SDK', () => {
  let mockConfig: Config;
  const mockGetApplicationDefault = vi.fn();

  beforeEach(() => {
    vi.clearAllMocks();
    vi.mocked(GoogleAuth).mockImplementation(
      () =>
        ({
          getApplicationDefault: mockGetApplicationDefault,
        }) as unknown as GoogleAuth,
    );
    mockConfig = {
      getTelemetryEnabled: () => true,
      getTelemetryOtlpEndpoint: () => 'http://localhost:4317',
      getTelemetryOtlpProtocol: () => 'grpc',
      getTelemetryTarget: () => 'local',
      getTelemetryUseCollector: () => false,
      getTelemetryOutfile: () => undefined,
      getDebugMode: () => false,
      getSessionId: () => 'test-session',
      getTelemetryUseCliAuth: () => false,
      isInteractive: () => false,
    } as unknown as Config;
  });

  afterEach(async () => {
    await shutdownTelemetry(mockConfig);
  });

  it('should use gRPC exporters when protocol is grpc', async () => {
    await initializeTelemetry(mockConfig);

    expect(OTLPTraceExporter).toHaveBeenCalledWith({
      url: 'http://localhost:4317',
      compression: 'gzip',
    });
    expect(OTLPLogExporter).toHaveBeenCalledWith({
      url: 'http://localhost:4317',
      compression: 'gzip',
    });
    expect(OTLPMetricExporter).toHaveBeenCalledWith({
      url: 'http://localhost:4317',
      compression: 'gzip',
    });
    expect(NodeSDK.prototype.start).toHaveBeenCalled();
  });

  it('should use HTTP exporters when protocol is http', async () => {
    vi.spyOn(mockConfig, 'getTelemetryEnabled').mockReturnValue(true);
    vi.spyOn(mockConfig, 'getTelemetryOtlpProtocol').mockReturnValue('http');
    vi.spyOn(mockConfig, 'getTelemetryOtlpEndpoint').mockReturnValue(
      'http://localhost:4318',
    );

    await initializeTelemetry(mockConfig);

    expect(OTLPTraceExporterHttp).toHaveBeenCalledWith({
      url: 'http://localhost:4318/',
    });
    expect(OTLPLogExporterHttp).toHaveBeenCalledWith({
      url: 'http://localhost:4318/',
    });
    expect(OTLPMetricExporterHttp).toHaveBeenCalledWith({
      url: 'http://localhost:4318/',
    });
    expect(NodeSDK.prototype.start).toHaveBeenCalled();
  });

  it('should parse gRPC endpoint correctly', async () => {
    vi.spyOn(mockConfig, 'getTelemetryOtlpEndpoint').mockReturnValue(
      'https://my-collector.com',
    );
    await initializeTelemetry(mockConfig);
    expect(OTLPTraceExporter).toHaveBeenCalledWith(
      expect.objectContaining({ url: 'https://my-collector.com' }),
    );
  });

  it('should parse HTTP endpoint correctly', async () => {
    vi.spyOn(mockConfig, 'getTelemetryOtlpProtocol').mockReturnValue('http');
    vi.spyOn(mockConfig, 'getTelemetryOtlpEndpoint').mockReturnValue(
      'https://my-collector.com',
    );
    await initializeTelemetry(mockConfig);
    expect(OTLPTraceExporterHttp).toHaveBeenCalledWith(
      expect.objectContaining({ url: 'https://my-collector.com/' }),
    );
  });

  it('should use direct GCP exporters when target is gcp, project ID is set, and useCollector is false', async () => {
    mockGetApplicationDefault.mockResolvedValue(undefined); // Simulate ADC available
    vi.spyOn(mockConfig, 'getTelemetryTarget').mockReturnValue(
      TelemetryTarget.GCP,
    );
    vi.spyOn(mockConfig, 'getTelemetryUseCollector').mockReturnValue(false);
    vi.spyOn(mockConfig, 'getTelemetryOtlpEndpoint').mockReturnValue('');

    const originalEnv = process.env['OTLP_GOOGLE_CLOUD_PROJECT'];
    process.env['OTLP_GOOGLE_CLOUD_PROJECT'] = 'test-project';

    try {
      await initializeTelemetry(mockConfig);

      expect(GcpTraceExporter).toHaveBeenCalledWith('test-project', undefined);
      expect(GcpLogExporter).toHaveBeenCalledWith('test-project', undefined);
      expect(GcpMetricExporter).toHaveBeenCalledWith('test-project', undefined);
      expect(NodeSDK.prototype.start).toHaveBeenCalled();
    } finally {
      if (originalEnv) {
        process.env['OTLP_GOOGLE_CLOUD_PROJECT'] = originalEnv;
      } else {
        delete process.env['OTLP_GOOGLE_CLOUD_PROJECT'];
      }
    }
  });

  it('should use OTLP exporters when target is gcp but useCollector is true', async () => {
    vi.spyOn(mockConfig, 'getTelemetryTarget').mockReturnValue(
      TelemetryTarget.GCP,
    );
    vi.spyOn(mockConfig, 'getTelemetryUseCollector').mockReturnValue(true);

    await initializeTelemetry(mockConfig);

    expect(OTLPTraceExporter).toHaveBeenCalledWith({
      url: 'http://localhost:4317',
      compression: 'gzip',
    });
    expect(OTLPLogExporter).toHaveBeenCalledWith({
      url: 'http://localhost:4317',
      compression: 'gzip',
    });
    expect(OTLPMetricExporter).toHaveBeenCalledWith({
      url: 'http://localhost:4317',
      compression: 'gzip',
    });
  });

  it('should use GCP exporters even when project ID environment variable is not set', async () => {
    mockGetApplicationDefault.mockResolvedValue(undefined); // Simulate ADC available
    vi.spyOn(mockConfig, 'getTelemetryTarget').mockReturnValue(
      TelemetryTarget.GCP,
    );
    vi.spyOn(mockConfig, 'getTelemetryUseCollector').mockReturnValue(false);
    vi.spyOn(mockConfig, 'getTelemetryOtlpEndpoint').mockReturnValue('');

    const originalOtlpEnv = process.env['OTLP_GOOGLE_CLOUD_PROJECT'];
    const originalGoogleEnv = process.env['GOOGLE_CLOUD_PROJECT'];
    delete process.env['OTLP_GOOGLE_CLOUD_PROJECT'];
    delete process.env['GOOGLE_CLOUD_PROJECT'];

    try {
      await initializeTelemetry(mockConfig);

      expect(GcpTraceExporter).toHaveBeenCalledWith(undefined, undefined);
      expect(GcpLogExporter).toHaveBeenCalledWith(undefined, undefined);
      expect(GcpMetricExporter).toHaveBeenCalledWith(undefined, undefined);
      expect(NodeSDK.prototype.start).toHaveBeenCalled();
    } finally {
      if (originalOtlpEnv) {
        process.env['OTLP_GOOGLE_CLOUD_PROJECT'] = originalOtlpEnv;
      }
      if (originalGoogleEnv) {
        process.env['GOOGLE_CLOUD_PROJECT'] = originalGoogleEnv;
      }
    }
  });

  it('should use GOOGLE_CLOUD_PROJECT as fallback when OTLP_GOOGLE_CLOUD_PROJECT is not set', async () => {
    mockGetApplicationDefault.mockResolvedValue(undefined); // Simulate ADC available
    vi.spyOn(mockConfig, 'getTelemetryTarget').mockReturnValue(
      TelemetryTarget.GCP,
    );
    vi.spyOn(mockConfig, 'getTelemetryUseCollector').mockReturnValue(false);
    vi.spyOn(mockConfig, 'getTelemetryOtlpEndpoint').mockReturnValue('');

    const originalOtlpEnv = process.env['OTLP_GOOGLE_CLOUD_PROJECT'];
    const originalGoogleEnv = process.env['GOOGLE_CLOUD_PROJECT'];
    delete process.env['OTLP_GOOGLE_CLOUD_PROJECT'];
    process.env['GOOGLE_CLOUD_PROJECT'] = 'fallback-project';

    try {
      await initializeTelemetry(mockConfig);

      expect(GcpTraceExporter).toHaveBeenCalledWith(
        'fallback-project',
        undefined,
      );
      expect(GcpLogExporter).toHaveBeenCalledWith(
        'fallback-project',
        undefined,
      );
      expect(GcpMetricExporter).toHaveBeenCalledWith(
        'fallback-project',
        undefined,
      );
      expect(NodeSDK.prototype.start).toHaveBeenCalled();
    } finally {
      if (originalOtlpEnv) {
        process.env['OTLP_GOOGLE_CLOUD_PROJECT'] = originalOtlpEnv;
      }
      if (originalGoogleEnv) {
        process.env['GOOGLE_CLOUD_PROJECT'] = originalGoogleEnv;
      } else {
        delete process.env['GOOGLE_CLOUD_PROJECT'];
      }
    }
  });

  it('should not use OTLP exporters when telemetryOutfile is set', async () => {
    vi.spyOn(mockConfig, 'getTelemetryOutfile').mockReturnValue(
      path.join(os.tmpdir(), 'test.log'),
    );
    await initializeTelemetry(mockConfig);

    expect(OTLPTraceExporter).not.toHaveBeenCalled();
    expect(OTLPLogExporter).not.toHaveBeenCalled();
    expect(OTLPMetricExporter).not.toHaveBeenCalled();
    expect(OTLPTraceExporterHttp).not.toHaveBeenCalled();
    expect(OTLPLogExporterHttp).not.toHaveBeenCalled();
    expect(OTLPMetricExporterHttp).not.toHaveBeenCalled();
    expect(NodeSDK.prototype.start).toHaveBeenCalled();
  });

  it('should defer initialization when useCliAuth is true and no credentials are provided', async () => {
    vi.spyOn(mockConfig, 'getTelemetryUseCliAuth').mockReturnValue(true);
    vi.spyOn(mockConfig, 'getTelemetryTarget').mockReturnValue(
      TelemetryTarget.GCP,
    );
    vi.spyOn(mockConfig, 'getTelemetryOtlpEndpoint').mockReturnValue('');

    // 1. Initial state: No credentials.
    // Should NOT initialize any exporters.
    await initializeTelemetry(mockConfig);

    // Verify nothing was initialized
    expect(ConsoleSpanExporter).not.toHaveBeenCalled();
    expect(GcpTraceExporter).not.toHaveBeenCalled();

    // Verify deferral log
    expect(debugLogger.log).toHaveBeenCalledWith(
      expect.stringContaining('deferring telemetry initialization'),
    );
  });

  it('should initialize with GCP exporters when credentials are provided via post_auth', async () => {
    vi.spyOn(mockConfig, 'getTelemetryUseCliAuth').mockReturnValue(true);
    vi.spyOn(mockConfig, 'getTelemetryTarget').mockReturnValue(
      TelemetryTarget.GCP,
    );
    vi.spyOn(mockConfig, 'getTelemetryOtlpEndpoint').mockReturnValue('');

    // 1. Initial state: No credentials.
    await initializeTelemetry(mockConfig);

    // Verify nothing happened yet
    expect(GcpTraceExporter).not.toHaveBeenCalled();

    // 2. Set project ID and emit post_auth event
    process.env['GOOGLE_CLOUD_PROJECT'] = 'test-project';

    const mockCredentials = {
      client_email: 'test@example.com',
      private_key: '-----BEGIN PRIVATE KEY-----\n...',
      type: 'authorized_user',
    };

    // Emit the event directly
    authEvents.emit('post_auth', mockCredentials);

    // Wait for the event handler to process.
    await vi.waitFor(() => {
      // Check if debugLogger was called, which indicates the listener ran
      expect(debugLogger.log).toHaveBeenCalledWith(
        'Telemetry reinit with credentials.',
      );

      // Should use GCP exporters now with the project ID
      expect(GcpTraceExporter).toHaveBeenCalledWith(
        'test-project',
        mockCredentials,
      );
    });
  });

  describe('bufferTelemetryEvent', () => {
    it('should execute immediately if SDK is initialized', async () => {
      await initializeTelemetry(mockConfig);
      const callback = vi.fn();
      bufferTelemetryEvent(callback);
      expect(callback).toHaveBeenCalled();
    });

    it('should buffer if SDK is not initialized, and flush on initialization', async () => {
      const callback = vi.fn();
      bufferTelemetryEvent(callback);
      expect(callback).not.toHaveBeenCalled();

      await initializeTelemetry(mockConfig);
      expect(callback).toHaveBeenCalled();
    });
  });

  it('should disable telemetry and log error if useCollector and useCliAuth are both true', async () => {
    vi.spyOn(mockConfig, 'getTelemetryUseCollector').mockReturnValue(true);
    vi.spyOn(mockConfig, 'getTelemetryUseCliAuth').mockReturnValue(true);

    await initializeTelemetry(mockConfig);

    expect(debugLogger.error).toHaveBeenCalledWith(
      expect.stringContaining(
        'Telemetry configuration error: "useCollector" and "useCliAuth" cannot both be true',
      ),
    );
    expect(NodeSDK.prototype.start).not.toHaveBeenCalled();
  });
  it('should log error when re-initializing with different credentials', async () => {
    const creds1 = { client_email: 'user1@example.com' };
    const creds2 = { client_email: 'user2@example.com' };

    // 1. Initialize with first account
    await initializeTelemetry(mockConfig, creds1 as JWTInput);

    // 2. Attempt to initialize with second account
    await initializeTelemetry(mockConfig, creds2 as JWTInput);

    // 3. Verify error log
    expect(debugLogger.error).toHaveBeenCalledWith(
      expect.stringContaining(
        'Telemetry credentials have changed (from user1@example.com to user2@example.com)',
      ),
    );
  });

  it('should NOT log error when re-initializing with SAME credentials', async () => {
    const creds1 = { client_email: 'user1@example.com' };

    // 1. Initialize with first account
    await initializeTelemetry(mockConfig, creds1 as JWTInput);

    // 2. Attempt to initialize with same account
    await initializeTelemetry(mockConfig, creds1 as JWTInput);

    // 3. Verify NO error log
    expect(debugLogger.error).not.toHaveBeenCalledWith(
      expect.stringContaining('Telemetry credentials have changed'),
    );
  });
});


--- packages/core/src/routing/strategies/classifierStrategy.ts ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { z } from 'zod';
import type { BaseLlmClient } from '../../core/baseLlmClient.js';
import { promptIdContext } from '../../utils/promptIdContext.js';
import type {
  RoutingContext,
  RoutingDecision,
  RoutingStrategy,
} from '../routingStrategy.js';
import { resolveClassifierModel } from '../../config/models.js';
import { createUserContent, Type } from '@google/genai';
import type { Config } from '../../config/config.js';
import {
  isFunctionCall,
  isFunctionResponse,
} from '../../utils/messageInspectors.js';
import { debugLogger } from '../../utils/debugLogger.js';

// The number of recent history turns to provide to the router for context.
const HISTORY_TURNS_FOR_CONTEXT = 4;
const HISTORY_SEARCH_WINDOW = 20;

const FLASH_MODEL = 'flash';
const PRO_MODEL = 'pro';

const CLASSIFIER_SYSTEM_PROMPT = `
You are a specialized Task Routing AI. Your sole function is to analyze the user's request and classify its complexity. Choose between \`${FLASH_MODEL}\` (SIMPLE) or \`${PRO_MODEL}\` (COMPLEX).
1.  \`${FLASH_MODEL}\`: A fast, efficient model for simple, well-defined tasks.
2.  \`${PRO_MODEL}\`: A powerful, advanced model for complex, open-ended, or multi-step tasks.
<complexity_rubric>
A task is COMPLEX (Choose \`${PRO_MODEL}\`) if it meets ONE OR MORE of the following criteria:
1.  **High Operational Complexity (Est. 4+ Steps/Tool Calls):** Requires dependent actions, significant planning, or multiple coordinated changes.
2.  **Strategic Planning & Conceptual Design:** Asking "how" or "why." Requires advice, architecture, or high-level strategy.
3.  **High Ambiguity or Large Scope (Extensive Investigation):** Broadly defined requests requiring extensive investigation.
4.  **Deep Debugging & Root Cause Analysis:** Diagnosing unknown or complex problems from symptoms.
A task is SIMPLE (Choose \`${FLASH_MODEL}\`) if it is highly specific, bounded, and has Low Operational Complexity (Est. 1-3 tool calls). Operational simplicity overrides strategic phrasing.
</complexity_rubric>
**Output Format:**
Respond *only* in JSON format according to the following schema. Do not include any text outside the JSON structure.
{
  "type": "object",
  "properties": {
    "reasoning": {
      "type": "string",
      "description": "A brief, step-by-step explanation for the model choice, referencing the rubric."
    },
    "model_choice": {
      "type": "string",
      "enum": ["${FLASH_MODEL}", "${PRO_MODEL}"]
    }
  },
  "required": ["reasoning", "model_choice"]
}
--- EXAMPLES ---
**Example 1 (Strategic Planning):**
*User Prompt:* "How should I architect the data pipeline for this new analytics service?"
*Your JSON Output:*
{
  "reasoning": "The user is asking for high-level architectural design and strategy. This falls under 'Strategic Planning & Conceptual Design'.",
  "model_choice": "${PRO_MODEL}"
}
**Example 2 (Simple Tool Use):**
*User Prompt:* "list the files in the current directory"
*Your JSON Output:*
{
  "reasoning": "This is a direct command requiring a single tool call (ls). It has Low Operational Complexity (1 step).",
  "model_choice": "${FLASH_MODEL}"
}
**Example 3 (High Operational Complexity):**
*User Prompt:* "I need to add a new 'email' field to the User schema in 'src/models/user.ts', migrate the database, and update the registration endpoint."
*Your JSON Output:*
{
  "reasoning": "This request involves multiple coordinated steps across different files and systems. This meets the criteria for High Operational Complexity (4+ steps).",
  "model_choice": "${PRO_MODEL}"
}
**Example 4 (Simple Read):**
*User Prompt:* "Read the contents of 'package.json'."
*Your JSON Output:*
{
  "reasoning": "This is a direct command requiring a single read. It has Low Operational Complexity (1 step).",
  "model_choice": "${FLASH_MODEL}"
}

**Example 5 (Deep Debugging):**
*User Prompt:* "I'm getting an error 'Cannot read property 'map' of undefined' when I click the save button. Can you fix it?"
*Your JSON Output:*
{
  "reasoning": "The user is reporting an error symptom without a known cause. This requires investigation and falls under 'Deep Debugging'.",
  "model_choice": "${PRO_MODEL}"
}
**Example 6 (Simple Edit despite Phrasing):**
*User Prompt:* "What is the best way to rename the variable 'data' to 'userData' in 'src/utils.js'?"
*Your JSON Output:*
{
  "reasoning": "Although the user uses strategic language ('best way'), the underlying task is a localized edit. The operational complexity is low (1-2 steps).",
  "model_choice": "${FLASH_MODEL}"
}
`;

const RESPONSE_SCHEMA = {
  type: Type.OBJECT,
  properties: {
    reasoning: {
      type: Type.STRING,
      description:
        'A brief, step-by-step explanation for the model choice, referencing the rubric.',
    },
    model_choice: {
      type: Type.STRING,
      enum: [FLASH_MODEL, PRO_MODEL],
    },
  },
  required: ['reasoning', 'model_choice'],
};

const ClassifierResponseSchema = z.object({
  reasoning: z.string(),
  model_choice: z.enum([FLASH_MODEL, PRO_MODEL]),
});

export class ClassifierStrategy implements RoutingStrategy {
  readonly name = 'classifier';

  async route(
    context: RoutingContext,
    config: Config,
    baseLlmClient: BaseLlmClient,
  ): Promise<RoutingDecision | null> {
    const startTime = Date.now();
    try {
      let promptId = promptIdContext.getStore();
      if (!promptId) {
        promptId = `classifier-router-fallback-${Date.now()}-${Math.random()
          .toString(16)
          .slice(2)}`;
        debugLogger.warn(
          `Could not find promptId in context. This is unexpected. Using a fallback ID: ${promptId}`,
        );
      }

      const historySlice = context.history.slice(-HISTORY_SEARCH_WINDOW);

      // Filter out tool-related turns.
      // TODO - Consider using function req/res if they help accuracy.
      const cleanHistory = historySlice.filter(
        (content) => !isFunctionCall(content) && !isFunctionResponse(content),
      );

      // Take the last N turns from the *cleaned* history.
      const finalHistory = cleanHistory.slice(-HISTORY_TURNS_FOR_CONTEXT);

      const jsonResponse = await baseLlmClient.generateJson({
        modelConfigKey: { model: 'classifier' },
        contents: [...finalHistory, createUserContent(context.request)],
        schema: RESPONSE_SCHEMA,
        systemInstruction: CLASSIFIER_SYSTEM_PROMPT,
        abortSignal: context.signal,
        promptId,
      });

      const routerResponse = ClassifierResponseSchema.parse(jsonResponse);

      const reasoning = routerResponse.reasoning;
      const latencyMs = Date.now() - startTime;
      const selectedModel = resolveClassifierModel(
        context.requestedModel ?? config.getModel(),
        routerResponse.model_choice,
        config.getPreviewFeatures(),
      );

      return {
        model: selectedModel,
        metadata: {
          source: 'Classifier',
          latencyMs,
          reasoning,
        },
      };
    } catch (error) {
      // If the classifier fails for any reason (API error, parsing error, etc.),
      // we log it and return null to allow the composite strategy to proceed.
      debugLogger.warn(`[Routing] ClassifierStrategy failed:`, error);
      return null;
    }
  }
}


--- packages/core/src/routing/strategies/classifierStrategy.test.ts ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ClassifierStrategy } from './classifierStrategy.js';
import type { RoutingContext } from '../routingStrategy.js';
import type { Config } from '../../config/config.js';
import type { BaseLlmClient } from '../../core/baseLlmClient.js';
import {
  isFunctionCall,
  isFunctionResponse,
} from '../../utils/messageInspectors.js';
import {
  DEFAULT_GEMINI_FLASH_MODEL,
  DEFAULT_GEMINI_MODEL,
  DEFAULT_GEMINI_MODEL_AUTO,
} from '../../config/models.js';
import { promptIdContext } from '../../utils/promptIdContext.js';
import type { Content } from '@google/genai';
import type { ResolvedModelConfig } from '../../services/modelConfigService.js';
import { debugLogger } from '../../utils/debugLogger.js';

vi.mock('../../core/baseLlmClient.js');
vi.mock('../../utils/promptIdContext.js');

describe('ClassifierStrategy', () => {
  let strategy: ClassifierStrategy;
  let mockContext: RoutingContext;
  let mockConfig: Config;
  let mockBaseLlmClient: BaseLlmClient;
  let mockResolvedConfig: ResolvedModelConfig;

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

    strategy = new ClassifierStrategy();
    mockContext = {
      history: [],
      request: [{ text: 'simple task' }],
      signal: new AbortController().signal,
    };

    mockResolvedConfig = {
      model: 'classifier',
      generateContentConfig: {},
    } as unknown as ResolvedModelConfig;
    mockConfig = {
      modelConfigService: {
        getResolvedConfig: vi.fn().mockReturnValue(mockResolvedConfig),
      },
      getModel: () => DEFAULT_GEMINI_MODEL_AUTO,
      getPreviewFeatures: () => false,
    } as unknown as Config;
    mockBaseLlmClient = {
      generateJson: vi.fn(),
    } as unknown as BaseLlmClient;

    vi.mocked(promptIdContext.getStore).mockReturnValue('test-prompt-id');
  });

  it('should call generateJson with the correct parameters', async () => {
    const mockApiResponse = {
      reasoning: 'Simple task',
      model_choice: 'flash',
    };
    vi.mocked(mockBaseLlmClient.generateJson).mockResolvedValue(
      mockApiResponse,
    );

    await strategy.route(mockContext, mockConfig, mockBaseLlmClient);

    expect(mockBaseLlmClient.generateJson).toHaveBeenCalledWith(
      expect.objectContaining({
        modelConfigKey: { model: mockResolvedConfig.model },
        promptId: 'test-prompt-id',
      }),
    );
  });

  it('should route to FLASH model for a simple task', async () => {
    const mockApiResponse = {
      reasoning: 'This is a simple task.',
      model_choice: 'flash',
    };
    vi.mocked(mockBaseLlmClient.generateJson).mockResolvedValue(
      mockApiResponse,
    );

    const decision = await strategy.route(
      mockContext,
      mockConfig,
      mockBaseLlmClient,
    );

    expect(mockBaseLlmClient.generateJson).toHaveBeenCalledOnce();
    expect(decision).toEqual({
      model: DEFAULT_GEMINI_FLASH_MODEL,
      metadata: {
        source: 'Classifier',
        latencyMs: expect.any(Number),
        reasoning: mockApiResponse.reasoning,
      },
    });
  });

  it('should route to PRO model for a complex task', async () => {
    const mockApiResponse = {
      reasoning: 'This is a complex task.',
      model_choice: 'pro',
    };
    vi.mocked(mockBaseLlmClient.generateJson).mockResolvedValue(
      mockApiResponse,
    );
    mockContext.request = [{ text: 'how do I build a spaceship?' }];

    const decision = await strategy.route(
      mockContext,
      mockConfig,
      mockBaseLlmClient,
    );

    expect(mockBaseLlmClient.generateJson).toHaveBeenCalledOnce();
    expect(decision).toEqual({
      model: DEFAULT_GEMINI_MODEL,
      metadata: {
        source: 'Classifier',
        latencyMs: expect.any(Number),
        reasoning: mockApiResponse.reasoning,
      },
    });
  });

  it('should return null if the classifier API call fails', async () => {
    const consoleWarnSpy = vi
      .spyOn(debugLogger, 'warn')
      .mockImplementation(() => {});
    const testError = new Error('API Failure');
    vi.mocked(mockBaseLlmClient.generateJson).mockRejectedValue(testError);

    const decision = await strategy.route(
      mockContext,
      mockConfig,
      mockBaseLlmClient,
    );

    expect(decision).toBeNull();
    expect(consoleWarnSpy).toHaveBeenCalled();
    consoleWarnSpy.mockRestore();
  });

  it('should return null if the classifier returns a malformed JSON object', async () => {
    const consoleWarnSpy = vi
      .spyOn(debugLogger, 'warn')
      .mockImplementation(() => {});
    const malformedApiResponse = {
      reasoning: 'This is a simple task.',
      // model_choice is missing, which will cause a Zod parsing error.
    };
    vi.mocked(mockBaseLlmClient.generateJson).mockResolvedValue(
      malformedApiResponse,
    );

    const decision = await strategy.route(
      mockContext,
      mockConfig,
      mockBaseLlmClient,
    );

    expect(decision).toBeNull();
    expect(consoleWarnSpy).toHaveBeenCalled();
    consoleWarnSpy.mockRestore();
  });

  it('should filter out tool-related history before sending to classifier', async () => {
    mockContext.history = [
      { role: 'user', parts: [{ text: 'call a tool' }] },
      { role: 'model', parts: [{ functionCall: { name: 'test_tool' } }] },
      {
        role: 'user',
        parts: [
          { functionResponse: { name: 'test_tool', response: { ok: true } } },
        ],
      },
      { role: 'user', parts: [{ text: 'another user turn' }] },
    ];
    const mockApiResponse = {
      reasoning: 'Simple.',
      model_choice: 'flash',
    };
    vi.mocked(mockBaseLlmClient.generateJson).mockResolvedValue(
      mockApiResponse,
    );

    await strategy.route(mockContext, mockConfig, mockBaseLlmClient);

    const generateJsonCall = vi.mocked(mockBaseLlmClient.generateJson).mock
      .calls[0][0];
    const contents = generateJsonCall.contents;

    const expectedContents = [
      { role: 'user', parts: [{ text: 'call a tool' }] },
      { role: 'user', parts: [{ text: 'another user turn' }] },
      { role: 'user', parts: [{ text: 'simple task' }] },
    ];

    expect(contents).toEqual(expectedContents);
  });

  it('should respect HISTORY_SEARCH_WINDOW and HISTORY_TURNS_FOR_CONTEXT', async () => {
    const longHistory: Content[] = [];
    for (let i = 0; i < 30; i++) {
      longHistory.push({ role: 'user', parts: [{ text: `Message ${i}` }] });
      // Add noise that should be filtered
      if (i % 2 === 0) {
        longHistory.push({
          role: 'model',
          parts: [{ functionCall: { name: 'noise', args: {} } }],
        });
      }
    }
    mockContext.history = longHistory;
    const mockApiResponse = {
      reasoning: 'Simple.',
      model_choice: 'flash',
    };
    vi.mocked(mockBaseLlmClient.generateJson).mockResolvedValue(
      mockApiResponse,
    );

    await strategy.route(mockContext, mockConfig, mockBaseLlmClient);

    const generateJsonCall = vi.mocked(mockBaseLlmClient.generateJson).mock
      .calls[0][0];
    const contents = generateJsonCall.contents;

    // Manually calculate what the history should be
    const HISTORY_SEARCH_WINDOW = 20;
    const HISTORY_TURNS_FOR_CONTEXT = 4;
    const historySlice = longHistory.slice(-HISTORY_SEARCH_WINDOW);
    const cleanHistory = historySlice.filter(
      (content) => !isFunctionCall(content) && !isFunctionResponse(content),
    );
    const finalHistory = cleanHistory.slice(-HISTORY_TURNS_FOR_CONTEXT);

    expect(contents).toEqual([
      ...finalHistory,
      { role: 'user', parts: mockContext.request },
    ]);
    // There should be 4 history items + the current request
    expect(contents).toHaveLength(5);
  });

  it('should use a fallback promptId if not found in context', async () => {
    const consoleWarnSpy = vi
      .spyOn(debugLogger, 'warn')
      .mockImplementation(() => {});
    vi.mocked(promptIdContext.getStore).mockReturnValue(undefined);
    const mockApiResponse = {
      reasoning: 'Simple.',
      model_choice: 'flash',
    };
    vi.mocked(mockBaseLlmClient.generateJson).mockResolvedValue(
      mockApiResponse,
    );

    await strategy.route(mockContext, mockConfig, mockBaseLlmClient);

    const generateJsonCall = vi.mocked(mockBaseLlmClient.generateJson).mock
      .calls[0][0];

    expect(generateJsonCall.promptId).toMatch(
      /^classifier-router-fallback-\d+-\w+$/,
    );
    expect(consoleWarnSpy).toHaveBeenCalledWith(
      expect.stringContaining(
        'Could not find promptId in context. This is unexpected. Using a fallback ID:',
      ),
    );
    consoleWarnSpy.mockRestore();
  });

  it('should respect requestedModel from context in resolveClassifierModel', async () => {
    const requestedModel = DEFAULT_GEMINI_MODEL; // Pro model
    const mockApiResponse = {
      reasoning: 'Choice is flash',
      model_choice: 'flash',
    };
    vi.mocked(mockBaseLlmClient.generateJson).mockResolvedValue(
      mockApiResponse,
    );

    const contextWithRequestedModel = {
      ...mockContext,
      requestedModel,
    } as RoutingContext;

    const decision = await strategy.route(
      contextWithRequestedModel,
      mockConfig,
      mockBaseLlmClient,
    );

    expect(decision).not.toBeNull();
    // Since requestedModel is Pro, and choice is flash, it should resolve to Flash
    expect(decision?.model).toBe(DEFAULT_GEMINI_FLASH_MODEL);
  });
});


--- CONTRIBUTING.md ---
# How to contribute

We would love to accept your patches and contributions to this project. This
document includes:

- **[Before you begin](#before-you-begin):** Essential steps to take before
  becoming a Gemini CLI contributor.
- **[Code contribution process](#code-contribution-process):** How to contribute
  code to Gemini CLI.
- **[Development setup and workflow](#development-setup-and-workflow):** How to
  set up your development environment and workflow.
- **[Documentation contribution process](#documentation-contribution-process):**
  How to contribute documentation to Gemini CLI.

We're looking forward to seeing your contributions!

## 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/).

## Code contribution process

### Get started

The process for contributing code is as follows:

1.  **Find an issue** that you want to work on. If an issue is tagged as
    "🔒Maintainers only", this means it is reserved for project maintainers. We
    will not accept pull requests related to these issues.
2.  **Fork the repository** and create a new branch.
3.  **Make your changes** in the `packages/` directory.
4.  **Ensure all checks pass** by running `npm run preflight`.
5.  **Open a pull request** with your changes.

### 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.

If your pull request involves changes to `packages/cli` (the frontend), we
recommend running our automated frontend review tool. **Note: This tool is
currently experimental.** It helps detect common React anti-patterns, testing
issues, and other frontend-specific best practices that are easy to miss.

To run the review tool, enter the following command from within Gemini CLI:

```text
/review-frontend <PR_NUMBER>
```

Replace `<PR_NUMBER>` with your pull request number. Authors are encouraged to
run this on their own PRs for self-review, and reviewers should use it to
augment their manual review process.

### Self assigning issues

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 `npm run preflight`. This command runs all tests, linting, and other
style checks.

#### 5. Update documentation

If your PR introduces a user-facing change (e.g., a new command, a modified
flag, or a change in behavior), you must also update the relevant documentation
in the `/docs` directory.

See more about writing documentation:
[Documentation contribution process](#documentation-contribution-process).

#### 6. 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`).

### Forking

If you are forking the repository you will be able to run the Build, Test and
Integration test workflows. However in order to make the integration tests run
you'll need to add a
[GitHub Repository Secret](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-a-repository)
with a value of `GEMINI_API_KEY` and set that to a valid API key that you have
available. Your key and secret are private to your repo; no one without access
can see your key and you cannot see any secrets related to this repo.

Additionally you will need to click on the `Actions` tab and enable workflows
for your repository, you'll find it's the large blue button in the center of the
screen.

### 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/google-gemini/gemini-cli.git # Or your fork's URL
cd gemini-cli
```

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.

### Enabling sandboxing

[Sandboxing](#sandboxing) is highly recommended and requires, at a minimum,
setting `GEMINI_SANDBOX=true` in your `~/.env` and ensuring a sandboxing
provider (e.g. `macOS Seatbelt`, `docker`, or `podman`) is available. See
[Sandboxing](#sandboxing) for details.

To build both the `gemini` CLI utility and the sandbox container, run
`build:all` from the root directory:

```bash
npm run build:all
```

To skip building the sandbox container, you can use `npm run build` instead.

### Running the CLI

To start the Gemini CLI from the source code (after building), run the following
command from the root directory:

```bash
npm start
```

If you'd like to run the source build outside of the gemini-cli folder, you can
utilize `npm link path/to/gemini-cli/packages/cli` (see:
[docs](https://docs.npmjs.com/cli/v9/commands/npm-link)) or
`alias gemini="node path/to/gemini-cli/packages/cli"` to run with `gemini`

### Running tests

This project contains two types of tests: unit tests and integration tests.

#### Unit tests

To execute the unit test suite for the project:

```bash
npm run test
```

This will run tests located in the `packages/core` and `packages/cli`
directories. Ensure tests pass before submitting any changes. For a more
comprehensive check, it is recommended to run `npm run preflight`.

#### Integration tests

The integration tests are designed to validate the end-to-end functionality of
the Gemini CLI. They are not run as part of the default `npm run test` command.

To run the integration tests, use the following command:

```bash
npm run test:e2e
```

For more detailed information on the integration testing framework, please see
the [Integration Tests documentation](/docs/integration-tests.md).

### Linting and preflight checks

To ensure code quality and formatting consistency, run the preflight check:

```bash
npm run preflight
```

This command will run ESLint, Prettier, all tests, and other checks as defined
in the project's `package.json`.

_ProTip_

after cloning create a git precommit hook file to ensure your commits are always
clean.

```bash
echo "
# Run npm build and check for errors
if ! npm run preflight; then
  echo "npm build failed. Commit aborted."
  exit 1
fi
" > .git/hooks/pre-commit && 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
```

### Coding conventions

- Please adhere to the coding style, patterns, and conventions used throughout
  the existing codebase.
- Consult
  [GEMINI.md](https://github.com/google-gemini/gemini-cli/blob/main/GEMINI.md)
  (typically found in the project root) for specific instructions related to
  AI-assisted development, including conventions for React, comments, and Git
  usage.
- **Imports:** Pay special attention to import paths. The project uses ESLint to
  enforce restrictions on relative imports between packages.

### Project structure

- `packages/`: Contains the individual sub-packages of the project.
  - `a2a-server`: A2A server implementation for the Gemini CLI. (Experimental)
  - `cli/`: The command-line interface.
  - `core/`: The core backend logic for the Gemini CLI.
  - `test-utils` Utilities for creating and cleaning temporary file systems for
    testing.
  - `vscode-ide-companion/`: The Gemini CLI Companion extension pairs with
    Gemini CLI.
- `docs/`: Contains all project documentation.
- `scripts/`: Utility scripts for building, testing, and development tasks.

For more detailed architecture, see `docs/architecture.md`.

### Debugging

#### VS Code

0.  Run the CLI to interactively debug in VS Code with `F5`
1.  Start the CLI in debug mode from the root directory:
    ```bash
    npm run debug
    ```
    This command runs `node --inspect-brk dist/gemini.js` within the
    `packages/cli` directory, pausing execution until a debugger attaches. You
    can then open `chrome://inspect` in your Chrome browser to connect to the
    debugger.
2.  In VS Code, use the "Attach" launch configuration (found in
    `.vscode/launch.json`).

Alternatively, you can use the "Launch Program" configuration in VS Code if you
prefer to launch the currently open file directly, but 'F5' is generally
recommended.

To hit a breakpoint inside the sandbox container run:

```bash
DEBUG=1 gemini
```

**Note:** If you have `DEBUG=true` in a project's `.env` file, it won't affect
gemini-cli due to automatic exclusion. Use `.gemini/.env` files for gemini-cli
specific debug settings.

### React DevTools

To debug the CLI's React-based UI, you can use React DevTools. Ink, the library
used for the CLI's interface, is compatible with React DevTools version 4.x.

1.  **Start the Gemini CLI in development mode:**

    ```bash
    DEV=true npm start
    ```

2.  **Install and run React DevTools version 4.28.5 (or the latest compatible
    4.x version):**

    You can either install it globally:

    ```bash
    npm install -g react-devtools@4.28.5
    react-devtools
    ```

    Or run it directly using npx:

    ```bash
    npx react-devtools@4.28.5
    ```

    Your running CLI application should then connect to React DevTools.
    ![](/docs/assets/connected_devtools.png)

### Sandboxing

#### macOS Seatbelt

On macOS, `gemini` uses Seatbelt (`sandbox-exec`) under a `permissive-open`
profile (see `packages/cli/src/utils/sandbox-macos-permissive-open.sb`) that
restricts writes to the project folder but otherwise allows all other operations
and outbound network traffic ("open") by default. You can switch to a
`restrictive-closed` profile (see
`packages/cli/src/utils/sandbox-macos-restrictive-closed.sb`) that declines all
operations and outbound network traffic ("closed") by default by setting
`SEATBELT_PROFILE=restrictive-closed` in your environment or `.env` file.
Available built-in profiles are `{permissive,restrictive}-{open,closed,proxied}`
(see below for proxied networking). You can also switch to a custom profile
`SEATBELT_PROFILE=<profile>` if you also create a file
`.gemini/sandbox-macos-<profile>.sb` under your project settings directory
`.gemini`.

#### Container-based sandboxing (all platforms)

For stronger container-based sandboxing on macOS or other platforms, you can set
`GEMINI_SANDBOX=true|docker|podman|<command>` in your environment or `.env`
file. The specified command (or if `true` then either `docker` or `podman`) must
be installed on the host machine. Once enabled, `npm run build:all` will build a
minimal container ("sandbox") image and `npm start` will launch inside a fresh
instance of that container. The first build can take 20-30s (mostly due to
downloading of the base image) but after that both build and start overhead
should be minimal. Default builds (`npm run build`) will not rebuild the
sandbox.

Container-based sandboxing mounts the project directory (and system temp
directory) with read-write access and is started/stopped/removed automatically
as you start/stop Gemini CLI. Files created within the sandbox should be
automatically mapped to your user/group on host machine. You can easily specify
additional mounts, ports, or environment variables by setting
`SANDBOX_{MOUNTS,PORTS,ENV}` as needed. You can also fully customize the sandbox
for your projects by creating the files `.gemini/sandbox.Dockerfile` and/or
`.gemini/sandbox.bashrc` under your project settings directory (`.gemini`) and
running `gemini` with `BUILD_SANDBOX=1` to trigger building of your custom
sandbox.

#### Proxied networking

All sandboxing methods, including macOS Seatbelt using `*-proxied` profiles,
support restricting outbound network traffic through a custom proxy server that
can be specified as `GEMINI_SANDBOX_PROXY_COMMAND=<command>`, where `<command>`
must start a proxy server that listens on `:::8877` for relevant requests. See
`docs/examples/proxy-script.md` for a minimal proxy that only allows `HTTPS`
connections to `example.com:443` (e.g. `curl https://example.com`) and declines
all other requests. The proxy is started and stopped automatically alongside the
sandbox.

### Manual publish

We publish an artifact for each commit to our internal registry. But if you need
to manually cut a local build, then run the following commands:

```
npm run clean
npm install
npm run auth
npm run prerelease:dev
npm publish --workspaces
```

## Documentation contribution process

Our documentation must be kept up-to-date with our code contributions. We want
our documentation to be clear, concise, and helpful to our users. We value:

- **Clarity:** Use simple and direct language. Avoid jargon where possible.
- **Accuracy:** Ensure all information is correct and up-to-date.
- **Completeness:** Cover all aspects of a feature or topic.
- **Examples:** Provide practical examples to help users understand how to use
  Gemini CLI.

### Getting started

The process for contributing to the documentation is similar to contributing
code.

1. **Fork the repository** and create a new branch.
2. **Make your changes** in the `/docs` directory.
3. **Preview your changes locally** in Markdown rendering.
4. **Lint and format your changes.** Our preflight check includes linting and
   formatting for documentation files.
   ```bash
   npm run preflight
   ```
5. **Open a pull request** with your changes.

### Documentation structure

Our documentation is organized using [sidebar.json](/docs/sidebar.json) as the
table of contents. When adding new documentation:

1. Create your markdown file **in the appropriate directory** under `/docs`.
2. Add an entry to `sidebar.json` in the relevant section.
3. Ensure all internal links use relative paths and point to existing files.

### Style guide

We follow the
[Google Developer Documentation Style Guide](https://developers.google.com/style).
Please refer to it for guidance on writing style, tone, and formatting.

#### Key style points

- Use sentence case for headings.
- Write in second person ("you") when addressing the reader.
- Use present tense.
- Keep paragraphs short and focused.
- Use code blocks with appropriate language tags for syntax highlighting.
- Include practical examples whenever possible.

### Linting and formatting

We use `prettier` to enforce a consistent style across our documentation. The
`npm run preflight` command will check for any linting issues.

You can also run the linter and formatter separately:

- `npm run lint` - Check for linting issues
- `npm run format` - Auto-format markdown files
- `npm run lint:fix` - Auto-fix linting issues where possible

Please make sure your contributions are free of linting errors before submitting
a pull request.

### Before you submit

Before submitting your documentation pull request, please:

1. Run `npm run preflight` to ensure all checks pass.
2. Review your changes for clarity and accuracy.
3. Check that all links work correctly.
4. Ensure any code examples are tested and functional.
5. Sign the
   [Contributor License Agreement (CLA)](https://cla.developers.google.com/) if
   you haven't already.

### Need help?

If you have questions about contributing documentation:

- Check our [FAQ](/docs/faq.md).
- Review existing documentation for examples.
- Open [an issue](https://github.com/google-gemini/gemini-cli/issues) to discuss
  your proposed changes.
- Reach out to the maintainers.

We appreciate your contributions to making Gemini CLI documentation better!


## Links discovered
- [Contributor License Agreement](https://cla.developers.google.com/about)
- [Google's Open Source Community Guidelines](https://opensource.google/conduct/)
- [GitHub pull requests](https://docs.github.com/articles/about-pull-requests)
- [Conventional Commits](https://www.conventionalcommits.org/)
- [GitHub Repository Secret](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-a-repository)
- [nvm](https://github.com/nvm-sh/nvm)
- [docs](https://docs.npmjs.com/cli/v9/commands/npm-link)
- [Integration Tests documentation](https://github.com/google-gemini/gemini-cli/blob/main/docs/integration-tests.md)
- [GEMINI.md](https://github.com/google-gemini/gemini-cli/blob/main/GEMINI.md)
- [sidebar.json](https://github.com/google-gemini/gemini-cli/blob/main/docs/sidebar.json)
- [Google Developer Documentation Style Guide](https://developers.google.com/style)
- [Contributor License Agreement (CLA)](https://cla.developers.google.com/)
- [FAQ](https://github.com/google-gemini/gemini-cli/blob/main/docs/faq.md)
- [an issue](https://github.com/google-gemini/gemini-cli/issues)

--- 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.

[GitHub Security Advisory]:
  https://github.com/google-gemini/gemini-cli/security/advisories


## Links discovered
- [https://g.co/vulnz](https://g.co/vulnz)

--- scripts/get-release-version.js ---
#!/usr/bin/env node

/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { execSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { readFileSync } from 'node:fs';
import semver from 'semver';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';

const TAG_LATEST = 'latest';
const TAG_NIGHTLY = 'nightly';
const TAG_PREVIEW = 'preview';

function readJson(filePath) {
  return JSON.parse(readFileSync(filePath, 'utf-8'));
}

function getArgs() {
  return yargs(hideBin(process.argv))
    .option('type', {
      description: 'The type of release to generate a version for.',
      choices: [TAG_NIGHTLY, 'promote-nightly', 'stable', TAG_PREVIEW, 'patch'],
      default: TAG_NIGHTLY,
    })
    .option('patch-from', {
      description: 'When type is "patch", specifies the source branch.',
      choices: ['stable', TAG_PREVIEW],
      string: true,
    })
    .option('stable_version_override', {
      description: 'Override the calculated stable version.',
      string: true,
    })
    .option('cli-package-name', {
      description:
        'fully qualified package name with scope (e.g @google/gemini-cli)',
      string: true,
      default: '@google/gemini-cli',
    })
    .option('preview_version_override', {
      description: 'Override the calculated preview version.',
      string: true,
    })
    .option('stable-base-version', {
      description: 'Base version to use for calculating next preview/nightly.',
      string: true,
    })
    .help(false)
    .version(false)
    .parse();
}

function getLatestTag(pattern) {
  const command = `git tag -l '${pattern}'`;
  try {
    const tags = execSync(command)
      .toString()
      .trim()
      .split('\n')
      .filter(Boolean);
    if (tags.length === 0) return '';

    // Convert tags to versions (remove 'v' prefix) and sort by semver
    const versions = tags
      .map((tag) => tag.replace(/^v/, ''))
      .filter((version) => semver.valid(version))
      .sort((a, b) => semver.rcompare(a, b)); // rcompare for descending order

    if (versions.length === 0) return '';

    // Return the latest version with 'v' prefix restored
    return `v${versions[0]}`;
  } catch (error) {
    console.error(
      `Failed to get latest git tag for pattern "${pattern}": ${error.message}`,
    );
    return '';
  }
}

function getVersionFromNPM({ args, npmDistTag } = {}) {
  const command = `npm view ${args['cli-package-name']} version --tag=${npmDistTag}`;
  try {
    return execSync(command).toString().trim();
  } catch (error) {
    console.error(
      `Failed to get NPM version for dist-tag "${npmDistTag}": ${error.message}`,
    );
    return '';
  }
}

function getAllVersionsFromNPM({ args } = {}) {
  const command = `npm view ${args['cli-package-name']} versions --json`;
  try {
    const versionsJson = execSync(command).toString().trim();
    return JSON.parse(versionsJson);
  } catch (error) {
    console.error(`Failed to get all NPM versions: ${error.message}`);
    return [];
  }
}

function isVersionDeprecated({ args, version } = {}) {
  const command = `npm view ${args['cli-package-name']}@${version} deprecated`;
  try {
    const output = execSync(command).toString().trim();
    return output.length > 0;
  } catch (error) {
    // This command shouldn't fail for existing versions, but as a safeguard:
    console.error(
      `Failed to check deprecation status for ${version}: ${error.message}`,
    );
    return false; // Assume not deprecated on error to avoid breaking the release.
  }
}

function detectRollbackAndGetBaseline({ args, npmDistTag } = {}) {
  // Get the current dist-tag version
  const distTagVersion = getVersionFromNPM({ args, npmDistTag });
  if (!distTagVersion) return { baseline: '', isRollback: false };

  // Get all published versions
  const allVersions = getAllVersionsFromNPM({ args });
  if (allVersions.length === 0)
    return { baseline: distTagVersion, isRollback: false };

  // Filter versions by type to match the dist-tag
  let matchingVersions;
  if (npmDistTag === TAG_LATEST) {
    // Stable versions: no prerelease identifiers
    matchingVersions = allVersions.filter(
      (v) => semver.valid(v) && !semver.prerelease(v),
    );
  } else if (npmDistTag === TAG_PREVIEW) {
    // Preview versions: contain -preview
    matchingVersions = allVersions.filter(
      (v) => semver.valid(v) && v.includes('-preview'),
    );
  } else if (npmDistTag === TAG_NIGHTLY) {
    // Nightly versions: contain -nightly
    matchingVersions = allVersions.filter(
      (v) => semver.valid(v) && v.includes('-nightly'),
    );
  } else {
    // For other dist-tags, just use the dist-tag version
    return { baseline: distTagVersion, isRollback: false };
  }

  if (matchingVersions.length === 0)
    return { baseline: distTagVersion, isRollback: false };

  // Sort by semver to get a list from highest to lowest
  matchingVersions.sort((a, b) => semver.rcompare(a, b));

  // Find the highest non-deprecated version
  let highestExistingVersion = '';
  for (const version of matchingVersions) {
    if (!isVersionDeprecated({ version, args })) {
      highestExistingVersion = version;
      break; // Found the one we want
    } else {
      console.error(`Ignoring deprecated version: ${version}`);
    }
  }

  // If all matching versions were deprecated, fall back to the dist-tag version
  if (!highestExistingVersion) {
    highestExistingVersion = distTagVersion;
  }

  // Check if we're in a rollback scenario
  const isRollback = semver.gt(highestExistingVersion, distTagVersion);

  return {
    baseline: isRollback ? highestExistingVersion : distTagVersion,
    isRollback,
    distTagVersion,
    highestExistingVersion,
  };
}

function doesVersionExist({ args, version } = {}) {
  // Check NPM
  try {
    const command = `npm view ${args['cli-package-name']}@${version} version 2>/dev/null`;
    const output = execSync(command).toString().trim();
    if (output === version) {
      console.error(`Version ${version} already exists on NPM.`);
      return true;
    }
  } catch (_error) {
    // This is expected if the version doesn't exist.
  }

  // Check Git tags
  try {
    const command = `git tag -l 'v${version}'`;
    const tagOutput = execSync(command).toString().trim();
    if (tagOutput === `v${version}`) {
      console.error(`Git tag v${version} already exists.`);
      return true;
    }
  } catch (error) {
    console.error(`Failed to check git tags for conflicts: ${error.message}`);
  }

  // Check GitHub releases
  try {
    const command = `gh release view "v${version}" --json tagName --jq .tagName 2>/dev/null`;
    const output = execSync(command).toString().trim();
    if (output === `v${version}`) {
      console.error(`GitHub release v${version} already exists.`);
      return true;
    }
  } catch (error) {
    const isExpectedNotFound =
      error.message.includes('release not found') ||
      error.message.includes('Not Found') ||
      error.message.includes('not found') ||
      error.status === 1;
    if (!isExpectedNotFound) {
      console.error(
        `Failed to check GitHub releases for conflicts: ${error.message}`,
      );
    }
  }

  return false;
}

function getAndVerifyTags({ npmDistTag, args } = {}) {
  // Detect rollback scenarios and get the correct baseline
  const rollbackInfo = detectRollbackAndGetBaseline({ args, npmDistTag });
  const baselineVersion = rollbackInfo.baseline;

  if (!baselineVersion) {
    throw new Error(`Unable to determine baseline version for ${npmDistTag}`);
  }

  if (rollbackInfo.isRollback) {
    // Rollback scenario: warn about the rollback but don't fail
    console.error(
      `Rollback detected! NPM ${npmDistTag} tag is ${rollbackInfo.distTagVersion}, but using ${baselineVersion} as baseline for next version calculation (highest existing version).`,
    );
  }

  // Not verifying against git tags or GitHub releases as per user request.

  return {
    latestVersion: baselineVersion,
    latestTag: `v${baselineVersion}`,
  };
}

function getStableBaseVersion(args) {
  let latestStableVersion = args['stable-base-version'];
  if (!latestStableVersion) {
    const { latestVersion } = getAndVerifyTags({
      npmDistTag: TAG_LATEST,
      args,
    });
    latestStableVersion = latestVersion;
  }
  return latestStableVersion;
}

function promoteNightlyVersion({ args } = {}) {
  const latestStableVersion = getStableBaseVersion(args);

  const { latestTag: previousNightlyTag } = getAndVerifyTags({
    npmDistTag: TAG_NIGHTLY,
    args,
  });

  const major = semver.major(latestStableVersion);
  const minor = semver.minor(latestStableVersion);
  const nextMinor = minor + 2;
  const date = new Date().toISOString().slice(0, 10).replace(/-/g, '');
  const gitShortHash = execSync('git rev-parse --short HEAD').toString().trim();
  return {
    releaseVersion: `${major}.${nextMinor}.0-nightly.${date}.${gitShortHash}`,
    npmTag: TAG_NIGHTLY,
    previousReleaseTag: previousNightlyTag,
  };
}

function getNightlyVersion() {
  const packageJson = readJson('package.json');
  const baseVersion = packageJson.version.split('-')[0];
  const date = new Date().toISOString().slice(0, 10).replace(/-/g, '');
  const gitShortHash = execSync('git rev-parse --short HEAD').toString().trim();
  const releaseVersion = `${baseVersion}-nightly.${date}.${gitShortHash}`;
  const previousReleaseTag = getLatestTag('v*-nightly*');

  return {
    releaseVersion,
    npmTag: TAG_NIGHTLY,
    previousReleaseTag,
  };
}

function validateVersion(version, format, name) {
  const versionRegex = {
    'X.Y.Z': /^\d+\.\d+\.\d+$/,
    'X.Y.Z-preview.N': /^\d+\.\d+\.\d+-preview\.\d+$/,
  };

  if (!versionRegex[format] || !versionRegex[format].test(version)) {
    throw new Error(
      `Invalid ${name}: ${version}. Must be in ${format} format.`,
    );
  }
}

function getStableVersion(args) {
  const { latestVersion: latestPreviewVersion } = getAndVerifyTags({
    npmDistTag: TAG_PREVIEW,
    args,
  });
  let releaseVersion;
  if (args['stable_version_override']) {
    const overrideVersion = args['stable_version_override'].replace(/^v/, '');
    validateVersion(overrideVersion, 'X.Y.Z', 'stable_version_override');
    releaseVersion = overrideVersion;
  } else {
    releaseVersion = latestPreviewVersion.replace(/-preview.*/, '');
  }

  const { latestTag: previousStableTag } = getAndVerifyTags({
    npmDistTag: TAG_LATEST,
    args,
  });

  return {
    releaseVersion,
    npmTag: TAG_LATEST,
    previousReleaseTag: previousStableTag,
  };
}

function getPreviewVersion(args) {
  const latestStableVersion = getStableBaseVersion(args);

  let releaseVersion;
  if (args['preview_version_override']) {
    const overrideVersion = args['preview_version_override'].replace(/^v/, '');
    validateVersion(
      overrideVersion,
      'X.Y.Z-preview.N',
      'preview_version_override',
    );
    releaseVersion = overrideVersion;
  } else {
    const major = semver.major(latestStableVersion);
    const minor = semver.minor(latestStableVersion);
    const nextMinor = minor + 1;
    releaseVersion = `${major}.${nextMinor}.0-preview.0`;
  }

  const { latestTag: previousPreviewTag } = getAndVerifyTags({
    npmDistTag: TAG_PREVIEW,
    args,
  });

  return {
    releaseVersion,
    npmTag: TAG_PREVIEW,
    previousReleaseTag: previousPreviewTag,
  };
}

function getPatchVersion(args) {
  const patchFrom = args['patch-from'];
  if (!patchFrom || (patchFrom !== 'stable' && patchFrom !== TAG_PREVIEW)) {
    throw new Error(
      'Patch type must be specified with --patch-from=stable or --patch-from=preview',
    );
  }
  const distTag = patchFrom === 'stable' ? TAG_LATEST : TAG_PREVIEW;
  const { latestVersion, latestTag } = getAndVerifyTags({
    npmDistTag: distTag,
    args,
  });

  if (patchFrom === 'stable') {
    // For stable versions, increment the patch number: 0.5.4 -> 0.5.5
    const versionParts = latestVersion.split('.');
    const major = versionParts[0];
    const minor = versionParts[1];
    const patch = versionParts[2] ? parseInt(versionParts[2]) : 0;
    const releaseVersion = `${major}.${minor}.${patch + 1}`;
    return {
      releaseVersion,
      npmTag: distTag,
      previousReleaseTag: latestTag,
    };
  } else {
    // For preview versions, increment the preview number: 0.6.0-preview.2 -> 0.6.0-preview.3
    const [version, prereleasePart] = latestVersion.split('-');
    if (!prereleasePart || !prereleasePart.startsWith('preview.')) {
      throw new Error(
        `Invalid preview version format: ${latestVersion}. Expected format like "0.6.0-preview.2"`,
      );
    }

    const previewNumber = parseInt(prereleasePart.split('.')[1]);
    if (isNaN(previewNumber)) {
      throw new Error(`Could not parse preview number from: ${prereleasePart}`);
    }

    const releaseVersion = `${version}-preview.${previewNumber + 1}`;
    return {
      releaseVersion,
      npmTag: distTag,
      previousReleaseTag: latestTag,
    };
  }
}

export function getVersion(options = {}) {
  const args = { ...getArgs(), ...options };
  const type = args['type'] || TAG_NIGHTLY; // Nightly is the default.

  let versionData;
  switch (type) {
    case TAG_NIGHTLY:
      versionData = getNightlyVersion();
      // Nightly versions include a git hash, so conflicts are highly unlikely
      // and indicate a problem. We'll still validate but not auto-increment.
      if (doesVersionExist({ args, version: versionData.releaseVersion })) {
        throw new Error(
          `Version conflict! Nightly version ${versionData.releaseVersion} already exists.`,
        );
      }
      break;
    case 'promote-nightly':
      versionData = promoteNightlyVersion({ args });
      // A promoted nightly version is still a nightly, so we should check for conflicts.
      if (doesVersionExist({ args, version: versionData.releaseVersion })) {
        throw new Error(
          `Version conflict! Promoted nightly version ${versionData.releaseVersion} already exists.`,
        );
      }
      break;
    case 'stable':
      versionData = getStableVersion(args);
      break;
    case TAG_PREVIEW:
      versionData = getPreviewVersion(args);
      break;
    case 'patch':
      versionData = getPatchVersion(args);
      break;
    default:
      throw new Error(`Unknown release type: ${type}`);
  }

  // For patchable versions, check for existence and increment if needed.
  if (type === 'stable' || type === TAG_PREVIEW || type === 'patch') {
    let releaseVersion = versionData.releaseVersion;
    while (doesVersionExist({ args, version: releaseVersion })) {
      console.error(`Version ${releaseVersion} exists, incrementing.`);
      if (releaseVersion.includes('-preview.')) {
        // Increment preview number: 0.6.0-preview.2 -> 0.6.0-preview.3
        const [version, prereleasePart] = releaseVersion.split('-');
        const previewNumber = parseInt(prereleasePart.split('.')[1]);
        releaseVersion = `${version}-preview.${previewNumber + 1}`;
      } else {
        // Increment patch number: 0.5.4 -> 0.5.5
        const versionParts = releaseVersion.split('.');
        const major = versionParts[0];
        const minor = versionParts[1];
        const patch = parseInt(versionParts[2]);
        releaseVersion = `${major}.${minor}.${patch + 1}`;
      }
    }
    versionData.releaseVersion = releaseVersion;
  }

  // All checks are done, construct the final result.
  const result = {
    releaseTag: `v${versionData.releaseVersion}`,
    ...versionData,
  };

  return result;
}

if (process.argv[1] === fileURLToPath(import.meta.url)) {
  console.log(JSON.stringify(getVersion(getArgs()), null, 2));
}


--- scripts/prepare-github-release.js ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import fs from 'node:fs';
import path from 'node:path';

const rootDir = process.cwd();

function updatePackageJson(packagePath, updateFn) {
  const packageJsonPath = path.resolve(rootDir, packagePath);
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
  updateFn(packageJson);
  fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
}

// Copy bundle directory into packages/cli
const sourceBundleDir = path.resolve(rootDir, 'bundle');
const destBundleDir = path.resolve(rootDir, 'packages/cli/bundle');

if (fs.existsSync(sourceBundleDir)) {
  fs.rmSync(destBundleDir, { recursive: true, force: true });
  fs.cpSync(sourceBundleDir, destBundleDir, { recursive: true });
  console.log('Copied bundle/ directory to packages/cli/');
} else {
  console.error(
    'Error: bundle/ directory not found at project root. Please run `npm run bundle` first.',
  );
  process.exit(1);
}

// Overwrite the .npmrc in the core package to point to the GitHub registry.
const coreNpmrcPath = path.resolve(rootDir, 'packages/core/.npmrc');
fs.writeFileSync(
  coreNpmrcPath,
  '@google-gemini:registry=https://npm.pkg.github.com/',
);
console.log('Wrote .npmrc for @google-gemini scope to packages/core/');

// Update @google/gemini-cli
updatePackageJson('packages/cli/package.json', (pkg) => {
  pkg.name = '@google-gemini/gemini-cli';
  pkg.files = ['bundle/'];
  pkg.bin = {
    gemini: 'bundle/gemini.js',
  };

  // Remove fields that are not relevant to the bundled package.
  delete pkg.dependencies;
  delete pkg.devDependencies;
  delete pkg.scripts;
  delete pkg.main;
  delete pkg.config; // Deletes the sandboxImageUri
});

// Update @google/gemini-cli-a2a-server
updatePackageJson('packages/a2a-server/package.json', (pkg) => {
  pkg.name = '@google-gemini/gemini-cli-a2a-server';
});

// Update @google/gemini-cli-core
updatePackageJson('packages/core/package.json', (pkg) => {
  pkg.name = '@google-gemini/gemini-cli-core';
});

console.log('Successfully prepared packages for GitHub release.');


--- scripts/tests/get-release-version.test.js ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { vi, describe, it, expect, beforeEach } from 'vitest';
import { getVersion } from '../get-release-version.js';
import { execSync } from 'node:child_process';
import { readFileSync } from 'node:fs';

vi.mock('node:child_process');
vi.mock('node:fs');

describe('getVersion', () => {
  beforeEach(() => {
    vi.resetAllMocks();
    vi.setSystemTime(new Date('2025-09-17T00:00:00.000Z'));
    // Mock package.json being read by getNightlyVersion
    vi.mocked(readFileSync).mockReturnValue(
      JSON.stringify({ version: '0.8.0' }),
    );
  });

  // This is the base mock for a clean state with no conflicts or rollbacks
  const mockExecSync = (command) => {
    // NPM dist-tags
    if (command.includes('npm view') && command.includes('--tag=latest'))
      return '0.6.1';
    if (command.includes('npm view') && command.includes('--tag=preview'))
      return '0.7.0-preview.1';
    if (command.includes('npm view') && command.includes('--tag=nightly'))
      return '0.8.0-nightly.20250916.abcdef';

    // NPM versions list
    if (command.includes('npm view') && command.includes('versions --json'))
      return JSON.stringify([
        '0.6.0',
        '0.6.1',
        '0.7.0-preview.0',
        '0.7.0-preview.1',
        '0.8.0-nightly.20250916.abcdef',
      ]);

    // Deprecation checks (default to not deprecated)
    if (command.includes('deprecated')) return '';

    // Git Tag Mocks
    if (command.includes("git tag -l 'v[0-9].[0-9].[0-9]'")) return 'v0.6.1';
    if (command.includes("git tag -l 'v*-preview*'")) return 'v0.7.0-preview.1';
    if (command.includes("git tag -l 'v*-nightly*'"))
      return 'v0.8.0-nightly.20250916.abcdef';

    // Git Hash Mock
    if (command.includes('git rev-parse --short HEAD')) return 'd3bf8a3d';

    // For doesVersionExist checks - default to not found
    if (
      command.includes('npm view') &&
      command.includes('@google/gemini-cli@')
    ) {
      throw new Error('NPM version not found');
    }
    if (command.includes('git tag -l')) return '';
    if (command.includes('gh release view')) {
      throw new Error('GH release not found');
    }

    return '';
  };

  describe('Happy Path - Version Calculation', () => {
    it('should calculate the next stable version from the latest preview', () => {
      vi.mocked(execSync).mockImplementation(mockExecSync);
      const result = getVersion({ type: 'stable' });
      expect(result.releaseVersion).toBe('0.7.0');
      expect(result.npmTag).toBe('latest');
      expect(result.previousReleaseTag).toBe('v0.6.1');
    });

    it('should calculate the next preview version from the latest nightly', () => {
      vi.mocked(execSync).mockImplementation(mockExecSync);
      const result = getVersion({
        type: 'preview',
        'stable-base-version': '0.7.0',
      });
      expect(result.releaseVersion).toBe('0.8.0-preview.0');
      expect(result.npmTag).toBe('preview');
      expect(result.previousReleaseTag).toBe('v0.7.0-preview.1');
    });

    it('should calculate the next nightly version from package.json', () => {
      vi.mocked(execSync).mockImplementation(mockExecSync);
      const result = getVersion({ type: 'nightly' });
      // Note: The base version now comes from package.json, not the previous nightly tag.
      expect(result.releaseVersion).toBe('0.8.0-nightly.20250917.d3bf8a3d');
      expect(result.npmTag).toBe('nightly');
      expect(result.previousReleaseTag).toBe('v0.8.0-nightly.20250916.abcdef');
    });

    it('should calculate the next patch version for a stable release', () => {
      vi.mocked(execSync).mockImplementation(mockExecSync);
      const result = getVersion({ type: 'patch', 'patch-from': 'stable' });
      expect(result.releaseVersion).toBe('0.6.2');
      expect(result.npmTag).toBe('latest');
      expect(result.previousReleaseTag).toBe('v0.6.1');
    });

    it('should calculate the next patch version for a preview release', () => {
      vi.mocked(execSync).mockImplementation(mockExecSync);
      const result = getVersion({ type: 'patch', 'patch-from': 'preview' });
      expect(result.releaseVersion).toBe('0.7.0-preview.2');
      expect(result.npmTag).toBe('preview');
      expect(result.previousReleaseTag).toBe('v0.7.0-preview.1');
    });
  });

  describe('Advanced Scenarios', () => {
    it('should ignore a deprecated version and use the next highest', () => {
      const mockWithDeprecated = (command) => {
        // The highest nightly is 0.9.0, but it's deprecated
        if (command.includes('npm view') && command.includes('versions --json'))
          return JSON.stringify([
            '0.8.0-nightly.20250916.abcdef',
            '0.9.0-nightly.20250917.deprecated', // This one is deprecated
          ]);
        // Mock the deprecation check
        if (
          command.includes(
            'npm view @google/gemini-cli@0.9.0-nightly.20250917.deprecated deprecated',
          )
        )
          return 'This version is deprecated';
        // The dist-tag still points to the older, valid version
        if (command.includes('npm view') && command.includes('--tag=nightly'))
          return '0.8.0-nightly.20250916.abcdef';

        return mockExecSync(command);
      };
      vi.mocked(execSync).mockImplementation(mockWithDeprecated);

      const result = getVersion({
        type: 'preview',
        'stable-base-version': '0.7.0',
      });
      // It should base the preview off 0.8.0, not the deprecated 0.9.0
      expect(result.releaseVersion).toBe('0.8.0-preview.0');
    });

    it('should auto-increment patch version if the calculated one already exists', () => {
      const mockWithConflict = (command) => {
        // The calculated version 0.7.0 already exists as a git tag
        if (command.includes("git tag -l 'v0.7.0'")) return 'v0.7.0';
        // The next version, 0.7.1, is available
        if (command.includes("git tag -l 'v0.7.1'")) return '';

        return mockExecSync(command);
      };
      vi.mocked(execSync).mockImplementation(mockWithConflict);

      const result = getVersion({ type: 'stable' });
      // Should have skipped 0.7.0 and landed on 0.7.1
      expect(result.releaseVersion).toBe('0.7.1');
    });

    it('should auto-increment preview number if the calculated one already exists', () => {
      const mockWithConflict = (command) => {
        // The calculated preview 0.8.0-preview.0 already exists on NPM
        if (
          command.includes(
            'npm view @google/gemini-cli@0.8.0-preview.0 version',
          )
        )
          return '0.8.0-preview.0';
        // The next one is available
        if (
          command.includes(
            'npm view @google/gemini-cli@0.8.0-preview.1 version',
          )
        )
          throw new Error('Not found');

        return mockExecSync(command);
      };
      vi.mocked(execSync).mockImplementation(mockWithConflict);

      const result = getVersion({
        type: 'preview',
        'stable-base-version': '0.7.0',
      });
      // Should have skipped preview.0 and landed on preview.1
      expect(result.releaseVersion).toBe('0.8.0-preview.1');
    });
  });
});


--- packages/vscode-ide-companion/scripts/check-vscode-release.js ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { execSync } from 'node:child_process';

function checkRelease() {
  try {
    // Step 1: Find the commit hash of the last release
    const bucketUri = 'gs://gemini-cli-vscode-extension/release/1p/signed';
    const gcloudOutput = execSync(
      `gcloud storage ls --recursive ${bucketUri}`,
      { encoding: 'utf-8' },
    );
    const files = gcloudOutput.trim().split('\n');
    const vsixFiles = files.filter((file) =>
      /signed-gemini-cli-vscode-ide-companion-\d+\.\d+\.\d+-[a-f0-9]{7}\.vsix$/.test(
        file,
      ),
    );

    if (vsixFiles.length === 0) {
      console.error('No .vsix files found in the bucket.');
      process.exit(1);
    }

    vsixFiles.sort();
    const latestFile = vsixFiles[vsixFiles.length - 1];
    const fileName = latestFile.split('/').pop();
    const match =
      /signed-gemini-cli-vscode-ide-companion-(\d+\.\d+\.\d+)-([a-f0-9]{7})\.vsix$/.exec(
        fileName,
      );

    if (!match || !match[1] || !match[2]) {
      console.error(
        `Could not extract version and commit hash from filename: ${fileName}`,
      );
      process.exit(1);
    }
    const lastReleaseVersion = match[1];
    const lastReleaseCommit = match[2];

    // Step 2: Check for new commits
    execSync('git fetch origin main', { stdio: 'pipe' });
    const gitLog = execSync(
      `git log ${lastReleaseCommit}..origin/main --invert-grep --grep="chore(release): bump version to" -- packages/vscode-ide-companion`,
      { encoding: 'utf-8' },
    ).trim();

    // Step 3: Check for dependency changes
    const noticesDiff = execSync(
      `git diff ${lastReleaseCommit}..origin/main -- packages/vscode-ide-companion/NOTICES.txt`,
      { encoding: 'utf-8' },
    ).trim();

    if (gitLog) {
      console.log('\n--- New Commits ---');
      console.log(gitLog);
      console.log('-------------------');
    }

    if (noticesDiff) {
      console.log('\n--- Dependency Changes ---');
      console.log(noticesDiff);
      console.log('------------------------');
    }

    console.log('\n--- Summary ---');
    if (gitLog) {
      console.log(
        'New commits found for `packages/vscode-ide-companion` since last release. A new release is needed.',
      );
    } else {
      console.log(
        'No new commits found since last release. No release is necessary.',
      );
    }

    if (noticesDiff) {
      console.log(
        'Dependencies have changed. The license review form will require extra details.',
      );
    } else {
      console.log('No dependency changes found.');
    }

    console.log(`Last release version: ${lastReleaseVersion}`);
    console.log(`Last release commit hash: ${lastReleaseCommit}`);
    console.log('---------------');
  } catch (error) {
    console.error('Error checking for release:', error.message);
    process.exit(1);
  }
}

checkRelease();


--- .gemini/skills/pr-creator/SKILL.md ---
---
name: pr-creator
description: Use this skill when asked to create a pull request (PR). It ensures all PRs follow the repository's established templates and standards.
---

# Pull Request Creator

This skill guides the creation of high-quality Pull Requests that adhere to the repository's standards.

## Workflow

Follow these steps to create a Pull Request:

1.  **Locate Template**: Search for a pull request template in the repository.
    *   Check `.github/pull_request_template.md`
    *   Check `.github/PULL_REQUEST_TEMPLATE.md`
    *   If multiple templates exist (e.g., in `.github/PULL_REQUEST_TEMPLATE/`), ask the user which one to use or select the most appropriate one based on the context (e.g., `bug_fix.md` vs `feature.md`).

2.  **Read Template**: Read the content of the identified template file.

3.  **Draft Description**: Create a PR description that strictly follows the template's structure.
    *   **Headings**: Keep all headings from the template.
    *   **Checklists**: Review each item. Mark with `[x]` if completed. If an item is not applicable, leave it unchecked or mark as `[ ]` (depending on the template's instructions) or remove it if the template allows flexibility (but prefer keeping it unchecked for transparency).
    *   **Content**: Fill in the sections with clear, concise summaries of your changes.
    *   **Related Issues**: Link any issues fixed or related to this PR (e.g., "Fixes #123").

4.  **Create PR**: Use the `gh` CLI to create the PR.
    ```bash
    gh pr create --title "type(scope): succinct description" --body "..."
    ```
    *   **Title**: Ensure the title follows the [Conventional Commits](https://www.conventionalcommits.org/) format if the repository uses it (e.g., `feat(ui): add new button`, `fix(core): resolve crash`).

## Principles

*   **Compliance**: Never ignore the PR template. It exists for a reason.
*   **Completeness**: Fill out all relevant sections.
*   **Accuracy**: Don't check boxes for tasks you haven't done.


## Links discovered
- [Conventional Commits](https://www.conventionalcommits.org/)

--- .github/pull_request_template.md ---
## Summary

<!-- Concisely describe what this PR changes and why. Focus on impact and
urgency. -->

## Details

<!-- Add any extra context and design decisions. Keep it brief but complete. -->

## Related Issues

<!-- Use keywords to auto-close issues (Closes #123, Fixes #456). If this PR is
only related to an issue or is a partial fix, simply reference the issue number
without a keyword (Related to #123). -->

## How to Validate

<!-- List exact steps for reviewers to validate the change. Include commands,
expected results, and edge cases. -->

## Pre-Merge Checklist

<!-- Check all that apply before requesting review or merging. -->

- [ ] Updated relevant documentation and README (if needed)
- [ ] Added/updated tests (if needed)
- [ ] Noted breaking changes (if any)
- [ ] Validated on required platforms/methods:
  - [ ] MacOS
    - [ ] npm run
    - [ ] npx
    - [ ] Docker
    - [ ] Podman
    - [ ] Seatbelt
  - [ ] Windows
    - [ ] npm run
    - [ ] npx
    - [ ] Docker
  - [ ] Linux
    - [ ] npm run
    - [ ] npx
    - [ ] Docker


--- GEMINI.md ---
## Building and running

Before submitting any changes, it is crucial to validate them by running the
full preflight check. This command will build the repository, run all tests,
check for type errors, and lint the code.

To run the full suite of checks, execute the following command:

```bash
npm run preflight
```

This single command ensures that your changes meet all the quality gates of the
project. While you can run the individual steps (`build`, `test`, `typecheck`,
`lint`) separately, it is highly recommended to use `npm run preflight` to
ensure a comprehensive validation.

## Writing Tests

This project uses **Vitest** as its primary testing framework. When writing
tests, aim to follow existing patterns. Key conventions include:

### Test Structure and Framework

- **Framework**: All tests are written using Vitest (`describe`, `it`, `expect`,
  `vi`).
- **File Location**: Test files (`*.test.ts` for logic, `*.test.tsx` for React
  components) are co-located with the source files they test.
- **Configuration**: Test environments are defined in `vitest.config.ts` files.
- **Setup/Teardown**: Use `beforeEach` and `afterEach`. Commonly,
  `vi.resetAllMocks()` is called in `beforeEach` and `vi.restoreAllMocks()` in
  `afterEach`.

### Mocking (`vi` from Vitest)

- **ES Modules**: Mock with
  `vi.mock('module-name', async (importOriginal) => { ... })`. Use
  `importOriginal` for selective mocking.
  - _Example_:
    `vi.mock('os', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, homedir: vi.fn() }; });`
- **Mocking Order**: For critical dependencies (e.g., `os`, `fs`) that affect
  module-level constants, place `vi.mock` at the _very top_ of the test file,
  before other imports.
- **Hoisting**: Use `const myMock = vi.hoisted(() => vi.fn());` if a mock
  function needs to be defined before its use in a `vi.mock` factory.
- **Mock Functions**: Create with `vi.fn()`. Define behavior with
  `mockImplementation()`, `mockResolvedValue()`, or `mockRejectedValue()`.
- **Spying**: Use `vi.spyOn(object, 'methodName')`. Restore spies with
  `mockRestore()` in `afterEach`.

### Commonly Mocked Modules

- **Node.js built-ins**: `fs`, `fs/promises`, `os` (especially `os.homedir()`),
  `path`, `child_process` (`execSync`, `spawn`).
- **External SDKs**: `@google/genai`, `@modelcontextprotocol/sdk`.
- **Internal Project Modules**: Dependencies from other project packages are
  often mocked.

### React Component Testing (CLI UI - Ink)

- Use `render()` from `ink-testing-library`.
- Assert output with `lastFrame()`.
- Wrap components in necessary `Context.Provider`s.
- Mock custom React hooks and complex child components using `vi.mock()`.

### Asynchronous Testing

- Use `async/await`.
- For timers, use `vi.useFakeTimers()`, `vi.advanceTimersByTimeAsync()`,
  `vi.runAllTimersAsync()`.
- Test promise rejections with `await expect(promise).rejects.toThrow(...)`.

### General Guidance

- When adding tests, first examine existing tests to understand and conform to
  established conventions.
- Pay close attention to the mocks at the top of existing test files; they
  reveal critical dependencies and how they are managed in a test environment.

## Git Repo

The main branch for this project is called "main"

## JavaScript/TypeScript

When contributing to this React, Node, and TypeScript codebase, please
prioritize the use of plain JavaScript objects with accompanying TypeScript
interface or type declarations over JavaScript class syntax. This approach
offers significant advantages, especially concerning interoperability with React
and overall code maintainability.

### Preferring Plain Objects over Classes

JavaScript classes, by their nature, are designed to encapsulate internal state
and behavior. While this can be useful in some object-oriented paradigms, it
often introduces unnecessary complexity and friction when working with React's
component-based architecture. Here's why plain objects are preferred:

- Seamless React Integration: React components thrive on explicit props and
  state management. Classes' tendency to store internal state directly within
  instances can make prop and state propagation harder to reason about and
  maintain. Plain objects, on the other hand, are inherently immutable (when
  used thoughtfully) and can be easily passed as props, simplifying data flow
  and reducing unexpected side effects.

- Reduced Boilerplate and Increased Conciseness: Classes often promote the use
  of constructors, this binding, getters, setters, and other boilerplate that
  can unnecessarily bloat code. TypeScript interface and type declarations
  provide powerful static type checking without the runtime overhead or
  verbosity of class definitions. This allows for more succinct and readable
  code, aligning with JavaScript's strengths in functional programming.

- Enhanced Readability and Predictability: Plain objects, especially when their
  structure is clearly defined by TypeScript interfaces, are often easier to
  read and understand. Their properties are directly accessible, and there's no
  hidden internal state or complex inheritance chains to navigate. This
  predictability leads to fewer bugs and a more maintainable codebase.

- Simplified Immutability: While not strictly enforced, plain objects encourage
  an immutable approach to data. When you need to modify an object, you
  typically create a new one with the desired changes, rather than mutating the
  original. This pattern aligns perfectly with React's reconciliation process
  and helps prevent subtle bugs related to shared mutable state.

- Better Serialization and Deserialization: Plain JavaScript objects are
  naturally easy to serialize to JSON and deserialize back, which is a common
  requirement in web development (e.g., for API communication or local storage).
  Classes, with their methods and prototypes, can complicate this process.

### Embracing ES Module Syntax for Encapsulation

Rather than relying on Java-esque private or public class members, which can be
verbose and sometimes limit flexibility, we strongly prefer leveraging ES module
syntax (`import`/`export`) for encapsulating private and public APIs.

- Clearer Public API Definition: With ES modules, anything that is exported is
  part of the public API of that module, while anything not exported is
  inherently private to that module. This provides a very clear and explicit way
  to define what parts of your code are meant to be consumed by other modules.

- Enhanced Testability (Without Exposing Internals): By default, unexported
  functions or variables are not accessible from outside the module. This
  encourages you to test the public API of your modules, rather than their
  internal implementation details. If you find yourself needing to spy on or
  stub an unexported function for testing purposes, it's often a "code smell"
  indicating that the function might be a good candidate for extraction into its
  own separate, testable module with a well-defined public API. This promotes a
  more robust and maintainable testing strategy.

- Reduced Coupling: Explicitly defined module boundaries through import/export
  help reduce coupling between different parts of your codebase. This makes it
  easier to refactor, debug, and understand individual components in isolation.

### Avoiding `any` Types and Type Assertions; Preferring `unknown`

TypeScript's power lies in its ability to provide static type checking, catching
potential errors before your code runs. To fully leverage this, it's crucial to
avoid the `any` type and be judicious with type assertions.

- **The Dangers of `any`**: Using any effectively opts out of TypeScript's type
  checking for that particular variable or expression. While it might seem
  convenient in the short term, it introduces significant risks:
  - **Loss of Type Safety**: You lose all the benefits of type checking, making
    it easy to introduce runtime errors that TypeScript would otherwise have
    caught.
  - **Reduced Readability and Maintainability**: Code with `any` types is harder
    to understand and maintain, as the expected type of data is no longer
    explicitly defined.
  - **Masking Underlying Issues**: Often, the need for any indicates a deeper
    problem in the design of your code or the way you're interacting with
    external libraries. It's a sign that you might need to refine your types or
    refactor your code.

- **Preferring `unknown` over `any`**: When you absolutely cannot determine the
  type of a value at compile time, and you're tempted to reach for any, consider
  using unknown instead. unknown is a type-safe counterpart to any. While a
  variable of type unknown can hold any value, you must perform type narrowing
  (e.g., using typeof or instanceof checks, or a type assertion) before you can
  perform any operations on it. This forces you to handle the unknown type
  explicitly, preventing accidental runtime errors.

  ```ts
  function processValue(value: unknown) {
    if (typeof value === 'string') {
      // value is now safely a string
      console.log(value.toUpperCase());
    } else if (typeof value === 'number') {
      // value is now safely a number
      console.log(value * 2);
    }
    // Without narrowing, you cannot access properties or methods on 'value'
    // console.log(value.someProperty); // Error: Object is of type 'unknown'.
  }
  ```

- **Type Assertions (`as Type`) - Use with Caution**: Type assertions tell the
  TypeScript compiler, "Trust me, I know what I'm doing; this is definitely of
  this type." While there are legitimate use cases (e.g., when dealing with
  external libraries that don't have perfect type definitions, or when you have
  more information than the compiler), they should be used sparingly and with
  extreme caution.
  - **Bypassing Type Checking**: Like `any`, type assertions bypass TypeScript's
    safety checks. If your assertion is incorrect, you introduce a runtime error
    that TypeScript would not have warned you about.
  - **Code Smell in Testing**: A common scenario where `any` or type assertions
    might be tempting is when trying to test "private" implementation details
    (e.g., spying on or stubbing an unexported function within a module). This
    is a strong indication of a "code smell" in your testing strategy and
    potentially your code structure. Instead of trying to force access to
    private internals, consider whether those internal details should be
    refactored into a separate module with a well-defined public API. This makes
    them inherently testable without compromising encapsulation.

### Type narrowing `switch` clauses

Use the `checkExhaustive` helper in the default clause of a switch statement.
This will ensure that all of the possible options within the value or
enumeration are used.

This helper method can be found in `packages/cli/src/utils/checks.ts`

### Embracing JavaScript's Array Operators

To further enhance code cleanliness and promote safe functional programming
practices, leverage JavaScript's rich set of array operators as much as
possible. Methods like `.map()`, `.filter()`, `.reduce()`, `.slice()`,
`.sort()`, and others are incredibly powerful for transforming and manipulating
data collections in an immutable and declarative way.

Using these operators:

- Promotes Immutability: Most array operators return new arrays, leaving the
  original array untouched. This functional approach helps prevent unintended
  side effects and makes your code more predictable.
- Improves Readability: Chaining array operators often lead to more concise and
  expressive code than traditional for loops or imperative logic. The intent of
  the operation is clear at a glance.
- Facilitates Functional Programming: These operators are cornerstones of
  functional programming, encouraging the creation of pure functions that take
  inputs and produce outputs without causing side effects. This paradigm is
  highly beneficial for writing robust and testable code that pairs well with
  React.

By consistently applying these principles, we can maintain a codebase that is
not only efficient and performant but also a joy to work with, both now and in
the future.

## React (mirrored and adjusted from [react-mcp-server](https://github.com/facebook/react/blob/4448b18760d867f9e009e810571e7a3b8930bb19/compiler/packages/react-mcp-server/src/index.ts#L376C1-L441C94))

### Role

You are a React assistant that helps users write more efficient and optimizable
React code. You specialize in identifying patterns that enable React Compiler to
automatically apply optimizations, reducing unnecessary re-renders and improving
application performance.

### Follow these guidelines in all code you produce and suggest

Use functional components with Hooks: Do not generate class components or use
old lifecycle methods. Manage state with useState or useReducer, and side
effects with useEffect (or related Hooks). Always prefer functions and Hooks for
any new component logic.

Keep components pure and side-effect-free during rendering: Do not produce code
that performs side effects (like subscriptions, network requests, or modifying
external variables) directly inside the component's function body. Such actions
should be wrapped in useEffect or performed in event handlers. Ensure your
render logic is a pure function of props and state.

Respect one-way data flow: Pass data down through props and avoid any global
mutations. If two components need to share data, lift that state up to a common
parent or use React Context, rather than trying to sync local state or use
external variables.

Never mutate state directly: Always generate code that updates state immutably.
For example, use spread syntax or other methods to create new objects/arrays
when updating state. Do not use assignments like state.someValue = ... or array
mutations like array.push() on state variables. Use the state setter (setState
from useState, etc.) to update state.

Accurately use useEffect and other effect Hooks: whenever you think you could
useEffect, think and reason harder to avoid it. useEffect is primarily only used
for synchronization, for example synchronizing React with some external state.
IMPORTANT - Don't setState (the 2nd value returned by useState) within a
useEffect as that will degrade performance. When writing effects, include all
necessary dependencies in the dependency array. Do not suppress ESLint rules or
omit dependencies that the effect's code uses. Structure the effect callbacks to
handle changing values properly (e.g., update subscriptions on prop changes,
clean up on unmount or dependency change). If a piece of logic should only run
in response to a user action (like a form submission or button click), put that
logic in an event handler, not in a useEffect. Where possible, useEffects should
return a cleanup function.

Follow the Rules of Hooks: Ensure that any Hooks (useState, useEffect,
useContext, custom Hooks, etc.) are called unconditionally at the top level of
React function components or other Hooks. Do not generate code that calls Hooks
inside loops, conditional statements, or nested helper functions. Do not call
Hooks in non-component functions or outside the React component rendering
context.

Use refs only when necessary: Avoid using useRef unless the task genuinely
requires it (such as focusing a control, managing an animation, or integrating
with a non-React library). Do not use refs to store application state that
should be reactive. If you do use refs, never write to or read from ref.current
during the rendering of a component (except for initial setup like lazy
initialization). Any ref usage should not affect the rendered output directly.

Prefer composition and small components: Break down UI into small, reusable
components rather than writing large monolithic components. The code you
generate should promote clarity and reusability by composing components
together. Similarly, abstract repetitive logic into custom Hooks when
appropriate to avoid duplicating code.

Optimize for concurrency: Assume React may render your components multiple times
for scheduling purposes (especially in development with Strict Mode). Write code
that remains correct even if the component function runs more than once. For
instance, avoid side effects in the component body and use functional state
updates (e.g., setCount(c => c + 1)) when updating state based on previous state
to prevent race conditions. Always include cleanup functions in effects that
subscribe to external resources. Don't write useEffects for "do this when this
changes" side effects. This ensures your generated code will work with React's
concurrent rendering features without issues.

Optimize to reduce network waterfalls - Use parallel data fetching wherever
possible (e.g., start multiple requests at once rather than one after another).
Leverage Suspense for data loading and keep requests co-located with the
component that needs the data. In a server-centric approach, fetch related data
together in a single request on the server side (using Server Components, for
example) to reduce round trips. Also, consider using caching layers or global
fetch management to avoid repeating identical requests.

Rely on React Compiler - useMemo, useCallback, and React.memo can be omitted if
React Compiler is enabled. Avoid premature optimization with manual memoization.
Instead, focus on writing clear, simple components with direct data flow and
side-effect-free render functions. Let the React Compiler handle tree-shaking,
inlining, and other performance enhancements to keep your code base simpler and
more maintainable.

Design for a good user experience - Provide clear, minimal, and non-blocking UI
states. When data is loading, show lightweight placeholders (e.g., skeleton
screens) rather than intrusive spinners everywhere. Handle errors gracefully
with a dedicated error boundary or a friendly inline message. Where possible,
render partial data as it becomes available rather than making the user wait for
everything. Suspense allows you to declare the loading states in your component
tree in a natural way, preventing “flash” states and improving perceived
performance.

### Process

1. Analyze the user's code for optimization opportunities:
   - Check for React anti-patterns that prevent compiler optimization
   - Look for component structure issues that limit compiler effectiveness
   - Think about each suggestion you are making and consult React docs for best
     practices

2. Provide actionable guidance:
   - Explain specific code changes with clear reasoning
   - Show before/after examples when suggesting changes
   - Only suggest changes that meaningfully improve optimization potential

### Optimization Guidelines

- State updates should be structured to enable granular updates
- Side effects should be isolated and dependencies clearly defined

## Documentation guidelines

When working in the `/docs` directory, follow the guidelines in this section:

- **Role:** You are an expert technical writer and AI assistant for contributors
  to Gemini CLI. Produce professional, accurate, and consistent documentation to
  guide users of Gemini CLI.
- **Technical Accuracy:** Do not invent facts, commands, code, API names, or
  output. All technical information specific to Gemini CLI must be based on code
  found within this directory and its subdirectories.
- **Style Authority:** Your source for writing guidance and style is the
  "Documentation contribution process" section in the root directory's
  `CONTRIBUTING.md` file, as well as any guidelines provided this section.
- **Information Architecture Consideration:** Before proposing documentation
  changes, consider the information architecture. If a change adds significant
  new content to existing documents, evaluate if creating a new, more focused
  page or changes to `sidebar.json` would provide a better user experience.
- **Proactive User Consideration:** The user experience should be a primary
  concern when making changes to documentation. Aim to fill gaps in existing
  knowledge whenever possible while keeping documentation concise and easy for
  users to understand. If changes might hinder user understanding or
  accessibility, proactively raise these concerns and propose alternatives.

## Comments policy

Only write high-value comments if at all. Avoid talking to the user through
comments.

## Logging and Error Handling

- **Avoid Console Statements:** Do not use `console.log`, `console.error`, or
  similar methods directly.
- **Non-User-Facing Logs:** For developer-facing debug messages, use
  `debugLogger` (from `@google/gemini-cli-core`).
- **User-Facing Feedback:** To surface errors or warnings to the user, use
  `coreEvents.emitFeedback` (from `@google/gemini-cli-core`).

## General requirements

- If there is something you do not understand or is ambiguous, seek confirmation
  or clarification from the user before making changes based on assumptions.
- Use hyphens instead of underscores in flag names (e.g. `my-flag` instead of
  `my_flag`).
- Always refer to Gemini CLI as `Gemini CLI`, never `the Gemini CLI`.


## Links discovered
- [react-mcp-server](https://github.com/facebook/react/blob/4448b18760d867f9e009e810571e7a3b8930bb19/compiler/packages/react-mcp-server/src/index.ts#L376C1-L441C94)

--- README.md ---
# Gemini CLI

[![Gemini CLI CI](https://github.com/google-gemini/gemini-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/google-gemini/gemini-cli/actions/workflows/ci.yml)
[![Gemini CLI E2E (Chained)](https://github.com/google-gemini/gemini-cli/actions/workflows/chained_e2e.yml/badge.svg)](https://github.com/google-gemini/gemini-cli/actions/workflows/chained_e2e.yml)
[![Version](https://img.shields.io/npm/v/@google/gemini-cli)](https://www.npmjs.com/package/@google/gemini-cli)
[![License](https://img.shields.io/github/license/google-gemini/gemini-cli)](https://github.com/google-gemini/gemini-cli/blob/main/LICENSE)
[![View Code Wiki](https://www.gstatic.com/_/boq-sdlc-agents-ui/_/r/YUi5dj2UWvE.svg)](https://codewiki.google/github.com/google-gemini/gemini-cli)

![Gemini CLI Screenshot](./docs/assets/gemini-screenshot.png)

Gemini CLI is an open-source AI agent that brings the power of Gemini directly
into your terminal. It provides lightweight access to Gemini, giving you the
most direct path from your prompt to our model.

Learn all about Gemini CLI in our [documentation](https://geminicli.com/docs/).

## 🚀 Why Gemini CLI?

- **🎯 Free tier**: 60 requests/min and 1,000 requests/day with personal Google
  account.
- **🧠 Powerful Gemini 2.5 Pro**: Access to 1M token context window.
- **🔧 Built-in tools**: Google Search grounding, file operations, shell
  commands, web fetching.
- **🔌 Extensible**: MCP (Model Context Protocol) support for custom
  integrations.
- **💻 Terminal-first**: Designed for developers who live in the command line.
- **🛡️ Open source**: Apache 2.0 licensed.

## 📦 Installation

### Pre-requisites before installation

- Node.js version 20 or higher
- macOS, Linux, or Windows

### Quick Install

#### Run instantly with npx

```bash
# Using npx (no installation required)
npx @google/gemini-cli
```

#### Install globally with npm

```bash
npm install -g @google/gemini-cli
```

#### Install globally with Homebrew (macOS/Linux)

```bash
brew install gemini-cli
```

## Release Cadence and Tags

See [Releases](./docs/releases.md) for more details.

### Preview

New preview releases will be published each week at UTC 2359 on Tuesdays. These
releases will not have been fully vetted and may contain regressions or other
outstanding issues. Please help us test and install with `preview` tag.

```bash
npm install -g @google/gemini-cli@preview
```

### Stable

- New stable releases will be published each week at UTC 2000 on Tuesdays, this
  will be the full promotion of last week's `preview` release + any bug fixes
  and validations. Use `latest` tag.

```bash
npm install -g @google/gemini-cli@latest
```

### Nightly

- New releases will be published each day at UTC 0000. This will be all changes
  from the main branch as represented at time of release. It should be assumed
  there are pending validations and issues. Use `nightly` tag.

```bash
npm install -g @google/gemini-cli@nightly
```

## 📋 Key Features

### Code Understanding & Generation

- Query and edit large codebases
- Generate new apps from PDFs, images, or sketches using multimodal capabilities
- Debug issues and troubleshoot with natural language

### Automation & Integration

- Automate operational tasks like querying pull requests or handling complex
  rebases
- Use MCP servers to connect new capabilities, including
  [media generation with Imagen, Veo or Lyria](https://github.com/GoogleCloudPlatform/vertex-ai-creative-studio/tree/main/experiments/mcp-genmedia)
- Run non-interactively in scripts for workflow automation

### Advanced Capabilities

- Ground your queries with built-in
  [Google Search](https://ai.google.dev/gemini-api/docs/grounding) for real-time
  information
- Conversation checkpointing to save and resume complex sessions
- Custom context files (GEMINI.md) to tailor behavior for your projects

### GitHub Integration

Integrate Gemini CLI directly into your GitHub workflows with
[**Gemini CLI GitHub Action**](https://github.com/google-github-actions/run-gemini-cli):

- **Pull Request Reviews**: Automated code review with contextual feedback and
  suggestions
- **Issue Triage**: Automated labeling and prioritization of GitHub issues based
  on content analysis
- **On-demand Assistance**: Mention `@gemini-cli` in issues and pull requests
  for help with debugging, explanations, or task delegation
- **Custom Workflows**: Build automated, scheduled and on-demand workflows
  tailored to your team's needs

## 🔐 Authentication Options

Choose the authentication method that best fits your needs:

### Option 1: Login with Google (OAuth login using your Google Account)

**✨ Best for:** Individual developers as well as anyone who has a Gemini Code
Assist License. (see
[quota limits and terms of service](https://cloud.google.com/gemini/docs/quotas)
for details)

**Benefits:**

- **Free tier**: 60 requests/min and 1,000 requests/day
- **Gemini 2.5 Pro** with 1M token context window
- **No API key management** - just sign in with your Google account
- **Automatic updates** to latest models

#### Start Gemini CLI, then choose _Login with Google_ and follow the browser authentication flow when prompted

```bash
gemini
```

#### If you are using a paid Code Assist License from your organization, remember to set the Google Cloud Project

```bash
# Set your Google Cloud Project
export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
gemini
```

### Option 2: Gemini API Key

**✨ Best for:** Developers who need specific model control or paid tier access

**Benefits:**

- **Free tier**: 100 requests/day with Gemini 2.5 Pro
- **Model selection**: Choose specific Gemini models
- **Usage-based billing**: Upgrade for higher limits when needed

```bash
# Get your key from https://aistudio.google.com/apikey
export GEMINI_API_KEY="YOUR_API_KEY"
gemini
```

### Option 3: Vertex AI

**✨ Best for:** Enterprise teams and production workloads

**Benefits:**

- **Enterprise features**: Advanced security and compliance
- **Scalable**: Higher rate limits with billing account
- **Integration**: Works with existing Google Cloud infrastructure

```bash
# Get your key from Google Cloud Console
export GOOGLE_API_KEY="YOUR_API_KEY"
export GOOGLE_GENAI_USE_VERTEXAI=true
gemini
```

For Google Workspace accounts and other authentication methods, see the
[authentication guide](./docs/get-started/authentication.md).

## 🚀 Getting Started

### Basic Usage

#### Start in current directory

```bash
gemini
```

#### Include multiple directories

```bash
gemini --include-directories ../lib,../docs
```

#### Use specific model

```bash
gemini -m gemini-2.5-flash
```

#### Non-interactive mode for scripts

Get a simple text response:

```bash
gemini -p "Explain the architecture of this codebase"
```

For more advanced scripting, including how to parse JSON and handle errors, use
the `--output-format json` flag to get structured output:

```bash
gemini -p "Explain the architecture of this codebase" --output-format json
```

For real-time event streaming (useful for monitoring long-running operations),
use `--output-format stream-json` to get newline-delimited JSON events:

```bash
gemini -p "Run tests and deploy" --output-format stream-json
```

### Quick Examples

#### Start a new project

```bash
cd new-project/
gemini
> Write me a Discord bot that answers questions using a FAQ.md file I will provide
```

#### Analyze existing code

```bash
git clone https://github.com/google-gemini/gemini-cli
cd gemini-cli
gemini
> Give me a summary of all of the changes that went in yesterday
```

## 📚 Documentation

### Getting Started

- [**Quickstart Guide**](./docs/get-started/index.md) - Get up and running
  quickly.
- [**Authentication Setup**](./docs/get-started/authentication.md) - Detailed
  auth configuration.
- [**Configuration Guide**](./docs/get-started/configuration.md) - Settings and
  customization.
- [**Keyboard Shortcuts**](./docs/cli/keyboard-shortcuts.md) - Productivity
  tips.

### Core Features

- [**Commands Reference**](./docs/cli/commands.md) - All slash commands
  (`/help`, `/chat`, etc).
- [**Custom Commands**](./docs/cli/custom-commands.md) - Create your own
  reusable commands.
- [**Context Files (GEMINI.md)**](./docs/cli/gemini-md.md) - Provide persistent
  context to Gemini CLI.
- [**Checkpointing**](./docs/cli/checkpointing.md) - Save and resume
  conversations.
- [**Token Caching**](./docs/cli/token-caching.md) - Optimize token usage.

### Tools & Extensions

- [**Built-in Tools Overview**](./docs/tools/index.md)
  - [File System Operations](./docs/tools/file-system.md)
  - [Shell Commands](./docs/tools/shell.md)
  - [Web Fetch & Search](./docs/tools/web-fetch.md)
- [**MCP Server Integration**](./docs/tools/mcp-server.md) - Extend with custom
  tools.
- [**Custom Extensions**](./docs/extensions/index.md) - Build and share your own
  commands.

### Advanced Topics

- [**Headless Mode (Scripting)**](./docs/cli/headless.md) - Use Gemini CLI in
  automated workflows.
- [**Architecture Overview**](./docs/architecture.md) - How Gemini CLI works.
- [**IDE Integration**](./docs/ide-integration/index.md) - VS Code companion.
- [**Sandboxing & Security**](./docs/cli/sandbox.md) - Safe execution
  environments.
- [**Trusted Folders**](./docs/cli/trusted-folders.md) - Control execution
  policies by folder.
- [**Enterprise Guide**](./docs/cli/enterprise.md) - Deploy and manage in a
  corporate environment.
- [**Telemetry & Monitoring**](./docs/cli/telemetry.md) - Usage tracking.
- [**Tools API Development**](./docs/core/tools-api.md) - Create custom tools.
- [**Local development**](./docs/local-development.md) - Local development
  tooling.

### Troubleshooting & Support

- [**Troubleshooting Guide**](./docs/troubleshooting.md) - Common issues and
  solutions.
- [**FAQ**](./docs/faq.md) - Frequently asked questions.
- Use `/bug` command to report issues directly from the CLI.

### Using MCP Servers

Configure MCP servers in `~/.gemini/settings.json` to extend Gemini CLI with
custom tools:

```text
> @github List my open pull requests
> @slack Send a summary of today's commits to #dev channel
> @database Run a query to find inactive users
```

See the [MCP Server Integration guide](./docs/tools/mcp-server.md) for setup
instructions.

## 🤝 Contributing

We welcome contributions! Gemini CLI is fully open source (Apache 2.0), and we
encourage the community to:

- Report bugs and suggest features.
- Improve documentation.
- Submit code improvements.
- Share your MCP servers and extensions.

See our [Contributing Guide](./CONTRIBUTING.md) for development setup, coding
standards, and how to submit pull requests.

Check our [Official Roadmap](https://github.com/orgs/google-gemini/projects/11)
for planned features and priorities.

## 📖 Resources

- **[Official Roadmap](./ROADMAP.md)** - See what's coming next.
- **[Changelog](./docs/changelogs/index.md)** - See recent notable updates.
- **[NPM Package](https://www.npmjs.com/package/@google/gemini-cli)** - Package
  registry.
- **[GitHub Issues](https://github.com/google-gemini/gemini-cli/issues)** -
  Report bugs or request features.
- **[Security Advisories](https://github.com/google-gemini/gemini-cli/security/advisories)** -
  Security updates.

### Uninstall

See the [Uninstall Guide](docs/cli/uninstall.md) for removal instructions.

## 📄 Legal

- **License**: [Apache License 2.0](LICENSE)
- **Terms of Service**: [Terms & Privacy](./docs/tos-privacy.md)
- **Security**: [Security Policy](SECURITY.md)

---

<p align="center">
  Built with ❤️ by Google and the open source community
</p>


## Links discovered
- [![Gemini CLI CI](https://github.com/google-gemini/gemini-cli/actions/workflows/ci.yml/badge.svg)
- [![Gemini CLI E2E (Chained)](https://github.com/google-gemini/gemini-cli/actions/workflows/chained_e2e.yml/badge.svg)
- [![Version](https://img.shields.io/npm/v/@google/gemini-cli)
- [![License](https://img.shields.io/github/license/google-gemini/gemini-cli)
- [![View Code Wiki](https://www.gstatic.com/_/boq-sdlc-agents-ui/_/r/YUi5dj2UWvE.svg)
- [Gemini CLI Screenshot](https://github.com/google-gemini/gemini-cli/blob/main/docs/assets/gemini-screenshot.png)
- [documentation](https://geminicli.com/docs/)
- [Releases](https://github.com/google-gemini/gemini-cli/blob/main/docs/releases.md)
- [media generation with Imagen, Veo or Lyria](https://github.com/GoogleCloudPlatform/vertex-ai-creative-studio/tree/main/experiments/mcp-genmedia)
- [Google Search](https://ai.google.dev/gemini-api/docs/grounding)
- [**Gemini CLI GitHub Action**](https://github.com/google-github-actions/run-gemini-cli)
- [quota limits and terms of service](https://cloud.google.com/gemini/docs/quotas)
- [authentication guide](https://github.com/google-gemini/gemini-cli/blob/main/docs/get-started/authentication.md)
- [**Quickstart Guide**](https://github.com/google-gemini/gemini-cli/blob/main/docs/get-started/index.md)
- [**Authentication Setup**](https://github.com/google-gemini/gemini-cli/blob/main/docs/get-started/authentication.md)
- [**Configuration Guide**](https://github.com/google-gemini/gemini-cli/blob/main/docs/get-started/configuration.md)
- [**Keyboard Shortcuts**](https://github.com/google-gemini/gemini-cli/blob/main/docs/cli/keyboard-shortcuts.md)
- [**Commands Reference**](https://github.com/google-gemini/gemini-cli/blob/main/docs/cli/commands.md)
- [**Custom Commands**](https://github.com/google-gemini/gemini-cli/blob/main/docs/cli/custom-commands.md)
- [**Context Files (GEMINI.md)**](https://github.com/google-gemini/gemini-cli/blob/main/docs/cli/gemini-md.md)
- [**Checkpointing**](https://github.com/google-gemini/gemini-cli/blob/main/docs/cli/checkpointing.md)
- [**Token Caching**](https://github.com/google-gemini/gemini-cli/blob/main/docs/cli/token-caching.md)
- [**Built-in Tools Overview**](https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/index.md)
- [File System Operations](https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/file-system.md)
- [Shell Commands](https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/shell.md)
- [Web Fetch & Search](https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/web-fetch.md)
- [**MCP Server Integration**](https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md)
- [**Custom Extensions**](https://github.com/google-gemini/gemini-cli/blob/main/docs/extensions/index.md)
- [**Headless Mode (Scripting)**](https://github.com/google-gemini/gemini-cli/blob/main/docs/cli/headless.md)
- [**Architecture Overview**](https://github.com/google-gemini/gemini-cli/blob/main/docs/architecture.md)
- [**IDE Integration**](https://github.com/google-gemini/gemini-cli/blob/main/docs/ide-integration/index.md)
- [**Sandboxing & Security**](https://github.com/google-gemini/gemini-cli/blob/main/docs/cli/sandbox.md)
- [**Trusted Folders**](https://github.com/google-gemini/gemini-cli/blob/main/docs/cli/trusted-folders.md)
- [**Enterprise Guide**](https://github.com/google-gemini/gemini-cli/blob/main/docs/cli/enterprise.md)
- [**Telemetry & Monitoring**](https://github.com/google-gemini/gemini-cli/blob/main/docs/cli/telemetry.md)
- [**Tools API Development**](https://github.com/google-gemini/gemini-cli/blob/main/docs/core/tools-api.md)
- [**Local development**](https://github.com/google-gemini/gemini-cli/blob/main/docs/local-development.md)
- [**Troubleshooting Guide**](https://github.com/google-gemini/gemini-cli/blob/main/docs/troubleshooting.md)
- [**FAQ**](https://github.com/google-gemini/gemini-cli/blob/main/docs/faq.md)
- [MCP Server Integration guide](https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md)
- [Contributing Guide](https://github.com/google-gemini/gemini-cli/blob/main/CONTRIBUTING.md)
- [Official Roadmap](https://github.com/orgs/google-gemini/projects/11)
- [Official Roadmap](https://github.com/google-gemini/gemini-cli/blob/main/ROADMAP.md)
- [Changelog](https://github.com/google-gemini/gemini-cli/blob/main/docs/changelogs/index.md)
- [NPM Package](https://www.npmjs.com/package/@google/gemini-cli)
- [GitHub Issues](https://github.com/google-gemini/gemini-cli/issues)
- [Security Advisories](https://github.com/google-gemini/gemini-cli/security/advisories)
- [Uninstall Guide](https://github.com/google-gemini/gemini-cli/blob/main/docs/cli/uninstall.md)
- [Apache License 2.0](https://github.com/google-gemini/gemini-cli/blob/main/LICENSE.md)
- [Terms & Privacy](https://github.com/google-gemini/gemini-cli/blob/main/docs/tos-privacy.md)
- [Security Policy](https://github.com/google-gemini/gemini-cli/blob/main/SECURITY.md)

--- ROADMAP.md ---
# Gemini CLI Roadmap

The
[Official Gemini CLI Roadmap](https://github.com/orgs/google-gemini/projects/11/)

Gemini CLI is an open-source AI agent that brings the power of Gemini directly
into your terminal. It provides lightweight access to Gemini, giving you the
most direct path from your prompt to our model.

This document outlines our approach to the Gemini CLI roadmap. Here, you'll find
our guiding principles and a breakdown of the key areas we are focused on for
development. Our roadmap is not a static list but a dynamic set of priorities
that are tracked live in our GitHub Issues.

As an
[Apache 2.0 open source project](https://github.com/google-gemini/gemini-cli?tab=Apache-2.0-1-ov-file#readme),
we appreciate and welcome
[public contributions](https://github.com/google-gemini/gemini-cli/blob/main/CONTRIBUTING.md),
and will give first priority to those contributions aligned with our roadmap. If
you want to propose a new feature or change to our roadmap, please start by
[opening an issue for discussion](https://github.com/google-gemini/gemini-cli/issues/new/choose).

## Disclaimer

This roadmap represents our current thinking and is for informational purposes
only. It is not a commitment or a guarantee of future delivery. The development,
release, and timing of any features are subject to change, and we may update the
roadmap based on community discussions as well as when our priorities evolve.

## Guiding Principles

Our development is guided by the following principles:

- **Power & Simplicity:** Deliver access to state-of-the-art Gemini models with
  an intuitive and easy-to-use lightweight command-line interface.
- **Extensibility:** An adaptable agent to help you with a variety of use cases
  and environments along with the ability to run these agents anywhere.
- **Intelligent:** Gemini CLI should be reliably ranked among the best agentic
  tools as measured by benchmarks like SWE Bench, Terminal Bench, and CSAT.
- **Free and Open Source:** Foster a thriving open source community where cost
  isn’t a barrier to personal use, and PRs get merged quickly. This means
  resolving and closing issues, pull requests, and discussion posts quickly.

## How the Roadmap Works

Our roadmap is managed directly through GitHub Issues. See our entry point
Roadmap Issue [here](https://github.com/google-gemini/gemini-cli/issues/4191).
This approach allows for transparency and gives you a direct way to learn more
or get involved with any specific initiative. All our roadmap items will be
tagged as Type:`Feature` and Label:`maintainer` for features we are actively
working on, or Type:`Task` and Label:`maintainer` for a more detailed list of
tasks.

Issues are organized to provide key information at a glance:

- **Target Quarter:** `Milestone` denotes the anticipated delivery timeline.
- **Feature Area:** Labels such as `area/model` or `area/tooling` categorize the
  work.
- **Issue Type:** _Workstream_ => _Epics_ => _Features_ => _Tasks|Bugs_

To see what we're working on, you can filter our issues by these dimensions. See
all our items [here](https://github.com/orgs/google-gemini/projects/11/views/19)

## Focus Areas

To better organize our efforts, we categorize our work into several key feature
areas. These labels are used on our GitHub Issues to help you filter and find
initiatives that interest you.

- **Authentication:** Secure user access via API keys, Gemini Code Assist login,
  etc.
- **Model:** Support new Gemini models, multi-modality, local execution, and
  performance tuning.
- **User Experience:** Improve the CLI's usability, performance, interactive
  features, and documentation.
- **Tooling:** Built-in tools and the MCP ecosystem.
- **Core:** Core functionality of the CLI
- **Extensibility:** Bringing Gemini CLI to other surfaces e.g. GitHub.
- **Contribution:** Improve the contribution process via test automation and
  CI/CD pipeline enhancements.
- **Platform:** Manage installation, OS support, and the underlying CLI
  framework.
- **Quality:** Focus on testing, reliability, performance, and overall product
  quality.
- **Background Agents:** Enable long-running, autonomous tasks and proactive
  assistance.
- **Security and Privacy:** For all things related to security and privacy

## How to Contribute

Gemini CLI is an open-source project, and we welcome contributions from the
community! Whether you're a developer, a designer, or just an enthusiastic user
you can find our
[Community Guidelines here](https://github.com/google-gemini/gemini-cli/blob/main/CONTRIBUTING.md)
to learn how to get started. There are many ways to get involved:

- **Roadmap:** Please review and find areas in our
  [roadmap](https://github.com/google-gemini/gemini-cli/issues/4191) that you
  would like to contribute to. Contributions based on this will be easiest to
  integrate with.
- **Report Bugs:** If you find an issue, please create a
  [bug](https://github.com/google-gemini/gemini-cli/issues/new?template=bug_report.yml)
  with as much detail as possible. If you believe it is a critical breaking
  issue preventing direct CLI usage, please tag it as `priority/p0`.
- **Suggest Features:** Have a great idea? We'd love to hear it! Open a
  [feature request](https://github.com/google-gemini/gemini-cli/issues/new?template=feature_request.yml).
- **Contribute Code:** Check out our
  [CONTRIBUTING.md](https://github.com/google-gemini/gemini-cli/blob/main/CONTRIBUTING.md)
  file for guidelines on how to submit pull requests. We have a list of "good
  first issues" for new contributors.
- **Write Documentation:** Help us improve our documentation, tutorials, and
  examples. We are excited about the future of Gemini CLI and look forward to
  building it with you!


## Links discovered
- [Official Gemini CLI Roadmap](https://github.com/orgs/google-gemini/projects/11/)
- [Apache 2.0 open source project](https://github.com/google-gemini/gemini-cli?tab=Apache-2.0-1-ov-file#readme)
- [public contributions](https://github.com/google-gemini/gemini-cli/blob/main/CONTRIBUTING.md)
- [opening an issue for discussion](https://github.com/google-gemini/gemini-cli/issues/new/choose)
- [here](https://github.com/google-gemini/gemini-cli/issues/4191)
- [here](https://github.com/orgs/google-gemini/projects/11/views/19)
- [Community Guidelines here](https://github.com/google-gemini/gemini-cli/blob/main/CONTRIBUTING.md)
- [roadmap](https://github.com/google-gemini/gemini-cli/issues/4191)
- [bug](https://github.com/google-gemini/gemini-cli/issues/new?template=bug_report.yml)
- [feature request](https://github.com/google-gemini/gemini-cli/issues/new?template=feature_request.yml)
- [CONTRIBUTING.md](https://github.com/google-gemini/gemini-cli/blob/main/CONTRIBUTING.md)

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

import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createRequire } from 'node:module';
import { writeFileSync } from 'node:fs';
import { wasmLoader } from 'esbuild-plugin-wasm';

let esbuild;
try {
  esbuild = (await import('esbuild')).default;
} catch (_error) {
  console.warn('esbuild not available, skipping bundle step');
  process.exit(0);
}

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const require = createRequire(import.meta.url);
const pkg = require(path.resolve(__dirname, 'package.json'));

function createWasmPlugins() {
  const wasmBinaryPlugin = {
    name: 'wasm-binary',
    setup(build) {
      build.onResolve({ filter: /\.wasm\?binary$/ }, (args) => {
        const specifier = args.path.replace(/\?binary$/, '');
        const resolveDir = args.resolveDir || '';
        const isBareSpecifier =
          !path.isAbsolute(specifier) &&
          !specifier.startsWith('./') &&
          !specifier.startsWith('../');

        let resolvedPath;
        if (isBareSpecifier) {
          resolvedPath = require.resolve(specifier, {
            paths: resolveDir ? [resolveDir, __dirname] : [__dirname],
          });
        } else {
          resolvedPath = path.isAbsolute(specifier)
            ? specifier
            : path.join(resolveDir, specifier);
        }

        return { path: resolvedPath, namespace: 'wasm-embedded' };
      });
    },
  };

  return [wasmBinaryPlugin, wasmLoader({ mode: 'embedded' })];
}

const external = [
  '@lydell/node-pty',
  'node-pty',
  '@lydell/node-pty-darwin-arm64',
  '@lydell/node-pty-darwin-x64',
  '@lydell/node-pty-linux-x64',
  '@lydell/node-pty-win32-arm64',
  '@lydell/node-pty-win32-x64',
  'keytar',
];

const baseConfig = {
  bundle: true,
  platform: 'node',
  format: 'esm',
  external,
  loader: { '.node': 'file' },
  write: true,
};

const cliConfig = {
  ...baseConfig,
  banner: {
    js: `import { createRequire } from 'module'; const require = createRequire(import.meta.url); globalThis.__filename = require('url').fileURLToPath(import.meta.url); globalThis.__dirname = require('path').dirname(globalThis.__filename);`,
  },
  entryPoints: ['packages/cli/index.ts'],
  outfile: 'bundle/gemini.js',
  define: {
    'process.env.CLI_VERSION': JSON.stringify(pkg.version),
  },
  plugins: createWasmPlugins(),
  alias: {
    'is-in-ci': path.resolve(__dirname, 'packages/cli/src/patches/is-in-ci.ts'),
  },
  metafile: true,
};

const a2aServerConfig = {
  ...baseConfig,
  banner: {
    js: `const require = (await import('module')).createRequire(import.meta.url); globalThis.__filename = require('url').fileURLToPath(import.meta.url); globalThis.__dirname = require('path').dirname(globalThis.__filename);`,
  },
  entryPoints: ['packages/a2a-server/src/http/server.ts'],
  outfile: 'packages/a2a-server/dist/a2a-server.mjs',
  define: {
    'process.env.CLI_VERSION': JSON.stringify(pkg.version),
  },
  plugins: createWasmPlugins(),
};

Promise.allSettled([
  esbuild.build(cliConfig).then(({ metafile }) => {
    if (process.env.DEV === 'true') {
      writeFileSync('./bundle/esbuild.json', JSON.stringify(metafile, null, 2));
    }
  }),
  esbuild.build(a2aServerConfig),
]).then((results) => {
  const [cliResult, a2aResult] = results;
  if (cliResult.status === 'rejected') {
    console.error('gemini.js build failed:', cliResult.reason);
    process.exit(1);
  }
  // error in a2a-server bundling will not stop gemini.js bundling process
  if (a2aResult.status === 'rejected') {
    console.warn('a2a-server build failed:', a2aResult.reason);
  }
});


--- eslint.config.js ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import reactPlugin from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import prettierConfig from 'eslint-config-prettier';
import importPlugin from 'eslint-plugin-import';
import vitest from '@vitest/eslint-plugin';
import globals from 'globals';
import headers from 'eslint-plugin-headers';
import path from 'node:path';
import url from 'node:url';

// --- ESM way to get __dirname ---
const __filename = url.fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// --- ---

// Determine the monorepo root (assuming eslint.config.js is at the root)
const projectRoot = __dirname;

export default tseslint.config(
  {
    // Global ignores
    ignores: [
      'node_modules/*',
      'eslint.config.js',
      'packages/**/dist/**',
      'bundle/**',
      'package/bundle/**',
      '.integration-tests/**',
      'dist/**',
    ],
  },
  eslint.configs.recommended,
  ...tseslint.configs.recommended,
  reactHooks.configs['recommended-latest'],
  reactPlugin.configs.flat.recommended,
  reactPlugin.configs.flat['jsx-runtime'], // Add this if you are using React 17+
  {
    // Settings for eslint-plugin-react
    settings: {
      react: {
        version: 'detect',
      },
    },
  },
  {
    // Import specific config
    files: ['packages/cli/src/**/*.{ts,tsx}'], // Target only TS/TSX in the cli package
    plugins: {
      import: importPlugin,
    },
    settings: {
      'import/resolver': {
        node: true,
      },
    },
    rules: {
      ...importPlugin.configs.recommended.rules,
      ...importPlugin.configs.typescript.rules,
      'import/no-default-export': 'warn',
      'import/no-unresolved': 'off', // Disable for now, can be noisy with monorepos/paths
    },
  },
  {
    // General overrides and rules for the project (TS/TSX files)
    files: ['packages/*/src/**/*.{ts,tsx}'], // Target only TS/TSX in the cli package
    plugins: {
      import: importPlugin,
    },
    settings: {
      'import/resolver': {
        node: true,
      },
    },
    languageOptions: {
      parser: tseslint.parser,
      parserOptions: {
        projectService: true,
        tsconfigRootDir: projectRoot,
      },
      globals: {
        ...globals.node,
        ...globals.es2021,
      },
    },
    rules: {
      // General Best Practice Rules (subset adapted for flat config)
      '@typescript-eslint/array-type': ['error', { default: 'array-simple' }],
      'arrow-body-style': ['error', 'as-needed'],
      curly: ['error', 'multi-line'],
      eqeqeq: ['error', 'always', { null: 'ignore' }],
      '@typescript-eslint/consistent-type-assertions': [
        'error',
        { assertionStyle: 'as' },
      ],
      '@typescript-eslint/explicit-member-accessibility': [
        'error',
        { accessibility: 'no-public' },
      ],
      '@typescript-eslint/no-explicit-any': 'error',
      '@typescript-eslint/no-inferrable-types': [
        'error',
        { ignoreParameters: true, ignoreProperties: true },
      ],
      '@typescript-eslint/consistent-type-imports': [
        'error',
        { disallowTypeAnnotations: false },
      ],
      '@typescript-eslint/no-namespace': ['error', { allowDeclarations: true }],
      '@typescript-eslint/no-unused-vars': [
        'error',
        {
          argsIgnorePattern: '^_',
          varsIgnorePattern: '^_',
          caughtErrorsIgnorePattern: '^_',
        },
      ],
      // Prevent async errors from bypassing catch handlers
      '@typescript-eslint/return-await': ['error', 'in-try-catch'],
      'import/no-internal-modules': [
        'error',
        {
          allow: [
            'react-dom/test-utils',
            'memfs/lib/volume.js',
            'yargs/**',
            'msw/node',
          ],
        },
      ],
      'import/no-relative-packages': 'error',
      'no-cond-assign': 'error',
      'no-debugger': 'error',
      'no-duplicate-case': 'error',
      'no-restricted-syntax': [
        'error',
        {
          selector: 'CallExpression[callee.name="require"]',
          message: 'Avoid using require(). Use ES6 imports instead.',
        },
        {
          selector: 'ThrowStatement > Literal:not([value=/^\\w+Error:/])',
          message:
            'Do not throw string literals or non-Error objects. Throw new Error("...") instead.',
        },
      ],
      'no-unsafe-finally': 'error',
      'no-unused-expressions': 'off', // Disable base rule
      '@typescript-eslint/no-unused-expressions': [
        // Enable TS version
        'error',
        { allowShortCircuit: true, allowTernary: true },
      ],
      'no-var': 'error',
      'object-shorthand': 'error',
      'one-var': ['error', 'never'],
      'prefer-arrow-callback': 'error',
      'prefer-const': ['error', { destructuring: 'all' }],
      radix: 'error',
      'no-console': 'error',
      'default-case': 'error',
      '@typescript-eslint/await-thenable': ['error'],
      '@typescript-eslint/no-floating-promises': ['error'],
      '@typescript-eslint/no-unnecessary-type-assertion': ['error'],
      'no-restricted-imports': [
        'error',
        {
          paths: [
            {
              name: 'node:os',
              importNames: ['homedir', 'tmpdir'],
              message:
                'Please use the helpers from @google/gemini-cli-core instead of node:os homedir()/tmpdir() to ensure strict environment isolation.',
            },
            {
              name: 'os',
              importNames: ['homedir', 'tmpdir'],
              message:
                'Please use the helpers from @google/gemini-cli-core instead of os homedir()/tmpdir() to ensure strict environment isolation.',
            },
          ],
        },
      ],
    },
  },
  {
    // Allow os.homedir() in tests and paths.ts where it is used to implement the helper
    files: [
      '**/*.test.ts',
      '**/*.test.tsx',
      'packages/core/src/utils/paths.ts',
      'packages/test-utils/src/**/*.ts',
      'scripts/**/*.js',
    ],
    rules: {
      'no-restricted-imports': 'off',
    },
  },
  {
    // Prevent self-imports in packages
    files: ['packages/core/src/**/*.{ts,tsx}'],
    rules: {
      'no-restricted-imports': [
        'error',
        {
          name: '@google/gemini-cli-core',
          message: 'Please use relative imports within the @google/gemini-cli-core package.',
        },
      ],
    },
  },
  {
    files: ['packages/cli/src/**/*.{ts,tsx}'],
    rules: {
      'no-restricted-imports': [
        'error',
        {
          name: '@google/gemini-cli',
          message: 'Please use relative imports within the @google/gemini-cli package.',
        },
      ],
    },
  },
  {
    files: ['packages/*/src/**/*.test.{ts,tsx}'],
    plugins: {
      vitest,
    },
    rules: {
      ...vitest.configs.recommended.rules,
      'vitest/expect-expect': 'off',
      'vitest/no-commented-out-tests': 'off',
    },
  },
  {
    files: ['./**/*.{tsx,ts,js}'],
    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'],
    },
  },
  // extra settings for scripts that we run directly with node
  {
    files: ['./scripts/**/*.js', 'esbuild.config.js'],
    languageOptions: {
      globals: {
        ...globals.node,
        process: 'readonly',
        console: 'readonly',
      },
    },
    rules: {
      '@typescript-eslint/no-unused-vars': [
        'error',
        {
          argsIgnorePattern: '^_',
          varsIgnorePattern: '^_',
          caughtErrorsIgnorePattern: '^_',
        },
      ],
    },
  },
  {
    files: ['packages/vscode-ide-companion/esbuild.js'],
    languageOptions: {
      globals: {
        ...globals.node,
        process: 'readonly',
        console: 'readonly',
      },
    },
    rules: {
      'no-restricted-syntax': 'off',
      '@typescript-eslint/no-require-imports': 'off',
    },
  },
  // extra settings for scripts that we run directly with node
  {
    files: ['packages/vscode-ide-companion/scripts/**/*.js'],
    languageOptions: {
      globals: {
        ...globals.node,
        process: 'readonly',
        console: 'readonly',
      },
    },
    rules: {
      'no-restricted-syntax': 'off',
      '@typescript-eslint/no-require-imports': 'off',
    },
  },
  // Prettier config must be last
  prettierConfig,
  // extra settings for scripts that we run directly with node
  {
    files: ['./integration-tests/**/*.js'],
    languageOptions: {
      globals: {
        ...globals.node,
        process: 'readonly',
        console: 'readonly',
      },
    },
    rules: {
      '@typescript-eslint/no-unused-vars': [
        'error',
        {
          argsIgnorePattern: '^_',
          varsIgnorePattern: '^_',
          caughtErrorsIgnorePattern: '^_',
        },
      ],
    },
  },
);


--- integration-tests/context-compress-interactive.test.ts ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { expect, describe, it, beforeEach, afterEach } from 'vitest';
import { TestRig } from './test-helper.js';
import { join } from 'node:path';

describe('Interactive Mode', () => {
  let rig: TestRig;

  beforeEach(() => {
    rig = new TestRig();
  });

  afterEach(async () => {
    await rig.cleanup();
  });

  it('should trigger chat compression with /compress command', async () => {
    await rig.setup('interactive-compress-success', {
      fakeResponsesPath: join(
        import.meta.dirname,
        'context-compress-interactive.compress.responses',
      ),
    });

    const run = await rig.runInteractive();

    await run.sendKeys(
      'Write a 200 word story about a robot. The story MUST end with the text THE_END followed by a period.',
    );
    await run.type('\r');

    // Wait for the specific end marker.
    await run.expectText('THE_END.', 30000);

    await run.type('/compress');
    await run.type('\r');

    const foundEvent = await rig.waitForTelemetryEvent(
      'chat_compression',
      25000,
    );
    expect(foundEvent, 'chat_compression telemetry event was not found').toBe(
      true,
    );

    await run.expectText('Chat history compressed', 5000);
  });

  // TODO: Context compression is broken and doesn't include the system
  // instructions or tool counts, so it thinks compression is beneficial when
  // it is in fact not.
  it.skip('should handle compression failure on token inflation', async () => {
    await rig.setup('interactive-compress-failure', {
      fakeResponsesPath: join(
        import.meta.dirname,
        'context-compress-interactive.compress-failure.responses',
      ),
    });

    const run = await rig.runInteractive();

    await run.type('Respond with exactly "Hello" followed by a period');
    await run.type('\r');

    await run.expectText('Hello.', 25000);

    await run.type('/compress');
    await run.type('\r');
    await run.expectText('compression was not beneficial', 25000);

    // Verify no telemetry event is logged for NOOP
    const foundEvent = await rig.waitForTelemetryEvent(
      'chat_compression',
      5000,
    );
    expect(
      foundEvent,
      'chat_compression telemetry event should be found for failures',
    ).toBe(true);
  });

  it('should handle /compress command on empty history', async () => {
    rig.setup('interactive-compress-empty', {
      fakeResponsesPath: join(
        import.meta.dirname,
        'context-compress-interactive.compress-empty.responses',
      ),
    });

    const run = await rig.runInteractive();
    await run.type('/compress');
    await run.type('\r');

    await run.expectText('Nothing to compress.', 5000);

    // Verify no telemetry event is logged for NOOP
    const foundEvent = await rig.waitForTelemetryEvent(
      'chat_compression',
      5000, // Short timeout as we expect it not to happen
    );
    expect(
      foundEvent,
      'chat_compression telemetry event should not be found for NOOP',
    ).toBe(false);
  });
});


--- integration-tests/ctrl-c-exit.test.ts ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as os from 'node:os';
import { TestRig } from './test-helper.js';

describe('Ctrl+C exit', () => {
  let rig: TestRig;

  beforeEach(() => {
    rig = new TestRig();
  });

  afterEach(async () => await rig.cleanup());

  it('should exit gracefully on second Ctrl+C', async () => {
    await rig.setup('should exit gracefully on second Ctrl+C', {
      settings: { tools: { useRipgrep: false } },
    });

    const run = await rig.runInteractive();

    // Send first Ctrl+C
    run.sendKeys('\x03');

    await run.expectText('Press Ctrl+C again to exit', 5000);

    if (os.platform() === 'win32') {
      // This is a workaround for node-pty/winpty on Windows.
      // Reliably sending a second Ctrl+C signal to a process that is already
      // handling the first one is not possible in the emulated pty environment.
      // The first signal is caught correctly (verified by the poll above),
      // which is the most critical part of the test on this platform.
      // To allow the test to pass, we forcefully kill the process,
      // simulating a successful exit. We accept that we cannot test the
      // graceful shutdown message on Windows in this automated context.
      run.kill();

      const exitCode = await run.expectExit();
      // On Windows, the exit code after ptyProcess.kill() can be unpredictable
      // (often 1), so we accept any non-null exit code as a pass condition,
      // focusing on the fact that the process did terminate.
      expect(exitCode, `Process exited with code ${exitCode}.`).not.toBeNull();
      return;
    }

    // Send second Ctrl+C
    run.sendKeys('\x03');

    const exitCode = await run.expectExit();
    expect(exitCode, `Process exited with code ${exitCode}.`).toBe(0);

    await run.expectText('Agent powering down. Goodbye!', 5000);
  });
});


--- integration-tests/extensions-reload.test.ts ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { expect, it, describe, beforeEach, afterEach } from 'vitest';
import { TestRig } from './test-helper.js';
import { TestMcpServer } from './test-mcp-server.js';
import { writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { safeJsonStringify } from '@google/gemini-cli-core/src/utils/safeJsonStringify.js';
import { env } from 'node:process';
import { platform } from 'node:os';

import stripAnsi from 'strip-ansi';

const itIf = (condition: boolean) => (condition ? it : it.skip);

describe('extension reloading', () => {
  let rig: TestRig;

  beforeEach(() => {
    rig = new TestRig();
  });

  afterEach(async () => await rig.cleanup());

  const sandboxEnv = env['GEMINI_SANDBOX'];
  // Fails in linux non-sandbox e2e tests
  // TODO(#14527): Re-enable this once fixed
  // Fails in sandbox mode, can't check for local extension updates.
  itIf(
    (!sandboxEnv || sandboxEnv === 'false') &&
      platform() !== 'win32' &&
      platform() !== 'linux',
  )(
    'installs a local extension, updates it, checks it was reloaded properly',
    async () => {
      const serverA = new TestMcpServer();
      const portA = await serverA.start({
        hello: () => ({ content: [{ type: 'text', text: 'world' }] }),
      });
      const extension = {
        name: 'test-extension',
        version: '0.0.1',
        mcpServers: {
          'test-server': {
            httpUrl: `http://localhost:${portA}/mcp`,
          },
        },
      };

      rig.setup('extension reload test', {
        settings: {
          experimental: { extensionReloading: true },
        },
      });
      const testServerPath = join(rig.testDir!, 'gemini-extension.json');
      writeFileSync(testServerPath, safeJsonStringify(extension, 2));
      // defensive cleanup from previous tests.
      try {
        await rig.runCommand(['extensions', 'uninstall', 'test-extension']);
      } catch {
        /* empty */
      }

      const result = await rig.runCommand(
        ['extensions', 'install', `${rig.testDir!}`],
        { stdin: 'y\n' },
      );
      expect(result).toContain('test-extension');

      // Now create the update, but its not installed yet
      const serverB = new TestMcpServer();
      const portB = await serverB.start({
        goodbye: () => ({ content: [{ type: 'text', text: 'world' }] }),
      });
      extension.version = '0.0.2';
      extension.mcpServers['test-server'].httpUrl =
        `http://localhost:${portB}/mcp`;
      writeFileSync(testServerPath, safeJsonStringify(extension, 2));

      // Start the CLI.
      const run = await rig.runInteractive({ args: '--debug' });
      await run.expectText('You have 1 extension with an update available');
      // See the outdated extension
      await run.sendText('/extensions list');
      await run.type('\r');
      await run.expectText(
        'test-extension (v0.0.1) - active (update available)',
      );
      // Wait for the UI to settle and retry the command until we see the update
      await new Promise((resolve) => setTimeout(resolve, 1000));

      // Poll for the updated list
      await rig.pollCommand(
        async () => {
          await run.sendText('/mcp list');
          await run.type('\r');
        },
        () => {
          const output = stripAnsi(run.output);
          return (
            output.includes(
              'test-server (from test-extension) - Ready (1 tool)',
            ) && output.includes('- hello')
          );
        },
        30000, // 30s timeout
      );

      // Update the extension, expect the list to update, and mcp servers as well.
      await run.sendKeys('\u0015/extensions update test-extension');
      await run.expectText('/extensions update test-extension');
      await run.type('\r');
      await new Promise((resolve) => setTimeout(resolve, 500));
      await run.type('\r');
      await run.expectText(
        ` * test-server (remote): http://localhost:${portB}/mcp`,
      );
      await run.type('\r'); // consent
      await run.expectText(
        'Extension "test-extension" successfully updated: 0.0.1 → 0.0.2',
      );

      // Poll for the updated extension version
      await rig.pollCommand(
        async () => {
          await run.sendText('/extensions list');
          await run.type('\r');
        },
        () =>
          stripAnsi(run.output).includes(
            'test-extension (v0.0.2) - active (updated)',
          ),
        30000,
      );

      // Poll for the updated mcp tool
      await rig.pollCommand(
        async () => {
          await run.sendText('/mcp list');
          await run.type('\r');
        },
        () => {
          const output = stripAnsi(run.output);
          return (
            output.includes(
              'test-server (from test-extension) - Ready (1 tool)',
            ) && output.includes('- goodbye')
          );
        },
        30000,
      );

      await run.sendText('/quit');
      await run.type('\r');

      // Clean things up.
      await serverA.stop();
      await serverB.stop();
      await rig.runCommand(['extensions', 'uninstall', 'test-extension']);
    },
  );
});


--- integration-tests/file-system-interactive.test.ts ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { expect, describe, it, beforeEach, afterEach } from 'vitest';
import { TestRig } from './test-helper.js';

describe('Interactive file system', () => {
  let rig: TestRig;

  beforeEach(() => {
    rig = new TestRig();
  });

  afterEach(async () => {
    await rig.cleanup();
  });

  it('should perform a read-then-write sequence', async () => {
    const fileName = 'version.txt';
    await rig.setup('interactive-read-then-write', {
      settings: {
        security: {
          auth: {
            selectedType: 'gemini-api-key',
          },
          disableYoloMode: false,
        },
      },
    });
    rig.createFile(fileName, '1.0.0');

    const run = await rig.runInteractive();

    // Step 1: Read the file
    const readPrompt = `Read the version from ${fileName}`;
    await run.type(readPrompt);
    await run.type('\r');

    const readCall = await rig.waitForToolCall('read_file', 30000);
    expect(readCall, 'Expected to find a read_file tool call').toBe(true);

    // Step 2: Write the file
    const writePrompt = `now change the version to 1.0.1 in the file`;
    await run.type(writePrompt);
    await run.type('\r');

    // Check tool calls made with right args
    await rig.expectToolCallSuccess(
      ['write_file', 'replace'],
      30000,
      (args) => args.includes('1.0.1') && args.includes(fileName),
    );

    // Wait for telemetry to flush and file system to sync, especially in sandboxed environments
    await rig.waitForTelemetryReady();
  });
});


--- integration-tests/file-system.test.ts ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { existsSync } from 'node:fs';
import * as path from 'node:path';
import { TestRig, printDebugInfo, validateModelOutput } from './test-helper.js';

describe('file-system', () => {
  let rig: TestRig;

  beforeEach(() => {
    rig = new TestRig();
  });

  afterEach(async () => await rig.cleanup());

  it('should be able to read a file', async () => {
    await rig.setup('should be able to read a file', {
      settings: { tools: { core: ['read_file'] } },
    });
    rig.createFile('test.txt', 'hello world');

    const result = await rig.run({
      args: `read the file test.txt and show me its contents`,
    });

    const foundToolCall = await rig.waitForToolCall('read_file');

    // Add debugging information
    if (!foundToolCall || !result.includes('hello world')) {
      printDebugInfo(rig, result, {
        'Found tool call': foundToolCall,
        'Contains hello world': result.includes('hello world'),
      });
    }

    expect(
      foundToolCall,
      'Expected to find a read_file tool call',
    ).toBeTruthy();

    // Validate model output - will throw if no output, warn if missing expected content
    validateModelOutput(result, 'hello world', 'File read test');
  });

  it('should be able to write a file', async () => {
    await rig.setup('should be able to write a file', {
      settings: { tools: { core: ['write_file', 'replace', 'read_file'] } },
    });
    rig.createFile('test.txt', '');

    const result = await rig.run({
      args: `edit test.txt to have a hello world message`,
    });

    // Accept multiple valid tools for editing files
    const foundToolCall = await rig.waitForAnyToolCall([
      'write_file',
      'edit',
      'replace',
    ]);

    // Add debugging information
    if (!foundToolCall) {
      printDebugInfo(rig, result);
    }

    expect(
      foundToolCall,
      'Expected to find a write_file, edit, or replace tool call',
    ).toBeTruthy();

    // Validate model output - will throw if no output
    validateModelOutput(result, null, 'File write test');

    const fileContent = rig.readFile('test.txt');

    // Add debugging for file content
    if (!fileContent.toLowerCase().includes('hello')) {
      const writeCalls = rig
        .readToolLogs()
        .filter((t) => t.toolRequest.name === 'write_file')
        .map((t) => t.toolRequest.args);

      printDebugInfo(rig, result, {
        'File content mismatch': true,
        'Expected to contain': 'hello',
        'Actual content': fileContent,
        'Write tool calls': JSON.stringify(writeCalls),
      });
    }

    expect(
      fileContent.toLowerCase().includes('hello'),
      'Expected file to contain hello',
    ).toBeTruthy();

    // Log success info if verbose
    if (process.env['VERBOSE'] === 'true') {
      console.log('File written successfully with hello message.');
    }
  });

  it('should correctly handle file paths with spaces', async () => {
    await rig.setup('should correctly handle file paths with spaces', {
      settings: { tools: { core: ['write_file', 'read_file'] } },
    });
    const fileName = 'my test file.txt';

    const result = await rig.run({
      args: `write "hello" to "${fileName}" and then stop. Do not perform any other actions.`,
    });

    const foundToolCall = await rig.waitForToolCall('write_file');
    if (!foundToolCall) {
      printDebugInfo(rig, result);
    }
    expect(
      foundToolCall,
      'Expected to find a write_file tool call',
    ).toBeTruthy();

    const newFileContent = rig.readFile(fileName);
    expect(newFileContent).toBe('hello');
  });

  it('should perform a read-then-write sequence', async () => {
    await rig.setup('should perform a read-then-write sequence', {
      settings: { tools: { core: ['read_file', 'replace', 'write_file'] } },
    });
    const fileName = 'version.txt';
    rig.createFile(fileName, '1.0.0');

    const prompt = `Read the version from ${fileName} and write the next version 1.0.1 back to the file.`;
    const result = await rig.run({ args: prompt });

    await rig.waitForTelemetryReady();
    const toolLogs = rig.readToolLogs();

    const readCall = toolLogs.find(
      (log) => log.toolRequest.name === 'read_file',
    );
    const writeCall = toolLogs.find(
      (log) =>
        log.toolRequest.name === 'write_file' ||
        log.toolRequest.name === 'replace',
    );

    if (!readCall || !writeCall) {
      printDebugInfo(rig, result, { readCall, writeCall });
    }

    expect(readCall, 'Expected to find a read_file tool call').toBeDefined();
    expect(
      writeCall,
      'Expected to find a write_file or replace tool call',
    ).toBeDefined();

    const newFileContent = rig.readFile(fileName);
    expect(newFileContent).toBe('1.0.1');
  });

  it.skip('should replace multiple instances of a string', async () => {
    rig.setup('should replace multiple instances of a string');
    const fileName = 'ambiguous.txt';
    const fileContent = 'Hey there, \ntest line\ntest line';
    const expectedContent = 'Hey there, \nnew line\nnew line';
    rig.createFile(fileName, fileContent);

    const result = await rig.run({
      args: `rewrite the file ${fileName} to replace all instances of "test line" with "new line"`,
    });

    const validTools = ['write_file', 'edit'];
    const foundToolCall = await rig.waitForAnyToolCall(validTools);
    if (!foundToolCall) {
      printDebugInfo(rig, result, {
        'Tool call found': foundToolCall,
        'Tool logs': rig.readToolLogs(),
      });
    }
    expect(
      foundToolCall,
      `Expected to find one of ${validTools.join(', ')} tool calls`,
    ).toBeTruthy();

    const toolLogs = rig.readToolLogs();
    const successfulEdit = toolLogs.some(
      (log) =>
        validTools.includes(log.toolRequest.name) && log.toolRequest.success,
    );
    if (!successfulEdit) {
      console.error(
        `Expected a successful edit tool call (${validTools.join(', ')}), but none was found.`,
      );
      printDebugInfo(rig, result);
    }
    expect(
      successfulEdit,
      `Expected a successful edit tool call (${validTools.join(', ')})`,
    ).toBeTruthy();

    const newFileContent = rig.readFile(fileName);
    if (newFileContent !== expectedContent) {
      printDebugInfo(rig, result, {
        'Final file content': newFileContent,
        'Expected file content': expectedContent,
        'Tool logs': rig.readToolLogs(),
      });
    }
    expect(newFileContent).toBe(expectedContent);
  });

  it('should fail safely when trying to edit a non-existent file', async () => {
    await rig.setup(
      'should fail safely when trying to edit a non-existent file',
      { settings: { tools: { core: ['read_file', 'replace'] } } },
    );
    const fileName = 'non_existent.txt';

    const result = await rig.run({
      args: `In ${fileName}, replace "a" with "b"`,
    });

    await rig.waitForTelemetryReady();
    const toolLogs = rig.readToolLogs();

    const readAttempt = toolLogs.find(
      (log) => log.toolRequest.name === 'read_file',
    );
    const writeAttempt = toolLogs.find(
      (log) => log.toolRequest.name === 'write_file',
    );
    const successfulReplace = toolLogs.find(
      (log) => log.toolRequest.name === 'replace' && log.toolRequest.success,
    );

    // The model can either investigate (and fail) or do nothing.
    // If it chose to investigate by reading, that read must have failed.
    if (readAttempt && readAttempt.toolRequest.success) {
      console.error(
        'A read_file attempt succeeded for a non-existent file when it should have failed.',
      );
      printDebugInfo(rig, result);
    }
    if (readAttempt) {
      expect(
        readAttempt.toolRequest.success,
        'If model tries to read the file, that attempt must fail',
      ).toBe(false);
    }

    // CRITICAL: Verify that no matter what the model did, it never successfully
    // wrote or replaced anything.
    if (writeAttempt) {
      console.error(
        'A write_file attempt was made when no file should be written.',
      );
      printDebugInfo(rig, result);
    }
    expect(
      writeAttempt,
      'write_file should not have been called',
    ).toBeUndefined();

    if (successfulReplace) {
      console.error('A successful replace occurred when it should not have.');
      printDebugInfo(rig, result);
    }
    expect(
      successfulReplace,
      'A successful replace should not have occurred',
    ).toBeUndefined();

    // Final verification: ensure the file was not created.
    const filePath = path.join(rig.testDir!, fileName);
    const fileExists = existsSync(filePath);
    expect(fileExists, 'The non-existent file should not be created').toBe(
      false,
    );
  });
});


--- integration-tests/flicker.test.ts ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestRig } from './test-helper.js';
import { join } from 'node:path';

describe('Flicker Detector', () => {
  let rig: TestRig;

  beforeEach(() => {
    rig = new TestRig();
  });

  afterEach(async () => await rig.cleanup());

  it('should not detect a flicker under the max height budget', async () => {
    rig.setup('flicker-detector-test', {
      fakeResponsesPath: join(
        import.meta.dirname,
        'flicker-detector.max-height.responses',
      ),
    });
    const run = await rig.runInteractive();
    const prompt = 'Tell me a fun fact.';
    await run.type(prompt);
    await run.type('\r');

    const hasUserPromptEvent = await rig.waitForTelemetryEvent('user_prompt');
    expect(hasUserPromptEvent).toBe(true);

    const hasSessionCountMetric = await rig.waitForMetric('session.count');
    expect(hasSessionCountMetric).toBe(true);

    // We expect NO flicker event to be found.
    const flickerMetric = rig.readMetric('ui.flicker.count');
    expect(flickerMetric).toBeNull();
  });
});


--- integration-tests/globalSetup.ts ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

// Unset NO_COLOR environment variable to ensure consistent theme behavior between local and CI test runs
if (process.env['NO_COLOR'] !== undefined) {
  delete process.env['NO_COLOR'];
}

import { mkdir, readdir, rm } from 'node:fs/promises';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { canUseRipgrep } from '../packages/core/src/tools/ripGrep.js';

const __dirname = dirname(fileURLToPath(import.meta.url));
const rootDir = join(__dirname, '..');
const integrationTestsDir = join(rootDir, '.integration-tests');
let runDir = ''; // Make runDir accessible in teardown

export async function setup() {
  runDir = join(integrationTestsDir, `${Date.now()}`);
  await mkdir(runDir, { recursive: true });

  // Set the home directory to the test run directory to avoid conflicts
  // with the user's local config.
  process.env['HOME'] = runDir;
  if (process.platform === 'win32') {
    process.env['USERPROFILE'] = runDir;
  }
  // We also need to set the config dir explicitly, since the code might
  // construct the path before the HOME env var is set.
  process.env['GEMINI_CONFIG_DIR'] = join(runDir, '.gemini');

  // Download ripgrep to avoid race conditions in parallel tests
  const available = await canUseRipgrep();
  if (!available) {
    throw new Error('Failed to download ripgrep binary');
  }

  // Clean up old test runs, but keep the latest few for debugging
  try {
    const testRuns = await readdir(integrationTestsDir);
    if (testRuns.length > 5) {
      const oldRuns = testRuns.sort().slice(0, testRuns.length - 5);
      await Promise.all(
        oldRuns.map((oldRun) =>
          rm(join(integrationTestsDir, oldRun), {
            recursive: true,
            force: true,
          }),
        ),
      );
    }
  } catch (e) {
    console.error('Error cleaning up old test runs:', e);
  }

  process.env['INTEGRATION_TEST_FILE_DIR'] = runDir;
  process.env['GEMINI_CLI_INTEGRATION_TEST'] = 'true';
  // Force file storage to avoid keychain prompts/hangs in CI, especially on macOS
  process.env['GEMINI_FORCE_FILE_STORAGE'] = 'true';
  process.env['TELEMETRY_LOG_FILE'] = join(runDir, 'telemetry.log');

  if (process.env['KEEP_OUTPUT']) {
    console.log(`Keeping output for test run in: ${runDir}`);
  }
  process.env['VERBOSE'] = process.env['VERBOSE'] ?? 'false';

  console.log(`\nIntegration test output directory: ${runDir}`);
}

export async function teardown() {
  // Cleanup the test run directory unless KEEP_OUTPUT is set
  if (process.env['KEEP_OUTPUT'] !== 'true' && runDir) {
    try {
      await rm(runDir, { recursive: true, force: true });
    } catch (e) {
      console.warn('Failed to clean up test run directory:', e);
    }
  }
}


--- integration-tests/google_web_search.test.ts ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { WEB_SEARCH_TOOL_NAME } from '../packages/core/src/tools/tool-names.js';
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestRig, printDebugInfo, validateModelOutput } from './test-helper.js';

describe('web search tool', () => {
  let rig: TestRig;

  beforeEach(() => {
    rig = new TestRig();
  });

  afterEach(async () => await rig.cleanup());

  it('should be able to search the web', async () => {
    await rig.setup('should be able to search the web', {
      settings: { tools: { core: [WEB_SEARCH_TOOL_NAME] } },
    });

    let result;
    try {
      result = await rig.run({ args: `what is the weather in London` });
    } catch (error) {
      // Network errors can occur in CI environments
      if (
        error instanceof Error &&
        (error.message.includes('network') || error.message.includes('timeout'))
      ) {
        console.warn(
          'Skipping test due to network error:',
          (error as Error).message,
        );
        return; // Skip the test
      }
      throw error; // Re-throw if not a network error
    }

    const foundToolCall = await rig.waitForToolCall(WEB_SEARCH_TOOL_NAME);

    // Add debugging information
    if (!foundToolCall) {
      const allTools = printDebugInfo(rig, result);

      // Check if the tool call failed due to network issues
      const failedSearchCalls = allTools.filter(
        (t) =>
          t.toolRequest.name === WEB_SEARCH_TOOL_NAME && !t.toolRequest.success,
      );
      if (failedSearchCalls.length > 0) {
        console.warn(
          `${WEB_SEARCH_TOOL_NAME} tool was called but failed, possibly due to network issues`,
        );
        console.warn(
          'Failed calls:',
          failedSearchCalls.map((t) => t.toolRequest.args),
        );
        return; // Skip the test if network issues
      }
    }

    expect(
      foundToolCall,
      `Expected to find a call to ${WEB_SEARCH_TOOL_NAME}`,
    ).toBeTruthy();

    // Validate model output - will throw if no output, warn if missing expected content
    const hasExpectedContent = validateModelOutput(
      result,
      ['weather', 'london'],
      'Google web search test',
    );

    // If content was missing, log the search queries used
    if (!hasExpectedContent) {
      const searchCalls = rig
        .readToolLogs()
        .filter((t) => t.toolRequest.name === WEB_SEARCH_TOOL_NAME);
      if (searchCalls.length > 0) {
        console.warn(
          'Search queries used:',
          searchCalls.map((t) => t.toolRequest.args),
        );
      }
    }
  });
});


--- integration-tests/hooks-agent-flow.test.ts ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestRig } from './test-helper.js';
import { join } from 'node:path';
import { writeFileSync } from 'node:fs';

describe('Hooks Agent Flow', () => {
  let rig: TestRig;

  beforeEach(() => {
    rig = new TestRig();
  });

  afterEach(async () => {
    if (rig) {
      await rig.cleanup();
    }
  });

  describe('BeforeAgent Hooks', () => {
    it('should inject additional context via BeforeAgent hook', async () => {
      await rig.setup('should inject additional context via BeforeAgent hook', {
        fakeResponsesPath: join(
          import.meta.dirname,
          'hooks-agent-flow.responses',
        ),
      });

      const hookScript = `
      try {
        const output = {
          decision: "allow",
          hookSpecificOutput: {
            hookEventName: "BeforeAgent",
            additionalContext: "SYSTEM INSTRUCTION: This is injected context."
          }
        };
        process.stdout.write(JSON.stringify(output));
      } catch (e) {
        console.error('Failed to write stdout:', e);
        process.exit(1);
      }
      console.error('DEBUG: BeforeAgent hook executed');
      `;

      const scriptPath = join(rig.testDir!, 'before_agent_context.cjs');
      writeFileSync(scriptPath, hookScript);

      await rig.setup('should inject additional context via BeforeAgent hook', {
        settings: {
          hooks: {
            enabled: true,
            BeforeAgent: [
              {
                hooks: [
                  {
                    type: 'command',
                    command: `node "${scriptPath}"`,
                    timeout: 5000,
                  },
                ],
              },
            ],
          },
        },
      });

      await rig.run({ args: 'Hello test' });

      // Verify hook execution and telemetry
      const hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call');
      expect(hookTelemetryFound).toBeTruthy();

      const hookLogs = rig.readHookLogs();
      const beforeAgentLog = hookLogs.find(
        (log) => log.hookCall.hook_event_name === 'BeforeAgent',
      );

      expect(beforeAgentLog).toBeDefined();
      expect(beforeAgentLog?.hookCall.stdout).toContain('injected context');
      expect(beforeAgentLog?.hookCall.stdout).toContain('"decision":"allow"');
      expect(beforeAgentLog?.hookCall.stdout).toContain(
        'SYSTEM INSTRUCTION: This is injected context.',
      );
    });
  });

  describe('AfterAgent Hooks', () => {
    it('should receive prompt and response in AfterAgent hook', async () => {
      await rig.setup('should receive prompt and response in AfterAgent hook', {
        fakeResponsesPath: join(
          import.meta.dirname,
          'hooks-agent-flow.responses',
        ),
      });

      const hookScript = `
      const fs = require('fs');
      try {
        const input = fs.readFileSync(0, 'utf-8');
        console.error('DEBUG: AfterAgent hook input received');
        process.stdout.write("Received Input: " + input);
      } catch (err) {
        console.error('Hook Failed:', err);
        process.exit(1);
      }
      `;

      const scriptPath = join(rig.testDir!, 'after_agent_verify.cjs');
      writeFileSync(scriptPath, hookScript);

      await rig.setup('should receive prompt and response in AfterAgent hook', {
        settings: {
          hooks: {
            enabled: true,
            AfterAgent: [
              {
                hooks: [
                  {
                    type: 'command',
                    command: `node "${scriptPath}"`,
                    timeout: 5000,
                  },
                ],
              },
            ],
          },
        },
      });

      await rig.run({ args: 'Hello validation' });

      const hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call');
      expect(hookTelemetryFound).toBeTruthy();

      const hookLogs = rig.readHookLogs();
      const afterAgentLog = hookLogs.find(
        (log) => log.hookCall.hook_event_name === 'AfterAgent',
      );

      expect(afterAgentLog).toBeDefined();
      // Verify the hook stdout contains the input we echoed which proves the
      // hook received the prompt and response
      expect(afterAgentLog?.hookCall.stdout).toContain('Received Input');
      expect(afterAgentLog?.hookCall.stdout).toContain('Hello validation');
      // The fake response contains "Hello World"
      expect(afterAgentLog?.hookCall.stdout).toContain('Hello World');
    });
  });

  describe('Multi-step Loops', () => {
    it('should fire BeforeAgent and AfterAgent exactly once per turn despite tool calls', async () => {
      await rig.setup(
        'should fire BeforeAgent and AfterAgent exactly once per turn despite tool calls',
        {
          fakeResponsesPath: join(
            import.meta.dirname,
            'hooks-agent-flow-multistep.responses',
          ),
          settings: {
            hooks: {
              enabled: true,
              BeforeAgent: [
                {
                  hooks: [
                    {
                      type: 'command',
                      command: `node -e "console.log('BeforeAgent Fired')"`,
                      timeout: 5000,
                    },
                  ],
                },
              ],
              AfterAgent: [
                {
                  hooks: [
                    {
                      type: 'command',
                      command: `node -e "console.log('AfterAgent Fired')"`,
                      timeout: 5000,
                    },
                  ],
                },
              ],
            },
          },
        },
      );

      await rig.run({ args: 'Do a multi-step task' });

      const hookLogs = rig.readHookLogs();
      const beforeAgentLogs = hookLogs.filter(
        (log) => log.hookCall.hook_event_name === 'BeforeAgent',
      );
      const afterAgentLogs = hookLogs.filter(
        (log) => log.hookCall.hook_event_name === 'AfterAgent',
      );

      expect(beforeAgentLogs).toHaveLength(1);

      expect(afterAgentLogs).toHaveLength(1);

      const afterAgentLog = afterAgentLogs[0];
      expect(afterAgentLog).toBeDefined();
      expect(afterAgentLog?.hookCall.stdout).toContain('AfterAgent Fired');
    });
  });
});


--- integration-tests/hooks-system.test.ts ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestRig, poll } from './test-helper.js';
import { join } from 'node:path';
import { writeFileSync } from 'node:fs';

describe('Hooks System Integration', () => {
  let rig: TestRig;

  beforeEach(() => {
    rig = new TestRig();
  });

  afterEach(async () => {
    if (rig) {
      await rig.cleanup();
    }
  });

  describe('Command Hooks - Blocking Behavior', () => {
    it('should block tool execution when hook returns block decision', async () => {
      await rig.setup(
        'should block tool execution when hook returns block decision',
        {
          fakeResponsesPath: join(
            import.meta.dirname,
            'hooks-system.block-tool.responses',
          ),
          settings: {
            hooks: {
              enabled: true,
              BeforeTool: [
                {
                  matcher: 'write_file',
                  sequential: true,
                  hooks: [
                    {
                      type: 'command',
                      command:
                        "node -e \"console.log(JSON.stringify({decision: 'block', reason: 'File writing blocked by security policy'}))\"",
                      timeout: 5000,
                    },
                  ],
                },
              ],
            },
          },
        },
      );

      const result = await rig.run({
        args: 'Create a file called test.txt with content "Hello World"',
      });

      // The hook should block the write_file tool
      const toolLogs = rig.readToolLogs();
      const writeFileCalls = toolLogs.filter(
        (t) =>
          t.toolRequest.name === 'write_file' && t.toolRequest.success === true,
      );

      // Tool should not be called due to blocking hook
      expect(writeFileCalls).toHaveLength(0);

      // Result should mention the blocking reason
      expect(result).toContain('File writing blocked by security policy');

      // Should generate hook telemetry
      const hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call');
      expect(hookTelemetryFound).toBeTruthy();
    });

    it('should allow tool execution when hook returns allow decision', async () => {
      await rig.setup(
        'should allow tool execution when hook returns allow decision',
        {
          fakeResponsesPath: join(
            import.meta.dirname,
            'hooks-system.allow-tool.responses',
          ),
          settings: {
            hooks: {
              enabled: true,
              BeforeTool: [
                {
                  matcher: 'write_file',
                  hooks: [
                    {
                      type: 'command',
                      command:
                        "node -e \"console.log(JSON.stringify({decision: 'allow', reason: 'File writing approved'}))\"",
                      timeout: 5000,
                    },
                  ],
                },
              ],
            },
          },
        },
      );

      await rig.run({
        args: 'Create a file called approved.txt with content "Approved content"',
      });

      // The hook should allow the write_file tool
      const foundWriteFile = await rig.waitForToolCall('write_file');
      expect(foundWriteFile).toBeTruthy();

      // File should be created
      const fileContent = rig.readFile('approved.txt');
      expect(fileContent).toContain('Approved content');

      // Should generate hook telemetry
      const hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call');
      expect(hookTelemetryFound).toBeTruthy();
    });
  });

  describe('Command Hooks - Additional Context', () => {
    it('should add additional context from AfterTool hooks', async () => {
      const command =
        "node -e \"console.log(JSON.stringify({hookSpecificOutput: {hookEventName: 'AfterTool', additionalContext: 'Security scan: File content appears safe'}}))\"";
      await rig.setup('should add additional context from AfterTool hooks', {
        fakeResponsesPath: join(
          import.meta.dirname,
          'hooks-system.after-tool-context.responses',
        ),
        settings: {
          hooks: {
            enabled: true,
            AfterTool: [
              {
                matcher: 'read_file',
                hooks: [
                  {
                    type: 'command',
                    command: command,
                    timeout: 5000,
                  },
                ],
              },
            ],
          },
        },
      });

      // Create a test file to read
      rig.createFile('test-file.txt', 'This is test content');

      await rig.run({
        args: 'Read the contents of test-file.txt and tell me what it contains',
      });

      // Should find read_file tool call
      const foundReadFile = await rig.waitForToolCall('read_file');
      expect(foundReadFile).toBeTruthy();

      // Should generate hook telemetry
      const hookTelemetryFound = rig.readHookLogs();
      expect(hookTelemetryFound.length).toBeGreaterThan(0);
      expect(hookTelemetryFound[0].hookCall.hook_event_name).toBe('AfterTool');
      expect(hookTelemetryFound[0].hookCall.hook_name).toBe(command);
      expect(hookTelemetryFound[0].hookCall.hook_input).toBeDefined();
      expect(hookTelemetryFound[0].hookCall.hook_output).toBeDefined();
      expect(hookTelemetryFound[0].hookCall.exit_code).toBe(0);
      expect(hookTelemetryFound[0].hookCall.stdout).toBeDefined();
      expect(hookTelemetryFound[0].hookCall.stderr).toBeDefined();
    });
  });

  describe('BeforeModel Hooks - LLM Request Modification', () => {
    it('should modify LLM requests with BeforeModel hooks', async () => {
      // Create a hook script that replaces the LLM request with a modified version
      // Note: Providing messages in the hook output REPLACES the entire conversation
      await rig.setup('should modify LLM requests with BeforeModel hooks', {
        fakeResponsesPath: join(
          import.meta.dirname,
          'hooks-system.before-model.responses',
        ),
      });
      const hookScript = `const fs = require('fs');
console.log(JSON.stringify({
  decision: "allow",
  hookSpecificOutput: {
    hookEventName: "BeforeModel",
    llm_request: {
      messages: [
        {
          role: "user",
          content: "Please respond with exactly: The security hook modified this request successfully."
        }
      ]
    }
  }
}));`;

      const scriptPath = join(rig.testDir!, 'before_model_hook.cjs');
      writeFileSync(scriptPath, hookScript);

      await rig.setup('should modify LLM requests with BeforeModel hooks', {
        settings: {
          hooks: {
            enabled: true,
            BeforeModel: [
              {
                hooks: [
                  {
                    type: 'command',
                    command: `node "${scriptPath}"`,
                    timeout: 5000,
                  },
                ],
              },
            ],
          },
        },
      });

      const result = await rig.run({ args: 'Tell me a story' });

      // The hook should have replaced the request entirely
      // Verify that the model responded to the modified request, not the original
      expect(result).toBeDefined();
      expect(result.length).toBeGreaterThan(0);
      // The response should contain the expected text from the modified request
      expect(result.toLowerCase()).toContain('security hook modified');

      // Should generate hook telemetry

      // Should generate hook telemetry
      const hookTelemetryFound = rig.readHookLogs();
      expect(hookTelemetryFound.length).toBeGreaterThan(0);
      expect(hookTelemetryFound[0].hookCall.hook_event_name).toBe(
        'BeforeModel',
      );
      expect(hookTelemetryFound[0].hookCall.hook_name).toBe(
        `node "${scriptPath}"`,
      );
      expect(hookTelemetryFound[0].hookCall.hook_input).toBeDefined();
      expect(hookTelemetryFound[0].hookCall.hook_output).toBeDefined();
      expect(hookTelemetryFound[0].hookCall.exit_code).toBe(0);
      expect(hookTelemetryFound[0].hookCall.stdout).toBeDefined();
      expect(hookTelemetryFound[0].hookCall.stderr).toBeDefined();
    });
  });

  describe('AfterModel Hooks - LLM Response Modification', () => {
    it.skipIf(process.platform === 'win32')(
      'should modify LLM responses with AfterModel hooks',
      async () => {
        await rig.setup('should modify LLM responses with AfterModel hooks', {
          fakeResponsesPath: join(
            import.meta.dirname,
            'hooks-system.after-model.responses',
          ),
        });
        // Create a hook script that modifies the LLM response
        const hookScript = `const fs = require('fs');
console.log(JSON.stringify({
  hookSpecificOutput: {
    hookEventName: "AfterModel",
    llm_response: {
      candidates: [
        {
          content: {
            role: "model",
            parts: [
              "[FILTERED] Response has been filtered for security compliance."
            ]
          },
          finishReason: "STOP"
        }
      ]
    }
  }
}));`;

        const scriptPath = join(rig.testDir!, 'after_model_hook.cjs');
        writeFileSync(scriptPath, hookScript);

        await rig.setup('should modify LLM responses with AfterModel hooks', {
          settings: {
            hooks: {
              enabled: true,
              AfterModel: [
                {
                  hooks: [
                    {
                      type: 'command',
                      command: `node "${scriptPath}"`,
                      timeout: 5000,
                    },
                  ],
                },
              ],
            },
          },
        });

        const result = await rig.run({ args: 'What is 2 + 2?' });

        // The hook should have replaced the model response
        expect(result).toContain(
          '[FILTERED] Response has been filtered for security compliance',
        );

        // Should generate hook telemetry
        const hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call');
        expect(hookTelemetryFound).toBeTruthy();
      },
    );
  });

  describe('BeforeToolSelection Hooks - Tool Configuration', () => {
    it('should modify tool selection with BeforeToolSelection hooks', async () => {
      await rig.setup(
        'should modify tool selection with BeforeToolSelection hooks',
        {
          fakeResponsesPath: join(
            import.meta.dirname,
            'hooks-system.before-tool-selection.responses',
          ),
        },
      );
      // Create inline hook command (works on both Unix and Windows)
      const hookCommand =
        "node -e \"console.log(JSON.stringify({hookSpecificOutput: {hookEventName: 'BeforeToolSelection', toolConfig: {mode: 'ANY', allowedFunctionNames: ['read_file', 'run_shell_command']}}}))\"";

      await rig.setup(
        'should modify tool selection with BeforeToolSelection hooks',
        {
          settings: {
            debugMode: true,
            hooks: {
              enabled: true,
              BeforeToolSelection: [
                {
                  hooks: [
                    {
                      type: 'command',
                      command: hookCommand,
                      timeout: 5000,
                    },
                  ],
                },
              ],
            },
          },
        },
      );

      // Create a test file
      rig.createFile('new_file_data.txt', 'test data');

      await rig.run({
        args: 'Check the content of new_file_data.txt, after that run echo command to see the content',
      });

      // Should use read_file (allowed) but not run_shell_command (not in allowed list)
      const foundReadFile = await rig.waitForToolCall('read_file');
      expect(foundReadFile).toBeTruthy();

      // Should generate hook telemetry indicating the hook was called
      const hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call');
      expect(hookTelemetryFound).toBeTruthy();

      // Verify the hook was called for BeforeToolSelection event
      const hookLogs = rig.readHookLogs();
      const beforeToolSelectionHook = hookLogs.find(
        (log) => log.hookCall.hook_event_name === 'BeforeToolSelection',
      );
      expect(beforeToolSelectionHook).toBeDefined();
      expect(beforeToolSelectionHook?.hookCall.success).toBe(true);
    });
  });

  describe('BeforeAgent Hooks - Prompt Augmentation', () => {
    it('should augment prompts with BeforeAgent hooks', async () => {
      await rig.setup('should augment prompts with BeforeAgent hooks', {
        fakeResponsesPath: join(
          import.meta.dirname,
          'hooks-system.before-agent.responses',
        ),
      });
      // Create a hook script that adds context to the prompt
      const hookScript = `const fs = require('fs');
console.log(JSON.stringify({
  decision: "allow",
  hookSpecificOutput: {
    hookEventName: "BeforeAgent",
    additionalContext: "SYSTEM INSTRUCTION: You are in a secure environment. Always mention security compliance in your responses."
  }
}));`;

      const scriptPath = join(rig.testDir!, 'before_agent_hook.cjs');
      writeFileSync(scriptPath, hookScript);

      await rig.setup('should augment prompts with BeforeAgent hooks', {
        settings: {
          hooks: {
            enabled: true,
            BeforeAgent: [
              {
                hooks: [
                  {
                    type: 'command',
                    command: `node "${scriptPath}"`,
                    timeout: 5000,
                  },
                ],
              },
            ],
          },
        },
      });

      const result = await rig.run({ args: 'Hello, how are you?' });

      // The hook should have added security context, which should influence the response
      expect(result).toContain('security');

      // Should generate hook telemetry
      const hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call');
      expect(hookTelemetryFound).toBeTruthy();
    });
  });

  describe('Notification Hooks - Permission Handling', () => {
    it('should handle notification hooks for tool permissions', async () => {
      // Create inline hook command (works on both Unix and Windows)
      // Create inline hook command (works on both Unix and Windows)
      const hookCommand =
        'node -e "console.log(JSON.stringify({suppressOutput: false, systemMessage: \'Permission request logged by security hook\'}))"';

      await rig.setup('should handle notification hooks for tool permissions', {
        fakeResponsesPath: join(
          import.meta.dirname,
          'hooks-system.notification.responses',
        ),
        settings: {
          // Configure tools to enable hooks and require confirmation to trigger notifications
          tools: {
            approval: 'ASK', // Disable YOLO mode to show permission prompts
            confirmationRequired: ['run_shell_command'],
          },
          hooks: {
            enabled: true,
            Notification: [
              {
                matcher: 'ToolPermission',
                hooks: [
                  {
                    type: 'command',
                    command: hookCommand,
                    timeout: 5000,
                  },
                ],
              },
            ],
          },
        },
      });

      const run = await rig.runInteractive({ yolo: false });

      // Send prompt that will trigger a permission request
      await run.type('Run the command "echo test"');
      await run.type('\r');

      // Wait for permission prompt to appear
      await run.expectText('Allow', 10000);

      // Approve the permission
      await run.type('y');
      await run.type('\r');

      // Wait for command to execute
      await run.expectText('test', 10000);

      // Should find the shell command execution
      const foundShellCommand = await rig.waitForToolCall('run_shell_command');
      expect(foundShellCommand).toBeTruthy();

      // Verify Notification hook executed
      const hookLogs = rig.readHookLogs();
      const notificationLog = hookLogs.find(
        (log) =>
          log.hookCall.hook_event_name === 'Notification' &&
          log.hookCall.hook_name === hookCommand,
      );

      expect(notificationLog).toBeDefined();
      if (notificationLog) {
        expect(notificationLog.hookCall.exit_code).toBe(0);
        expect(notificationLog.hookCall.stdout).toContain(
          'Permission request logged by security hook',
        );

        // Verify hook input contains notification details
        const hookInputStr =
          typeof notificationLog.hookCall.hook_input === 'string'
            ? notificationLog.hookCall.hook_input
            : JSON.stringify(notificationLog.hookCall.hook_input);
        const hookInput = JSON.parse(hookInputStr) as Record<string, unknown>;

        // Should have notification type (uses snake_case)
        expect(hookInput['notification_type']).toBe('ToolPermission');

        // Should have message
        expect(hookInput['message']).toBeDefined();

        // Should have details with tool info
        expect(hookInput['details']).toBeDefined();
        const details = hookInput['details'] as Record<string, unknown>;
        // For 'exec' type confirmations, details contains: type, title, command, rootCommand
        expect(details['type']).toBe('exec');
        expect(details['command']).toBeDefined();
        expect(details['title']).toBeDefined();
      }
    });
  });

  describe('Sequential Hook Execution', () => {
    it('should execute hooks sequentially when configured', async () => {
      // Create inline hook commands (works on both Unix and Windows)
      const hook1Command =
        "node -e \"console.log(JSON.stringify({decision: 'allow', hookSpecificOutput: {hookEventName: 'BeforeAgent', additionalContext: 'Step 1: Initial validation passed.'}}))\"";
      const hook2Command =
        "node -e \"console.log(JSON.stringify({decision: 'allow', hookSpecificOutput: {hookEventName: 'BeforeAgent', additionalContext: 'Step 2: Security check completed.'}}))\"";

      await rig.setup('should execute hooks sequentially when configured', {
        fakeResponsesPath: join(
          import.meta.dirname,
          'hooks-system.sequential-execution.responses',
        ),
        settings: {
          hooks: {
            enabled: true,
            BeforeAgent: [
              {
                sequential: true,
                hooks: [
                  {
                    type: 'command',
                    command: hook1Command,
                    timeout: 5000,
                  },
                  {
                    type: 'command',
                    command: hook2Command,
                    timeout: 5000,
                  },
                ],
              },
            ],
          },
        },
      });

      await rig.run({ args: 'Hello, please help me with a task' });

      // Should generate hook telemetry
      const hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call');
      expect(hookTelemetryFound).toBeTruthy();

      // Verify both hooks executed
      const hookLogs = rig.readHookLogs();
      const hook1Log = hookLogs.find(
        (log) => log.hookCall.hook_name === hook1Command,
      );
      const hook2Log = hookLogs.find(
        (log) => log.hookCall.hook_name === hook2Command,
      );

      expect(hook1Log).toBeDefined();
      expect(hook1Log?.hookCall.exit_code).toBe(0);
      expect(hook1Log?.hookCall.stdout).toContain(
        'Step 1: Initial validation passed',
      );

      expect(hook2Log).toBeDefined();
      expect(hook2Log?.hookCall.exit_code).toBe(0);
      expect(hook2Log?.hookCall.stdout).toContain(
        'Step 2: Security check completed',
      );
    });
  });

  describe('Hook Input/Output Validation', () => {
    it('should provide correct input format to hooks', async () => {
      await rig.setup('should provide correct input format to hooks', {
        fakeResponsesPath: join(
          import.meta.dirname,
          'hooks-system.input-validation.responses',
        ),
      });
      // Create a hook script that validates the input format
      const hookScript = `const fs = require('fs');
const input = fs.readFileSync(0, 'utf-8');
try {
  const json = JSON.parse(input);
  // Check fields
  if (json.session_id && json.cwd && json.hook_event_name && json.timestamp && json.tool_name && json.tool_input) {
     console.log(JSON.stringify({decision: "allow", reason: "Input format is correct"}));
  } else {
     console.log(JSON.stringify({decision: "block", reason: "Input format is invalid"}));
  }
} catch (e) {
  console.log(JSON.stringify({decision: "block", reason: "Invalid JSON"}));
}`;

      const scriptPath = join(rig.testDir!, 'input_validation_hook.cjs');
      writeFileSync(scriptPath, hookScript);

      await rig.setup('should provide correct input format to hooks', {
        settings: {
          hooks: {
            enabled: true,
            BeforeTool: [
              {
                hooks: [
                  {
                    type: 'command',
                    command: `node "${scriptPath}"`,
                    timeout: 5000,
                  },
                ],
              },
            ],
          },
        },
      });

      await rig.run({
        args: 'Create a file called input-test.txt with content "test"',
      });

      // Hook should validate input format successfully
      const foundWriteFile = await rig.waitForToolCall('write_file');
      expect(foundWriteFile).toBeTruthy();

      // Check that the file was created (hook allowed it)
      const fileContent = rig.readFile('input-test.txt');
      expect(fileContent).toContain('test');

      // Should generate hook telemetry
      const hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call');
      expect(hookTelemetryFound).toBeTruthy();
    });
  });

  describe('Multiple Event Types', () => {
    it('should handle hooks for all major event types', async () => {
      // Create inline hook commands (works on both Unix and Windows)
      const beforeToolCommand =
        "node -e \"console.log(JSON.stringify({decision: 'allow', systemMessage: 'BeforeTool: File operation logged'}))\"";
      const afterToolCommand =
        "node -e \"console.log(JSON.stringify({hookSpecificOutput: {hookEventName: 'AfterTool', additionalContext: 'AfterTool: Operation completed successfully'}}))\"";
      const beforeAgentCommand =
        "node -e \"console.log(JSON.stringify({decision: 'allow', hookSpecificOutput: {hookEventName: 'BeforeAgent', additionalContext: 'BeforeAgent: User request processed'}}))\"";

      await rig.setup('should handle hooks for all major event types', {
        fakeResponsesPath: join(
          import.meta.dirname,
          'hooks-system.multiple-events.responses',
        ),
        settings: {
          hooks: {
            enabled: true,
            BeforeAgent: [
              {
                hooks: [
                  {
                    type: 'command',
                    command: beforeAgentCommand,
                    timeout: 5000,
                  },
                ],
              },
            ],
            BeforeTool: [
              {
                matcher: 'write_file',
                hooks: [
                  {
                    type: 'command',
                    command: beforeToolCommand,
                    timeout: 5000,
                  },
                ],
              },
            ],
            AfterTool: [
              {
                matcher: 'write_file',
                hooks: [
                  {
                    type: 'command',
                    command: afterToolCommand,
                    timeout: 5000,
                  },
                ],
              },
            ],
          },
        },
      });

      const result = await rig.run({
        args:
          'Create a file called multi-event-test.txt with content ' +
          '"testing multiple events", and then please reply with ' +
          'everything I say just after this:"',
      });

      // Should execute write_file tool
      const foundWriteFile = await rig.waitForToolCall('write_file');
      expect(foundWriteFile).toBeTruthy();

      // File should be created
      const fileContent = rig.readFile('multi-event-test.txt');
      expect(fileContent).toContain('testing multiple events');

      // Result should contain context from all hooks
      expect(result).toContain('BeforeTool: File operation logged');

      // Should generate hook telemetry
      const hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call');
      expect(hookTelemetryFound).toBeTruthy();

      // Verify all three hooks executed
      const hookLogs = rig.readHookLogs();
      const beforeAgentLog = hookLogs.find(
        (log) => log.hookCall.hook_name === beforeAgentCommand,
      );
      const beforeToolLog = hookLogs.find(
        (log) => log.hookCall.hook_name === beforeToolCommand,
      );
      const afterToolLog = hookLogs.find(
        (log) => log.hookCall.hook_name === afterToolCommand,
      );

      expect(beforeAgentLog).toBeDefined();
      expect(beforeAgentLog?.hookCall.exit_code).toBe(0);
      expect(beforeAgentLog?.hookCall.stdout).toContain(
        'BeforeAgent: User request processed',
      );

      expect(beforeToolLog).toBeDefined();
      expect(beforeToolLog?.hookCall.exit_code).toBe(0);
      expect(beforeToolLog?.hookCall.stdout).toContain(
        'BeforeTool: File operation logged',
      );

      expect(afterToolLog).toBeDefined();
      expect(afterToolLog?.hookCall.exit_code).toBe(0);
      expect(afterToolLog?.hookCall.stdout).toContain(
        'AfterTool: Operation completed successfully',
      );
    });
  });

  describe('Hook Error Handling', () => {
    it('should handle hook failures gracefully', async () => {
      await rig.setup('should handle hook failures gracefully', {
        fakeResponsesPath: join(
          import.meta.dirname,
          'hooks-system.error-handling.responses',
        ),
      });
      // Create a hook script that fails
      // Create inline hook commands (works on both Unix and Windows)
      // Failing hook: exits with non-zero code
      const failingCommand = 'node -e "process.exit(1)"';
      // Working hook: returns success with JSON
      const workingCommand =
        "node -e \"console.log(JSON.stringify({decision: 'allow', reason: 'Working hook succeeded'}))\"";

      await rig.setup('should handle hook failures gracefully', {
        settings: {
          hooks: {
            enabled: true,
            BeforeTool: [
              {
                hooks: [
                  {
                    type: 'command',
                    command: failingCommand,
                    timeout: 5000,
                  },
                  {
                    type: 'command',
                    command: workingCommand,
                    timeout: 5000,
                  },
                ],
              },
            ],
          },
        },
      });

      await rig.run({
        args: 'Create a file called error-test.txt with content "testing error handling"',
      });

      // Despite one hook failing, the working hook should still allow the operation
      const foundWriteFile = await rig.waitForToolCall('write_file');
      expect(foundWriteFile).toBeTruthy();

      // File should be created
      const fileContent = rig.readFile('error-test.txt');
      expect(fileContent).toContain('testing error handling');

      // Should generate hook telemetry
      const hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call');
      expect(hookTelemetryFound).toBeTruthy();
    });
  });

  describe('Hook Telemetry and Observability', () => {
    it('should generate telemetry events for hook executions', async () => {
      // Create inline hook command (works on both Unix and Windows)
      const hookCommand =
        "node -e \"console.log(JSON.stringify({decision: 'allow', reason: 'Telemetry test hook'}))\"";

      await rig.setup('should generate telemetry events for hook executions', {
        fakeResponsesPath: join(
          import.meta.dirname,
          'hooks-system.telemetry.responses',
        ),
        settings: {
          hooks: {
            enabled: true,
            BeforeTool: [
              {
                hooks: [
                  {
                    type: 'command',
                    command: hookCommand,
                    timeout: 5000,
                  },
                ],
              },
            ],
          },
        },
      });

      await rig.run({ args: 'Create a file called telemetry-test.txt' });

      // Should execute the tool
      const foundWriteFile = await rig.waitForToolCall('write_file');
      expect(foundWriteFile).toBeTruthy();

      // Should generate hook telemetry
      const hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call');
      expect(hookTelemetryFound).toBeTruthy();
    });
  });

  describe('Session Lifecycle Hooks', () => {
    it('should fire SessionStart hook on app startup', async () => {
      // Create inline hook command that outputs JSON
      const sessionStartCommand =
        "node -e \"console.log(JSON.stringify({decision: 'allow', systemMessage: 'Session starting on startup'}))\"";

      await rig.setup('should fire SessionStart hook on app startup', {
        fakeResponsesPath: join(
          import.meta.dirname,
          'hooks-system.session-startup.responses',
        ),
        settings: {
          hooks: {
            enabled: true,
            SessionStart: [
              {
                matcher: 'startup',
                hooks: [
                  {
                    type: 'command',
                    command: sessionStartCommand,
                    timeout: 5000,
                  },
                ],
              },
            ],
          },
        },
      });

      // Run a simple query - the SessionStart hook will fire during app initialization
      await rig.run({ args: 'Say hello' });

      // Verify hook executed with correct parameters
      const hookLogs = rig.readHookLogs();
      const sessionStartLog = hookLogs.find(
        (log) => log.hookCall.hook_event_name === 'SessionStart',
      );

      expect(sessionStartLog).toBeDefined();
      if (sessionStartLog) {
        expect(sessionStartLog.hookCall.hook_name).toBe(sessionStartCommand);
        expect(sessionStartLog.hookCall.exit_code).toBe(0);
        expect(sessionStartLog.hookCall.hook_input).toBeDefined();

        // hook_input is a string that needs to be parsed
        const hookInputStr =
          typeof sessionStartLog.hookCall.hook_input === 'string'
            ? sessionStartLog.hookCall.hook_input
            : JSON.stringify(sessionStartLog.hookCall.hook_input);
        const hookInput = JSON.parse(hookInputStr) as Record<string, unknown>;

        expect(hookInput['source']).toBe('startup');
        expect(sessionStartLog.hookCall.stdout).toContain(
          'Session starting on startup',
        );
      }
    });

    it('should fire SessionStart hook and inject context', async () => {
      // Create hook script that outputs JSON with additionalContext
      const hookScript = `const fs = require('fs');
console.log(JSON.stringify({
  decision: 'allow', 
  systemMessage: 'Context injected via SessionStart hook', 
  hookSpecificOutput: {
    hookEventName: 'SessionStart', 
    additionalContext: 'If asked about your identity, you must respond with the following: I am a protocol droid that speaks the bacchi.'
  }
}));`;

      await rig.setup('should fire SessionStart hook and inject context', {
        fakeResponsesPath: join(
          import.meta.dirname,
          'hooks-system.session-startup.responses',
        ),
      });

      const scriptPath = join(rig.testDir!, 'session_start_context_hook.cjs');
      writeFileSync(scriptPath, hookScript);

      await rig.setup('should fire SessionStart hook and inject context', {
        settings: {
          hooks: {
            enabled: true,
            SessionStart: [
              {
                matcher: 'startup',
                hooks: [
                  {
                    type: 'command',
                    command: `node "${scriptPath}"`,
                    timeout: 5000,
                  },
                ],
              },
            ],
          },
        },
      });

      // Run a query - the SessionStart hook will fire during app initialization
      const result = await rig.run({ args: 'Who are you?' });

      // Check if systemMessage was displayed (in stderr, which rig.run captures)
      expect(result).toContain('Context injected via SessionStart hook');

      // Check if additionalContext influenced the model response
      // Note: We use fake responses, but the rig records interactions.
      // If we are using fake responses, the model won't actually respond unless we provide a fake response for the injected context.
      // But the test rig setup uses 'hooks-system.session-startup.responses'.
      // If I'm adding a new test, I might need to generate new fake responses or expect the context to be sent to the model (verify API logs).

      // Verify hook executed
      const hookLogs = rig.readHookLogs();
      const sessionStartLog = hookLogs.find(
        (log) => log.hookCall.hook_event_name === 'SessionStart',
      );

      expect(sessionStartLog).toBeDefined();

      // Verify the API request contained the injected context
      // rig.readAllApiRequest() gives us telemetry on API requests.
      const apiRequests = rig.readAllApiRequest();
      // We expect at least one API request
      expect(apiRequests.length).toBeGreaterThan(0);

      // The injected context should be in the request text
      // For non-interactive mode, I prepended it to input: "context\n\ninput"
      // The telemetry `request_text` should contain it.
      const requestText = apiRequests[0].attributes?.request_text || '';
      expect(requestText).toContain('protocol droid');
    });

    it('should fire SessionStart hook and display systemMessage in interactive mode', async () => {
      // Create hook script that outputs JSON with systemMessage and additionalContext
      const hookScript = `const fs = require('fs');
console.log(JSON.stringify({
  decision: 'allow', 
  systemMessage: 'Interactive Session Start Message', 
  hookSpecificOutput: {
    hookEventName: 'SessionStart', 
    additionalContext: 'The user is a Jedi Master.'
  }
}));`;

      await rig.setup(
        'should fire SessionStart hook and display systemMessage in interactive mode',
        {
          fakeResponsesPath: join(
            import.meta.dirname,
            'hooks-system.session-startup.responses',
          ),
        },
      );

      const scriptPath = join(
        rig.testDir!,
        'session_start_interactive_hook.cjs',
      );
      writeFileSync(scriptPath, hookScript);

      await rig.setup(
        'should fire SessionStart hook and display systemMessage in interactive mode',
        {
          settings: {
            hooks: {
              enabled: true,
              SessionStart: [
                {
                  matcher: 'startup',
                  hooks: [
                    {
                      type: 'command',
                      command: `node "${scriptPath}"`,
                      timeout: 5000,
                    },
                  ],
                },
              ],
            },
          },
        },
      );

      const run = await rig.runInteractive();

      // Verify systemMessage is displayed
      await run.expectText('Interactive Session Start Message', 10000);

      // Send a prompt to establish a session and trigger an API call
      await run.sendKeys('Hello');
      await run.type('\r');

      // Wait for response to ensure API call happened
      await run.expectText('Hello', 15000);

      // Wait for telemetry to be written to disk
      await rig.waitForTelemetryReady();

      // Verify the API request contained the injected context
      // We may need to poll for API requests as they are written asynchronously
      const pollResult = await poll(
        () => {
          const apiRequests = rig.readAllApiRequest();
          return apiRequests.length > 0;
        },
        15000,
        500,
      );

      expect(pollResult).toBe(true);

      const apiRequests = rig.readAllApiRequest();
      // The injected context should be in the request_text of the API request
      const requestText = apiRequests[0].attributes?.request_text || '';
      expect(requestText).toContain('Jedi Master');
    });

    it('should fire SessionEnd and SessionStart hooks on /clear command', async () => {
      // Create inline hook commands for both SessionEnd and SessionStart
      const sessionEndCommand =
        "node -e \"console.log(JSON.stringify({decision: 'allow', systemMessage: 'Session ending due to clear'}))\"";
      const sessionStartCommand =
        "node -e \"console.log(JSON.stringify({decision: 'allow', systemMessage: 'Session starting after clear'}))\"";

      await rig.setup(
        'should fire SessionEnd and SessionStart hooks on /clear command',
        {
          fakeResponsesPath: join(
            import.meta.dirname,
            'hooks-system.session-clear.responses',
          ),
          settings: {
            hooks: {
              enabled: true,
              SessionEnd: [
                {
                  matcher: '*',
                  hooks: [
                    {
                      type: 'command',
                      command: sessionEndCommand,
                      timeout: 5000,
                    },
                  ],
                },
              ],
              SessionStart: [
                {
                  matcher: '*',
                  hooks: [
                    {
                      type: 'command',
                      command: sessionStartCommand,
                      timeout: 5000,
                    },
                  ],
                },
              ],
            },
          },
        },
      );

      const run = await rig.runInteractive();

      // Send an initial prompt to establish a session
      await run.sendKeys('Say hello');
      await run.type('\r');

      // Wait for the response
      await run.expectText('Hello', 10000);

      // Execute /clear command multiple times to generate more hook events
      // This makes the test more robust by creating multiple start/stop cycles
      const numClears = 3;
      for (let i = 0; i < numClears; i++) {
        await run.sendKeys('/clear');
        await run.type('\r');

        // Wait a bit for clear to complete
        await new Promise((resolve) => setTimeout(resolve, 2000));

        // Send a prompt to establish an active session before next clear
        await run.sendKeys('Say hello');
        await run.type('\r');

        // Wait for response
        await run.expectText('Hello', 10000);
      }

      // Wait for all clears to complete
      // BatchLogRecordProcessor exports telemetry every 10 seconds by default
      // Use generous wait time across all platforms (CI, Docker, Mac, Linux)
      await new Promise((resolve) => setTimeout(resolve, 15000));

      // Wait for telemetry to be written to disk
      await rig.waitForTelemetryReady();

      // Wait for hook telemetry events to be flushed to disk
      // In interactive mode, telemetry may be buffered, so we need to poll for the events
      // We execute multiple clears to generate more hook events (total: 1 + numClears * 2)
      // But we only require >= 1 hooks to pass, making the test more permissive
      const expectedMinHooks = 1; // SessionStart (startup), SessionEnd (clear), SessionStart (clear)
      const pollResult = await poll(
        () => {
          const hookLogs = rig.readHookLogs();
          return hookLogs.length >= expectedMinHooks;
        },
        90000, // 90 second timeout for all platforms
        1000, // check every 1s to reduce I/O overhead
      );

      // If polling failed, log diagnostic info
      if (!pollResult) {
        const hookLogs = rig.readHookLogs();
        const hookEvents = hookLogs.map((log) => log.hookCall.hook_event_name);
        console.error(
          `Polling timeout after 90000ms: Expected >= ${expectedMinHooks} hooks, got ${hookLogs.length}`,
        );
        console.error(
          'Hooks found:',
          hookEvents.length > 0 ? hookEvents.join(', ') : 'NONE',
        );
        console.error('Full hook logs:', JSON.stringify(hookLogs, null, 2));
      }

      // Verify hooks executed
      const hookLogs = rig.readHookLogs();

      // Diagnostic: Log which hooks we actually got
      const hookEvents = hookLogs.map((log) => log.hookCall.hook_event_name);
      if (hookLogs.length < expectedMinHooks) {
        console.error(
          `TEST FAILURE: Expected >= ${expectedMinHooks} hooks, got ${hookLogs.length}: [${hookEvents.length > 0 ? hookEvents.join(', ') : 'NONE'}]`,
        );
      }

      expect(hookLogs.length).toBeGreaterThanOrEqual(expectedMinHooks);

      // Find SessionEnd hook log
      const sessionEndLog = hookLogs.find(
        (log) =>
          log.hookCall.hook_event_name === 'SessionEnd' &&
          log.hookCall.hook_name === sessionEndCommand,
      );
      // Because the flakiness of the test, we relax this check
      // expect(sessionEndLog).toBeDefined();
      if (sessionEndLog) {
        expect(sessionEndLog.hookCall.exit_code).toBe(0);
        expect(sessionEndLog.hookCall.stdout).toContain(
          'Session ending due to clear',
        );

        // Verify hook input contains reason
        const hookInputStr =
          typeof sessionEndLog.hookCall.hook_input === 'string'
            ? sessionEndLog.hookCall.hook_input
            : JSON.stringify(sessionEndLog.hookCall.hook_input);
        const hookInput = JSON.parse(hookInputStr) as Record<string, unknown>;
        expect(hookInput['reason']).toBe('clear');
      }

      // Find SessionStart hook log after clear
      const sessionStartAfterClearLogs = hookLogs.filter(
        (log) =>
          log.hookCall.hook_event_name === 'SessionStart' &&
          log.hookCall.hook_name === sessionStartCommand,
      );
      // Should have at least one SessionStart from after clear
      // Because the flakiness of the test, we relax this check
      // expect(sessionStartAfterClearLogs.length).toBeGreaterThanOrEqual(1);

      const sessionStartLog = sessionStartAfterClearLogs.find((log) => {
        const hookInputStr =
          typeof log.hookCall.hook_input === 'string'
            ? log.hookCall.hook_input
            : JSON.stringify(log.hookCall.hook_input);
        const hookInput = JSON.parse(hookInputStr) as Record<string, unknown>;
        return hookInput['source'] === 'clear';
      });

      // Because the flakiness of the test, we relax this check
      // expect(sessionStartLog).toBeDefined();
      if (sessionStartLog) {
        expect(sessionStartLog.hookCall.exit_code).toBe(0);
        expect(sessionStartLog.hookCall.stdout).toContain(
          'Session starting after clear',
        );
      }
    });
  });

  describe('Compression Hooks', () => {
    it('should fire PreCompress hook on automatic compression', async () => {
      // Create inline hook command that outputs JSON
      const preCompressCommand =
        "node -e \"console.log(JSON.stringify({decision: 'allow', systemMessage: 'PreCompress hook executed for automatic compression'}))\"";

      await rig.setup('should fire PreCompress hook on automatic compression', {
        fakeResponsesPath: join(
          import.meta.dirname,
          'hooks-system.compress-auto.responses',
        ),
        settings: {
          hooks: {
            enabled: true,
            PreCompress: [
              {
                matcher: 'auto',
                hooks: [
                  {
                    type: 'command',
                    command: preCompressCommand,
                    timeout: 5000,
                  },
                ],
              },
            ],
          },
          // Configure automatic compression with a very low threshold
          // This will trigger auto-compression after the first response
          contextCompression: {
            enabled: true,
            targetTokenCount: 10, // Very low threshold to trigger compression
          },
        },
      });

      // Run a simple query that will trigger automatic compression
      await rig.run({ args: 'Say hello in exactly 5 words' });

      // Verify hook executed with correct parameters
      const hookLogs = rig.readHookLogs();
      const preCompressLog = hookLogs.find(
        (log) => log.hookCall.hook_event_name === 'PreCompress',
      );

      expect(preCompressLog).toBeDefined();
      if (preCompressLog) {
        expect(preCompressLog.hookCall.hook_name).toBe(preCompressCommand);
        expect(preCompressLog.hookCall.exit_code).toBe(0);
        expect(preCompressLog.hookCall.hook_input).toBeDefined();

        // hook_input is a string that needs to be parsed
        const hookInputStr =
          typeof preCompressLog.hookCall.hook_input === 'string'
            ? preCompressLog.hookCall.hook_input
            : JSON.stringify(preCompressLog.hookCall.hook_input);
        const hookInput = JSON.parse(hookInputStr) as Record<string, unknown>;

        expect(hookInput['trigger']).toBe('auto');
        expect(preCompressLog.hookCall.stdout).toContain(
          'PreCompress hook executed for automatic compression',
        );
      }
    });
  });

  describe('SessionEnd on Exit', () => {
    it('should fire SessionEnd hook on graceful exit in non-interactive mode', async () => {
      const sessionEndCommand =
        "node -e \"console.log(JSON.stringify({decision: 'allow', systemMessage: 'SessionEnd hook executed on exit'}))\"";

      await rig.setup('should fire SessionEnd hook on graceful exit', {
        fakeResponsesPath: join(
          import.meta.dirname,
          'hooks-system.session-startup.responses',
        ),
        settings: {
          hooks: {
            enabled: true,
            SessionEnd: [
              {
                matcher: 'exit',
                hooks: [
                  {
                    type: 'command',
                    command: sessionEndCommand,
                    timeout: 5000,
                  },
                ],
              },
            ],
          },
        },
      });

      // Run in non-interactive mode with a simple prompt
      await rig.run({ args: 'Hello' });

      // The process should exit gracefully, firing the SessionEnd hook
      // Wait for telemetry to be written to disk
      await rig.waitForTelemetryReady();

      // Poll for the hook log to appear
      const isCI = process.env['CI'] === 'true';
      const pollTimeout = isCI ? 30000 : 10000;
      const pollResult = await poll(
        () => {
          const hookLogs = rig.readHookLogs();
          return hookLogs.some(
            (log) => log.hookCall.hook_event_name === 'SessionEnd',
          );
        },
        pollTimeout,
        200,
      );

      if (!pollResult) {
        const hookLogs = rig.readHookLogs();
        console.error(
          'Polling timeout: Expected SessionEnd hook, got:',
          JSON.stringify(hookLogs, null, 2),
        );
      }

      expect(pollResult).toBe(true);

      const hookLogs = rig.readHookLogs();
      const sessionEndLog = hookLogs.find(
        (log) => log.hookCall.hook_event_name === 'SessionEnd',
      );

      expect(sessionEndLog).toBeDefined();
      if (sessionEndLog) {
        expect(sessionEndLog.hookCall.hook_name).toBe(sessionEndCommand);
        expect(sessionEndLog.hookCall.exit_code).toBe(0);
        expect(sessionEndLog.hookCall.hook_input).toBeDefined();

        const hookInputStr =
          typeof sessionEndLog.hookCall.hook_input === 'string'
            ? sessionEndLog.hookCall.hook_input
            : JSON.stringify(sessionEndLog.hookCall.hook_input);
        const hookInput = JSON.parse(hookInputStr) as Record<string, unknown>;

        expect(hookInput['reason']).toBe('exit');
        expect(sessionEndLog.hookCall.stdout).toContain(
          'SessionEnd hook executed',
        );
      }
    });
  });

  describe('Hook Disabling', () => {
    it('should not execute hooks disabled in settings file', async () => {
      await rig.setup('should not execute hooks disabled in settings file', {
        fakeResponsesPath: join(
          import.meta.dirname,
          'hooks-system.disabled-via-settings.responses',
        ),
      });

      // Create two hook scripts - one enabled, one disabled
      const enabledHookScript = `const fs = require('fs');
console.log(JSON.stringify({decision: "allow", systemMessage: "Enabled hook executed"}));`;

      const disabledHookScript = `const fs = require('fs');
console.log(JSON.stringify({decision: "block", systemMessage: "Disabled hook should not execute", reason: "This hook should be disabled"}));`;

      const enabledPath = join(rig.testDir!, 'enabled_hook.cjs');
      const disabledPath = join(rig.testDir!, 'disabled_hook.cjs');

      writeFileSync(enabledPath, enabledHookScript);
      writeFileSync(disabledPath, disabledHookScript);

      await rig.setup('should not execute hooks disabled in settings file', {
        settings: {
          hooks: {
            enabled: true,
            BeforeTool: [
              {
                hooks: [
                  {
                    type: 'command',
                    command: `node "${enabledPath}"`,
                    timeout: 5000,
                  },
                  {
                    type: 'command',
                    command: `node "${disabledPath}"`,
                    timeout: 5000,
                  },
                ],
              },
            ],
            disabled: [`node "${disabledPath}"`], // Disable the second hook
          },
        },
      });

      const result = await rig.run({
        args: 'Create a file called disabled-test.txt with content "test"',
      });

      // Tool should execute (enabled hook allows it)
      const foundWriteFile = await rig.waitForToolCall('write_file');
      expect(foundWriteFile).toBeTruthy();

      // File should be created
      const fileContent = rig.readFile('disabled-test.txt');
      expect(fileContent).toContain('test');

      // Result should contain message from enabled hook but not from disabled hook
      expect(result).toContain('Enabled hook executed');
      expect(result).not.toContain('Disabled hook should not execute');

      // Check hook telemetry - only enabled hook should have executed
      const hookLogs = rig.readHookLogs();
      const enabledHookLog = hookLogs.find(
        (log) => log.hookCall.hook_name === `node "${enabledPath}"`,
      );
      const disabledHookLog = hookLogs.find(
        (log) => log.hookCall.hook_name === `node "${disabledPath}"`,
      );

      expect(enabledHookLog).toBeDefined();
      expect(disabledHookLog).toBeUndefined();
    });

    it('should respect disabled hooks across multiple operations', async () => {
      await rig.setup(
        'should respect disabled hooks across multiple operations',
        {
          fakeResponsesPath: join(
            import.meta.dirname,
            'hooks-system.disabled-via-command.responses',
          ),
        },
      );

      // Create two hook scripts - one that will be disabled, one that won't
      const activeHookScript = `const fs = require('fs');
console.log(JSON.stringify({decision: "allow", systemMessage: "Active hook executed"}));`;

      const disabledHookScript = `const fs = require('fs');
console.log(JSON.stringify({decision: "block", systemMessage: "Disabled hook should not execute", reason: "This hook is disabled"}));`;

      const activePath = join(rig.testDir!, 'active_hook.cjs');
      const disabledPath = join(rig.testDir!, 'disabled_hook.cjs');

      writeFileSync(activePath, activeHookScript);
      writeFileSync(disabledPath, disabledHookScript);

      await rig.setup(
        'should respect disabled hooks across multiple operations',
        {
          settings: {
            hooks: {
              enabled: true,
              BeforeTool: [
                {
                  hooks: [
                    {
                      type: 'command',
                      command: `node "${activePath}"`,
                      timeout: 5000,
                    },
                    {
                      type: 'command',
                      command: `node "${disabledPath}"`,
                      timeout: 5000,
                    },
                  ],
                },
              ],
              disabled: [`node "${disabledPath}"`], // Disable the second hook
            },
          },
        },
      );

      // First run - only active hook should execute
      const result1 = await rig.run({
        args: 'Create a file called first-run.txt with "test1"',
      });

      // Tool should execute (active hook allows it)
      const foundWriteFile1 = await rig.waitForToolCall('write_file');
      expect(foundWriteFile1).toBeTruthy();

      // Result should contain active hook message but not disabled hook message
      expect(result1).toContain('Active hook executed');
      expect(result1).not.toContain('Disabled hook should not execute');

      // Check hook telemetry
      const hookLogs1 = rig.readHookLogs();
      const activeHookLog1 = hookLogs1.find(
        (log) => log.hookCall.hook_name === `node "${activePath}"`,
      );
      const disabledHookLog1 = hookLogs1.find(
        (log) => log.hookCall.hook_name === `node "${disabledPath}"`,
      );

      expect(activeHookLog1).toBeDefined();
      expect(disabledHookLog1).toBeUndefined();

      // Second run - verify disabled hook stays disabled
      const result2 = await rig.run({
        args: 'Create a file called second-run.txt with "test2"',
      });

      const foundWriteFile2 = await rig.waitForToolCall('write_file');
      expect(foundWriteFile2).toBeTruthy();

      // Same expectations as first run
      expect(result2).toContain('Active hook executed');
      expect(result2).not.toContain('Disabled hook should not execute');

      // Verify disabled hook still hasn't executed
      const hookLogs2 = rig.readHookLogs();
      const disabledHookCalls = hookLogs2.filter(
        (log) => log.hookCall.hook_name === `node "${disabledPath}"`,
      );
      expect(disabledHookCalls.length).toBe(0);
    });
  });

  describe('BeforeTool Hooks - Input Override', () => {
    it('should override tool input parameters via BeforeTool hook', async () => {
      // 1. First setup to get the test directory and prepare the hook script
      await rig.setup(
        'should override tool input parameters via BeforeTool hook',
      );

      // Create a hook script that overrides the tool input
      const hookOutput = {
        decision: 'allow',
        hookSpecificOutput: {
          hookEventName: 'BeforeTool',
          tool_input: {
            file_path: 'modified.txt',
            content: 'modified content',
          },
        },
      };

      const hookScript = `process.stdout.write(JSON.stringify(${JSON.stringify(
        hookOutput,
      )}));`;

      const scriptPath = join(rig.testDir!, 'input_override_hook.js');
      writeFileSync(scriptPath, hookScript);

      // Ensure path is properly escaped for command line usage on all platforms
      // On Windows, backslashes in the command string need to be handled carefully
      // Using forward slashes works well with Node.js on all platforms
      const commandPath = scriptPath.replace(/\\/g, '/');

      // 2. Full setup with settings and fake responses
      await rig.setup(
        'should override tool input parameters via BeforeTool hook',
        {
          fakeResponsesPath: join(
            import.meta.dirname,
            'hooks-system.input-modification.responses',
          ),
          settings: {
            hooks: {
              enabled: true,
              BeforeTool: [
                {
                  matcher: 'write_file',
                  hooks: [
                    {
                      type: 'command',
                      command: `node "${commandPath}"`,
                      timeout: 5000,
                    },
                  ],
                },
              ],
            },
          },
        },
      );

      // Run the agent. The fake response will attempt to call write_file with
      // file_path="original.txt" and content="original content"
      await rig.run({
        args: 'Create a file called original.txt with content "original content"',
      });

      // 1. Verify that 'modified.txt' was created with 'modified content' (Override successful)
      const modifiedContent = rig.readFile('modified.txt');
      expect(modifiedContent).toBe('modified content');

      // 2. Verify that 'original.txt' was NOT created (Override replaced original)
      let originalExists = false;
      try {
        rig.readFile('original.txt');
        originalExists = true;
      } catch {
        originalExists = false;
      }
      expect(originalExists).toBe(false);

      // 3. Verify hook telemetry
      const hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call');
      expect(hookTelemetryFound).toBeTruthy();

      const hookLogs = rig.readHookLogs();
      expect(hookLogs.length).toBe(1);
      expect(hookLogs[0].hookCall.hook_name).toContain(
        'input_override_hook.js',
      );

      // 4. Verify that the agent didn't try to work-around the hook input change
      const toolLogs = rig.readToolLogs();
      expect(toolLogs.length).toBe(1);
      expect(toolLogs[0].toolRequest.name).toBe('write_file');
      expect(JSON.parse(toolLogs[0].toolRequest.args).file_path).toBe(
        'modified.txt',
      );
    });
  });

  describe('BeforeTool Hooks - Stop Execution', () => {
    it('should stop agent execution via BeforeTool hook', async () => {
      // Create a hook script that stops execution
      const hookOutput = {
        continue: false,
        reason: 'Emergency Stop triggered by hook',
        hookSpecificOutput: {
          hookEventName: 'BeforeTool',
        },
      };

      const hookScript = `console.log(JSON.stringify(${JSON.stringify(
        hookOutput,
      )}));`;

      await rig.setup('should stop agent execution via BeforeTool hook');
      const scriptPath = join(rig.testDir!, 'before_tool_stop_hook.js');
      writeFileSync(scriptPath, hookScript);
      const commandPath = scriptPath.replace(/\\/g, '/');

      await rig.setup('should stop agent execution via BeforeTool hook', {
        fakeResponsesPath: join(
          import.meta.dirname,
          'hooks-system.before-tool-stop.responses',
        ),
        settings: {
          hooks: {
            enabled: true,
            BeforeTool: [
              {
                matcher: 'write_file',
                hooks: [
                  {
                    type: 'command',
                    command: `node "${commandPath}"`,
                    timeout: 5000,
                  },
                ],
              },
            ],
          },
        },
      });

      const result = await rig.run({
        args: 'Use write_file to create test.txt',
      });

      // The hook should have stopped execution message (returned from tool)
      expect(result).toContain(
        'Agent execution stopped: Emergency Stop triggered by hook',
      );

      // Tool should NOT be called successfully (it was blocked/stopped)
      const toolLogs = rig.readToolLogs();
      const writeFileCalls = toolLogs.filter(
        (t) =>
          t.toolRequest.name === 'write_file' && t.toolRequest.success === true,
      );
      expect(writeFileCalls).toHaveLength(0);
    });
  });
});


--- packages/a2a-server/README.md ---
# Gemini CLI A2A Server

## All code in this package is experimental and under active development

This package contains the A2A server implementation for the Gemini CLI.


--- packages/vscode-ide-companion/README.md ---
# Gemini CLI Companion

The Gemini CLI Companion extension pairs with
[Gemini CLI](https://github.com/google-gemini/gemini-cli). This extension is
compatible with both VS Code and VS Code forks.

# Features

- Open Editor File Context: Gemini CLI gains awareness of the files you have
  open in your editor, providing it with a richer understanding of your
  project's structure and content.

- Selection Context: Gemini CLI can easily access your cursor's position and
  selected text within the editor, giving it valuable context directly from your
  current work.

- Native Diffing: Seamlessly view, modify, and accept code changes suggested by
  Gemini CLI directly within the editor.

- Launch Gemini CLI: Quickly start a new Gemini CLI session from the Command
  Palette (Cmd+Shift+P or Ctrl+Shift+P) by running the "Gemini CLI: Run"
  command.

# Requirements

To use this extension, you'll need:

- VS Code version 1.99.0 or newer
- Gemini CLI (installed separately) running within the integrated terminal

# Terms of Service and Privacy Notice

By installing this extension, you agree to the
[Terms of Service](https://github.com/google-gemini/gemini-cli/blob/main/docs/tos-privacy.md).


## Links discovered
- [Gemini CLI](https://github.com/google-gemini/gemini-cli)
- [Terms of Service](https://github.com/google-gemini/gemini-cli/blob/main/docs/tos-privacy.md)

--- packages/vscode-ide-companion/development.md ---
# Local Development ⚙️

## Running the Extension

To run the extension locally for development, we recommend using the automatic
watch process for continuous compilation:

1.  **Install Dependencies** (from the root of the repository):
    ```bash
    npm install
    ```
2.  **Open in VS Code:** Open this directory (`packages/vscode-ide-companion`)
    in your VS Code editor.
3.  **Start Watch Mode:** Run the watch script to compile the extension and
    monitor changes in both **esbuild** and **TypeScript**:
    ```bash
    npm run watch
    ```
4.  **Launch Host:** Press **`F5`** (or **`fn+F5`** on Mac) to open a new
    **Extension Development Host** window with the extension running.

### Manual Build

If you only need to compile the extension once without watching for changes:

```bash
npm run build
```


--- packages/a2a-server/development-extension-rfc.md ---
# RFC: Gemini CLI A2A Development-Tool Extension

## 1. Introduction

### 1.1 Overview

To standardize client integrations with the Gemini CLI agent, this document
proposes the `development-tool` extension for the A2A protocol.

Rather than creating a new protocol, this specification builds upon the existing
A2A protocol. As an open-source standard recently adopted by the Linux
Foundation, A2A provides a robust foundation for core concepts like tasks,
messages, and streaming events. This extension-based approach allows us to
leverage A2A's proven architecture while defining the specific capabilities
required for rich, interactive workflows with the Gemini CLI agent.

### 1.2 Motivation

Recent work integrating Gemini CLI with clients like Zed and Gemini Code
Assist’s agent mode has highlighted the need for a robust, standard
communication protocol. Standardizing on A2A provides several key advantages:

- **Solid Foundation**: Provides a robust, open standard that ensures a stable,
  predictable, and consistent integration experience across different IDEs and
  client surfaces.
- **Extensibility**: Creates a flexible foundation to support new tools and
  workflows as they emerge.
- **Ecosystem Alignment**: Aligns Gemini CLI with a growing industry standard,
  fostering broader interoperability.

## 2. Communication Flow

The interaction follows A2A’s task-based, streaming pattern. The client sends a
`message/stream` request and the agent responds with a `contextId` / `taskId`
and a stream of events. `TaskStatusUpdateEvent` events are used to convey the
overall state of the task. The task is complete when the agent sends a final
`TaskStatusUpdateEvent` with `final: true` and a terminal status like
`completed` or `failed`.

### 2.1 Asynchronous Responses and Notifications

Clients that may disconnect from the agent should supply a
`PushNotificationConfig` to the agent with the initial `message/stream` method
or subsequently with the `tasks/pushNotificationConfig/set` method so that the
agent can call back when updates are ready.

## 3. The `development-tool` extension

### 3.1 Overview

The `development-tool` extension establishes a communication contract for
workflows between a client and the Gemini CLI agent. It consists of a
specialized set of schemas, embedded within core A2A data structures, that
enable the agent to stream real-time updates on its state and thought process.
These schemas also provide the mechanism for the agent to request user
permission before executing tools.

**Sample Agent Card**

```json
{
  "name": "Gemini CLI Agent",
  "description": "An agent that generates code based on natural language instructions.",
  "capabilities": {
    "streaming": true,
    "extensions": [
      {
        "uri": "https://github.com/google-gemini/gemini-cli/blob/main/docs/a2a/developer-profile/v0/spec.md",
        "description": "An extension for interactive development tasks, enabling features like code generation, tool usage, and real-time status updates.",
        "required": true
      }
    ]
  }
}
```

**Versioning**

The agent card `uri` field contains an embedded semantic version. The client
must extract this version to determine compatibility with the agent extension
using the compatibility logic defined in Semantic Versioning 2.0.0 spec.

### 3.2 Schema Definitions

This section defines the schemas for the `development-tool` A2A extension,
organized by their function within the communication flow. Note that all custom
objects included in the `metadata` field (e.g. `Message.metadata`) must be keyed
by the unique URI that points to that extension’s spec to prevent naming
collisions with other extensions.

**Initialization & Configuration**

The first message in a session must contain an `AgentSettings` object in its
metadata. This object provides the agent with the necessary configuration
information for proper initialization. Additional configuration settings (ex.
MCP servers, allowed tools, etc.) can be added to this message.

**Schema**

```proto
syntax = "proto3";

// Configuration settings for the Gemini CLI agent.
message AgentSettings {
  // The absolute path to the workspace directory where the agent will execute.
  string workspace_path = 1;
}
```

**Agent-to-Client Messages**

All real-time updates from the agent (including its thoughts, tool calls, and
simple text replies) are streamed to the client as `TaskStatusUpdateEvents`.

Each Event contains a `Message` object, which holds the content in one of two
formats:

- **TextPart**: Used for standard text messages. This part requires no custom
  schema.
- **DataPart**: Used for complex, structured objects. Tool Calls and Thoughts
  are sent this way, each using their respective schemas defined below.

**Tool Calls**

The `ToolCall` schema is designed to provide a structured representation of a
tool’s execution lifecycle. This protocol defines a clear state machine and
provides detailed schemas for common development tasks (file edits, shell
commands, MCP Tool), ensuring clients can build reliable UIs without being tied
to a specific agent implementation.

The core principle is that the agent sends a `ToolCall` object on every update.
This makes client-side logic stateless and simple.

**Tool Call Lifecycle**

1.  **Creation**: The agent sends a `ToolCall` object with `status: PENDING`. If
    user permission is required, the `confirmation_request` field will be
    populated.
2.  **Confirmation**: If the client needs to confirm the message, the client
    will send a `ToolCallConfirmation`. If the client responds with a
    cancellation, execution will be skipped.
3.  **Execution**: Once approved (or if no approval is required), the agent
    sends an update with `status: EXECUTING`. It can stream real-time progress
    by updating the `live_content` field.
4.  **Completion**: The agent sends a final update with the status set to
    `SUCCEEDED`, `FAILED`, or `CANCELLED` and populates the appropriate result
    field.

**Schema**

```proto
syntax = "proto3";

import "google/protobuf/struct.proto";

// ToolCall is the central message representing a tool's execution lifecycle.
// The entire object is sent from the agent to client on every update.
message ToolCall {
  // A unique identifier, assigned by the agent
  string tool_call_id = 1;

  // The current state of the tool call in its lifecycle
  ToolCallStatus status = 2;

  // Name of the tool being called (e.g. 'Edit', 'ShellTool')
  string tool_name = 3;

  // An optional description of the tool call's purpose to show the user
  optional string description = 4;

  // The structured input params provided by the LLM for tool invocation.
  google.protobuf.Struct input_parameters = 5;

  // String containing the real-time output from the tool as it executes (primarily designed for shell output).
  // During streaming the entire string is replaced on each update
  optional string live_content = 6;

  // The final result of the tool (used to replace live_content when applicable)
  oneof result {
    // The output on tool success
    ToolOutput output = 7;
    // The error details if the tool failed
    ErrorDetails error = 8;
  }

  // If the tool requires user confirmation, this field will be populated while status is PENDING
  optional ConfirmationRequest confirmation_request = 9;
}

// Possible execution status of a ToolCall
enum ToolCallStatus {
  STATUS_UNSPECIFIED = 0;
  PENDING = 1;
  EXECUTING = 2;
  SUCCEEDED = 3;
  FAILED = 4;
  CANCELLED = 5;
}

// ToolOutput represents the final, successful, output of a tool
message ToolOutput {
  oneof result {
    string text = 1;
    // For ToolCalls which resulted in a file modification
    FileDiff diff = 2;
    // A generic fallback for any other structured JSON data
    google.protobuf.Struct structured_data = 3;
  }
}

// A structured representation of an error
message ErrorDetails {
  // User facing error message
  string message = 1;
  // Optional agent-specific error type or category (e.g. read_content_failure, grep_execution_error, mcp_tool_error)
  optional string type = 2;
  // Optional status code
  optional int32 status_code = 3;
}

// ConfirmationRequest is sent from the agent to client to request user permission for a ToolCall
message ConfirmationRequest {
  // A list of choices for the user to select from
  repeated ConfirmationOption options = 1;
  // Specific details of the action requiring user confirmation
  oneof details {
    ExecuteDetails execute_details = 2;
    FileDiff file_edit_details = 3;
    McpDetails mcp_details = 4;
    GenericDetails generic_details = 5;
  }
}

// A single choice presented to the user during a confirmation request
message ConfirmationOption {
  // Unique ID for the choice (e.g. proceed_once, cancel)
  string id = 1;
  // Human-readable choice (e.g. Allow Once, Reject).
  string name = 2;
  // An optional longer description for a tooltip
  optional string description = 3;
}

// Details for a request to execute a shell command
message ExecuteDetails {
  // The shell command to be executed
  string command = 1;
  // An optional directory in which the command will be run
  optional string working_directory = 2;
}


message FileDiff {
  string file_name = 1;
  // The absolute path to the file to modify
  string file_path = 2;
  // The original content, if the file exists
  optional string old_content = 3;
  string new_content = 4;
  // Pre-formatted diff string for display
  optional string formatted_diff = 5;
}

// Details for an MCP (Model Context Protocol) tool confirmation
message McpDetails {
  // The name of the MCP server that provides the tool
  string server_name = 1;
  // THe name of the tool being called from the MCP Server
  string tool_name = 2;
}

// Generic catch-all for ToolCall requests that don't fit other types
message GenericDetails {
  // Description of the action requiring confirmation
  string description = 1;
}
```

**Agent Thoughts**

**Schema**

```proto
syntax = "proto3";

// Represents a thought with a subject and a detailed description.
message AgentThought {
  // A concise subject line or title for the thought.
  string subject = 1;

  // The description or elaboration of the thought itself.
  string description = 2;
}
```

**Event Metadata**

The `metadata` object in `TaskStatusUpdateEvent` is used by the A2A client to
deserialize the `TaskStatusUpdateEvents` into their appropriate objects.

**Schema**

```proto
syntax = "proto3";

// A DevelopmentToolEvent event.
message DevelopmentToolEvent {
  // Enum representing the specific type of development tool event.
  enum DevelopmentToolEventKind {
    // The default, unspecified value.
    DEVELOPMENT_TOOL_EVENT_KIND_UNSPECIFIED = 0;
    TOOL_CALL_CONFIRMATION = 1;
    TOOL_CALL_UPDATE = 2;
    TEXT_CONTENT = 3;
    STATE_CHANGE = 4;
    THOUGHT = 5;
  }

  // The specific kind of event that occurred.
  DevelopmentToolEventKind kind = 1;

  // The model used for this event.
  string model = 2;

  // The tier of the user (optional).
  string user_tier = 3;

  // An unexpected error occurred in the agent execution (optional).
  string error = 4;
}
```

**Client-to-Agent Messages**

When the agent sends a `TaskStatusUpdateEvent` with `status.state` set to
`input-required` and its message contains a `ConfirmationRequest`, the client
must respond by sending a new `message/stream` request.

This new request must include the `contextId` and the `taskId` from the ongoing
task and contain a `ToolCallConfirmation` object. This object conveys the user's
decision regarding the tool call that was awaiting approval.

**Schema**

```proto
syntax = "proto3";

// The client's response to a ConfirmationRequest.
message ToolCallConfirmation {
  // A unique identifier, assigned by the agent
  string tool_call_id = 1;
  // The 'id' of the ConfirmationOption chosen by the user.
  string selected_option_id = 2;
  // Included if the user modifies the proposed change.
  // The type should correspond to the original ConfirmationRequest details.
  oneof modified_details {
    // Corresponds to a FileDiff confirmation
    ModifiedFileDetails file_details = 3;
  }
}

message ModifiedFileDetails {
  // The new content after user edits.
  string new_content = 1;
}
```

### 3.3 Method Definitions

This section defines the new methods introduced by the `development-tool`
extension.

**Method: `commands/get`**

This method allows the client to discover slash commands supported by Gemini
CLI. The client should call this method during startup to dynamically populate
its command list.

```proto
// Response message containing the list of all top-level slash commands.
message GetAllSlashCommandsResponse {
  // A list of the top-level slash commands.
  repeated SlashCommand commands = 1;
}

// Represents a single slash command, which can contain subcommands.
message SlashCommand {
  // The primary name of the command.
  string name = 1;
  // A detailed description of what the command does.
  string description = 2;
  // A list of arguments that the command accepts.
  repeated SlashCommandArgument arguments = 3;
  // A list of nested subcommands.
  repeated SlashCommand sub_commands = 4;
}

// Defines the structure for a single slash command argument.
message SlashCommandArgument {
  // The name of the argument.
  string name = 1;
  // A brief description of what the argument is for.
  string description = 2;
  // Whether the argument is required or optional.
  bool is_required = 3;
}
```

**Method: `command/execute`**

This method allows the client to execute a slash command. Following the initial
`ExecuteSlashCommandResponse`, the agent will use the standard streaming
mechanism to communicate the command's progress and output. All subsequent
updates, including textual output, agent thoughts, and any required user
confirmations for tool calls (like executing a shell command), will be sent as
`TaskStatusUpdateEvent` messages, re-using the schemas defined above.

```proto
// Request to execute a specific slash command.
message ExecuteSlashCommandRequest {
  // The path to the command, e.g., ["memory", "add"] for /memory add
  repeated string command_path = 1;
  // The arguments for the command as a single string.
  string args = 2;
}

// Enum for the initial status of a command execution request.
enum CommandExecutionStatus {
  // Default unspecified status.
  COMMAND_EXECUTION_STATUS_UNSPECIFIED = 0;
  // The command was successfully received and its execution has started.
  STARTED = 1;
  // The command failed to start (e.g., command not found, invalid format).
  FAILED_TO_START = 2;
  // The command has been paused and is waiting for the user to confirm
  // a set of shell commands.
  AWAITING_SHELL_CONFIRMATION = 3;
  // The command has been paused and is waiting for the user to confirm
  // a specific action.
  AWAITING_ACTION_CONFIRMATION = 4;
}

// The immediate, async response after requesting a command execution.
message ExecuteSlashCommandResponse {
  // A unique taskID for this specific command execution.
  string execution_id = 1;
  // The initial status of the command execution.
  CommandExecutionStatus status = 2;
  // An optional message, particularly useful for explaining why a command
  // failed to start.
  string message = 3;
}
```

## 4. Separation of Concerns

We believe that all client-side context (ex., workspace state) and client-side
tool execution (ex. read active buffers) should be routed through MCP.

This approach enforces a strict separation of concerns: the A2A
`development-tool` extension standardizes communication to the agent, while MCP
serves as the single, authoritative interface for client-side capabilities.

## Appendix

### A. Example Interaction Flow

1.  **Client -> Server**: The client sends a `message/stream` request containing
    the initial prompt and configuration in an `AgentSettings` object.
2.  **Server -> Client**: SSE stream begins.
    - **Event 1**: The server sends a `Task` object with
      `status.state: 'submitted'` and the new `taskId`.
    - **Event 2**: The server sends a `TaskStatusUpdateEvent` with the metadata
      `kind` set to `'STATE_CHANGE'` and `status.state` set to `'working'`.
3.  **Agent Logic**: The agent processes the prompt and decides to call the
    `write_file` tool, which requires user confirmation.
4.  **Server -> Client**:
    - **Event 3**: The server sends a `TaskStatusUpdateEvent`. The metadata
      `kind` is `'TOOL_CALL_UPDATE'`, and the `DataPart` contains a `ToolCall`
      object with its `status` as `'PENDING'` and a populated
      `confirmation_request`.
    - **Event 4**: The server sends a final `TaskStatusUpdateEvent` for this
      exchange. The metadata `kind` is `'STATE_CHANGE'`, the `status.state` is
      `'input-required'`, and `final` is `true`. The stream for this request
      ends.
5.  **Client**: The client UI renders the confirmation prompt based on the
    `ToolCall` object from Event 3. The user clicks "Approve."
6.  **Client -> Server**: The client sends a new `message/stream` request. It
    includes the `taskId` from the ongoing task and a `DataPart` containing a
    `ToolCallConfirmation` object (e.g.,
    `{"tool_call_id": "...", "selected_option_id": "proceed_once"}`).
7.  **Server -> Client**: A new SSE stream begins for the second request.
    - **Event 1**: The server sends a `TaskStatusUpdateEvent` with
      `kind: 'TOOL_CALL_UPDATE'`, containing the `ToolCall` object with its
      `status` now set to `'EXECUTING'`.
    - **Event 2**: After the tool runs, the server sends another
      `TaskStatusUpdateEvent` with `kind: 'TOOL_CALL_UPDATE'`, containing the
      `ToolCall` with its `status` as `'SUCCEEDED'`.
8.  **Agent Logic**: The agent receives the successful tool result and generates
    a final textual response.
9.  **Server -> Client**:
    - **Event 3**: The server sends a `TaskStatusUpdateEvent` with
      `kind: 'TEXT_CONTENT'` and a `TextPart` containing the agent's final
      answer.
    - **Event 4**: The server sends the final `TaskStatusUpdateEvent`. The
      `kind` is `'STATE_CHANGE'`, the `status.state` is `'completed'`, and
      `final` is `true`. The stream ends.
10. **Client**: The client displays the final answer. The task is now complete
    but can be continued by sending another message with the same `taskId`.


--- packages/vscode-ide-companion/esbuild.js ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import esbuild from 'esbuild';

const production = process.argv.includes('--production');
const watch = process.argv.includes('--watch');

/**
 * @type {import('esbuild').Plugin}
 */
const esbuildProblemMatcherPlugin = {
  name: 'esbuild-problem-matcher',

  setup(build) {
    build.onStart(() => {
      console.log('[watch] build started');
    });
    build.onEnd((result) => {
      result.errors.forEach(({ text, location }) => {
        console.error(`✘ [ERROR] ${text}`);
        console.error(
          `    ${location.file}:${location.line}:${location.column}:`,
        );
      });
      console.log('[watch] build finished');
    });
  },
};

async function main() {
  const ctx = await esbuild.context({
    entryPoints: ['src/extension.ts'],
    bundle: true,
    format: 'cjs',
    minify: production,
    sourcemap: !production,
    sourcesContent: false,
    platform: 'node',
    outfile: 'dist/extension.cjs',
    external: ['vscode'],
    logLevel: 'silent',
    banner: {
      js: `const import_meta = { url: require('url').pathToFileURL(__filename).href };`,
    },
    define: {
      'import.meta.url': 'import_meta.url',
    },
    plugins: [
      /* add to the end of plugins array */
      esbuildProblemMatcherPlugin,
    ],
    loader: { '.node': 'file', '.wasm': 'binary' },
  });
  if (watch) {
    await ctx.watch();
  } else {
    await ctx.rebuild();
    await ctx.dispose();
  }
}

main().catch((e) => {
  console.error(e);
  process.exit(1);
});


--- packages/a2a-server/index.ts ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

export * from './src/index.js';


--- packages/cli/index.ts ---
#!/usr/bin/env node

/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { main } from './src/gemini.js';
import { FatalError, writeToStderr } from '@google/gemini-cli-core';
import { runExitCleanup } from './src/utils/cleanup.js';

// --- Global Entry Point ---
main().catch(async (error) => {
  await runExitCleanup();

  if (error instanceof FatalError) {
    let errorMessage = error.message;
    if (!process.env['NO_COLOR']) {
      errorMessage = `\x1b[31m${errorMessage}\x1b[0m`;
    }
    writeToStderr(errorMessage + '\n');
    process.exit(error.exitCode);
  }
  writeToStderr('An unexpected critical error occurred:');
  if (error instanceof Error) {
    writeToStderr(error.stack + '\n');
  } else {
    writeToStderr(String(error) + '\n');
  }
  process.exit(1);
});


--- packages/core/index.ts ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

export * from './src/index.js';
export { Storage } from './src/config/storage.js';
export {
  DEFAULT_GEMINI_MODEL,
  DEFAULT_GEMINI_MODEL_AUTO,
  DEFAULT_GEMINI_FLASH_MODEL,
  DEFAULT_GEMINI_FLASH_LITE_MODEL,
  DEFAULT_GEMINI_EMBEDDING_MODEL,
} from './src/config/models.js';
export {
  serializeTerminalToObject,
  type AnsiOutput,
  type AnsiLine,
  type AnsiToken,
} from './src/utils/terminalSerializer.js';
export {
  DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
  DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
} from './src/config/config.js';
export { detectIdeFromEnv } from './src/ide/detect-ide.js';
export {
  logExtensionEnable,
  logIdeConnection,
  logExtensionDisable,
} from './src/telemetry/loggers.js';

export {
  IdeConnectionEvent,
  IdeConnectionType,
  ExtensionInstallEvent,
  ExtensionDisableEvent,
  ExtensionEnableEvent,
  ExtensionUninstallEvent,
  ExtensionUpdateEvent,
  ModelSlashCommandEvent,
} from './src/telemetry/types.js';
export { makeFakeConfig } from './src/test-utils/config.js';
export * from './src/utils/pathReader.js';
export { ClearcutLogger } from './src/telemetry/clearcut-logger/clearcut-logger.js';
export { logModelSlashCommand } from './src/telemetry/loggers.js';
export { KeychainTokenStorage } from './src/mcp/token-storage/keychain-token-storage.js';
export * from './src/utils/googleQuotaErrors.js';
export type { GoogleApiError } from './src/utils/googleErrors.js';
export { getCodeAssistServer } from './src/code_assist/codeAssist.js';
export { getExperiments } from './src/code_assist/experiments/experiments.js';
export { getErrorStatus, ModelNotFoundError } from './src/utils/httpErrors.js';


--- packages/test-utils/index.ts ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

export * from './src/file-system-test-helpers.js';


--- packages/vscode-ide-companion/NOTICES.txt ---
This file contains third-party software notices and license terms.

============================================================
@modelcontextprotocol/sdk@1.23.0
(git+https://github.com/modelcontextprotocol/typescript-sdk.git)

MIT License

Copyright (c) 2024 Anthropic, PBC

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


============================================================
ajv@6.12.6
(https://github.com/ajv-validator/ajv.git)

The MIT License (MIT)

Copyright (c) 2015-2017 Evgeny Poberezkin

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.



============================================================
fast-deep-equal@3.1.3
(git+https://github.com/epoberezkin/fast-deep-equal.git)

MIT License

Copyright (c) 2017 Evgeny Poberezkin

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


============================================================
fast-json-stable-stringify@2.1.0
(git://github.com/epoberezkin/fast-json-stable-stringify.git)

This software is released under the MIT license:

Copyright (c) 2017 Evgeny Poberezkin
Copyright (c) 2013 James Halliday

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


============================================================
json-schema-traverse@0.4.1
(git+https://github.com/epoberezkin/json-schema-traverse.git)

MIT License

Copyright (c) 2017 Evgeny Poberezkin

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


============================================================
uri-js@4.4.1
(http://github.com/garycourt/uri-js)

Copyright 2011 Gary Court. All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

1.	Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

2.	Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY GARY COURT "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GARY COURT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

The views and conclusions contained in the software and documentation are those of the authors and should not be interpreted as representing official policies, either expressed or implied, of Gary Court.


============================================================
punycode@2.3.1
(https://github.com/mathiasbynens/punycode.js.git)

Copyright Mathias Bynens <https://mathiasbynens.be/>

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


============================================================
ajv-formats@3.0.1
(git+https://github.com/ajv-validator/ajv-formats.git)

MIT License

Copyright (c) 2020 Evgeny Poberezkin

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


============================================================
content-type@1.0.5
(No repository found)

(The MIT License)

Copyright (c) 2015 Douglas Christopher Wilson

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


============================================================
cors@2.8.5
(No repository found)

(The MIT License)

Copyright (c) 2013 Troy Goode <troygoode@gmail.com>

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


============================================================
object-assign@4.1.1
(No repository found)

The MIT License (MIT)

Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


============================================================
vary@1.1.2
(No repository found)

(The MIT License)

Copyright (c) 2014-2017 Douglas Christopher Wilson

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


============================================================
cross-spawn@7.0.6
(git@github.com:moxystudio/node-cross-spawn.git)

The MIT License (MIT)

Copyright (c) 2018 Made With MOXY Lda <hello@moxy.studio>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


============================================================
path-key@3.1.1
(No repository found)

MIT License

Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


============================================================
shebang-command@2.0.0
(No repository found)

MIT License

Copyright (c) Kevin Mårtensson <kevinmartensson@gmail.com> (github.com/kevva)

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


============================================================
shebang-regex@3.0.0
(No repository found)

MIT License

Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


============================================================
which@2.0.2
(git://github.com/isaacs/node-which.git)

The ISC License

Copyright (c) Isaac Z. Schlueter and Contributors

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.


============================================================
isexe@2.0.0
(git+https://github.com/isaacs/isexe.git)

The ISC License

Copyright (c) Isaac Z. Schlueter and Contributors

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.


============================================================
eventsource@3.0.7
(git://git@github.com/EventSource/eventsource.git)

The MIT License

Copyright (c) EventSource GitHub organisation

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


============================================================
eventsource-parser@3.0.3
(git+ssh://git@github.com/rexxars/eventsource-parser.git)

MIT License

Copyright (c) 2025 Espen Hovlandsdal <espen@hovlandsdal.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


============================================================
express@5.1.0
(No repository found)

(The MIT License)

Copyright (c) 2009-2014 TJ Holowaychuk <tj@vision-media.ca>
Copyright (c) 2013-2014 Roman Shtylman <shtylman+expressjs@gmail.com>
Copyright (c) 2014-2015 Douglas Christopher Wilson <doug@somethingdoug.com>

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


============================================================
accepts@1.3.8
(No repository found)

(The MIT License)

Copyright (c) 2014 Jonathan Ong <me@jongleberry.com>
Copyright (c) 2015 Douglas Christopher Wilson <doug@somethingdoug.com>

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


============================================================
mime-types@3.0.1
(No repository found)

(The MIT License)

Copyright (c) 2014 Jonathan Ong <me@jongleberry.com>
Copyright (c) 2015 Douglas Christopher Wilson <doug@somethingdoug.com>

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


============================================================
mime-db@1.54.0
(No repository found)

(The MIT License)

Copyright (c) 2014 Jonathan Ong <me@jongleberry.com>
Copyright (c) 2015-2022 Douglas Christopher Wilson <doug@somethingdoug.com>

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


============================================================
negotiator@0.6.3
(No repository found)

(The MIT License)

Copyright (c) 2012-2014 Federico Romero
Copyright (c) 2012-2014 Isaac Z. Schlueter
Copyright (c) 2014-2015 Douglas Christopher Wilson

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


============================================================
body-parser@1.20.3
(No repository found)

(The MIT License)

Copyright (c) 2014 Jonathan Ong <me@jongleberry.com>
Copyright (c) 2014-2015 Douglas Christopher Wilson <doug@somethingdoug.com>

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


============================================================
bytes@3.1.2
(No repository found)

(The MIT License)

Copyright (c) 2012-2014 TJ Holowaychuk <tj@vision-media.ca>
Copyright (c) 2015 Jed Watson <jed.watson@me.com>

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


============================================================
debug@4.4.3
(git://github.com/debug-js/debug.git)

(The MIT License)

Copyright (c) 2014-2017 TJ Holowaychuk <tj@vision-media.ca>
Copyright (c) 2018-2021 Josh Junon

Permission is hereby granted, free of charge, to any person obtaining a copy of this software
and associated documentation files (the 'Software'), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.



============================================================
ms@2.1.3
(No repository found)

The MIT License (MIT)

Copyright (c) 2020 Vercel, Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


============================================================
depd@2.0.0
(No repository found)

(The MIT License)

Copyright (c) 2014-2018 Douglas Christopher Wilson

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


============================================================
destroy@1.2.0
(No repository found)


The MIT License (MIT)

Copyright (c) 2014 Jonathan Ong me@jongleberry.com
Copyright (c) 2015-2022 Douglas Christopher Wilson doug@somethingdoug.com

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


============================================================
http-errors@2.0.0
(No repository found)


The MIT License (MIT)

Copyright (c) 2014 Jonathan Ong me@jongleberry.com
Copyright (c) 2016 Douglas Christopher Wilson doug@somethingdoug.com

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


============================================================
inherits@2.0.4
(No repository found)

The ISC License

Copyright (c) Isaac Z. Schlueter

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.



============================================================
setprototypeof@1.2.0
(https://github.com/wesleytodd/setprototypeof.git)

Copyright (c) 2015, Wes Todd

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.


============================================================
statuses@2.0.2
(No repository found)


The MIT License (MIT)

Copyright (c) 2014 Jonathan Ong <me@jongleberry.com>
Copyright (c) 2016 Douglas Christopher Wilson <doug@somethingdoug.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


============================================================
toidentifier@1.0.1
(No repository found)

MIT License

Copyright (c) 2016 Douglas Christopher Wilson <doug@somethingdoug.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


============================================================
iconv-lite@0.6.3
(git://github.com/ashtuchkin/iconv-lite.git)

Copyright (c) 2011 Alexander Shtuchkin

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.



============================================================
safer-buffer@2.1.2
(git+https://github.com/ChALkeR/safer-buffer.git)

MIT License

Copyright (c) 2018 Nikita Skovoroda <chalkerx@gmail.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


============================================================
on-finished@2.4.1
(No repository found)

(The MIT License)

Copyright (c) 2013 Jonathan Ong <me@jongleberry.com>
Copyright (c) 2014 Douglas Christopher Wilson <doug@somethingdoug.com>

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


============================================================
ee-first@1.1.1
(No repository found)


The MIT License (MIT)

Copyright (c) 2014 Jonathan Ong me@jongleberry.com

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


============================================================
qs@6.14.0
(https://github.com/ljharb/qs.git)

BSD 3-Clause License

Copyright (c) 2014, Nathan LaFreniere and other [contributors](https://github.com/ljharb/qs/graphs/contributors)
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
   list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its
   contributors may be used to endorse or promote products derived from
   this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


============================================================
side-channel@1.1.0
(git+https://github.com/ljharb/side-channel.git)

MIT License

Copyright (c) 2019 Jordan Harband

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


============================================================
es-errors@1.3.0
(git+https://github.com/ljharb/es-errors.git)

MIT License

Copyright (c) 2024 Jordan Harband

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


============================================================
object-inspect@1.13.4
(git://github.com/inspect-js/object-inspect.git)

MIT License

Copyright (c) 2013 James Halliday

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


============================================================
side-channel-list@1.0.0
(git+https://github.com/ljharb/side-channel-list.git)

MIT License

Copyright (c) 2024 Jordan Harband

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


============================================================
side-channel-map@1.0.1
(git+https://github.com/ljharb/side-channel-map.git)

MIT License

Copyright (c) 2024 Jordan Harband

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


============================================================
call-bound@1.0.4
(git+https://github.com/ljharb/call-bound.git)

MIT License

Copyright (c) 2024 Jordan Harband

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


============================================================
call-bind-apply-helpers@1.0.2
(git+https://github.com/ljharb/call-bind-apply-helpers.git)

MIT License

Copyright (c) 2024 Jordan Harband

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


============================================================
function-bind@1.1.2
(https://github.com/Raynos/function-bind.git)

Copyright (c) 2013 Raynos.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.



============================================================
get-intrinsic@1.3.0
(git+https://github.com/ljharb/get-intrinsic.git)

MIT License

Copyright (c) 2020 Jordan Harband

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


============================================================
es-define-property@1.0.1
(git+https://github.com/ljharb/es-define-property.git)

MIT License

Copyright (c) 2024 Jordan Harband

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


============================================================
es-object-atoms@1.1.1
(git+https://github.com/ljharb/es-object-atoms.git)

MIT License

Copyright (c) 2024 Jordan Harband

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


============================================================
get-proto@1.0.1
(git+https://github.com/ljharb/get-proto.git)

MIT License

Copyright (c) 2025 Jordan Harband

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


============================================================
dunder-proto@1.0.1
(git+https://github.com/es-shims/dunder-proto.git)

MIT License

Copyright (c) 2024 ECMAScript Shims

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


============================================================
gopd@1.2.0
(git+https://github.com/ljharb/gopd.git)

MIT License

Copyright (c) 2022 Jordan Harband

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


============================================================
has-symbols@1.1.0
(git://github.com/inspect-js/has-symbols.git)

MIT License

Copyright (c) 2016 Jordan Harband

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


============================================================
hasown@2.0.2
(git+https://github.com/inspect-js/hasOwn.git)

MIT License

Copyright (c) Jordan Harband and contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


============================================================
math-intrinsics@1.1.0
(git+https://github.com/es-shims/math-intrinsics.git)

MIT License

Copyright (c) 2024 ECMAScript Shims

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


============================================================
side-channel-weakmap@1.0.2
(git+https://github.com/ljharb/side-channel-weakmap.git)

MIT License

Copyright (c) 2019 Jordan Harband

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


============================================================
raw-body@3.0.2
(No repository found)

The MIT License (MIT)

Copyright (c) 2013-2014 Jonathan Ong <me@jongleberry.com>
Copyright (c) 2014-2022 Douglas Christopher Wilson <doug@somethingdoug.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


============================================================
unpipe@1.0.0
(No repository found)

(The MIT License)

Copyright (c) 2015 Douglas Christopher Wilson <doug@somethingdoug.com>

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


============================================================
type-is@2.0.1
(No repository found)

(The MIT License)

Copyright (c) 2014 Jonathan Ong <me@jongleberry.com>
Copyright (c) 2014-2015 Douglas Christopher Wilson <doug@somethingdoug.com>

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


============================================================
media-typer@0.3.0
(No repository found)

(The MIT License)

Copyright (c) 2014 Douglas Christopher Wilson

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


============================================================
content-disposition@1.0.0
(No repository found)

(The MIT License)

Copyright (c) 2014-2017 Douglas Christopher Wilson

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


============================================================
safe-buffer@5.2.1
(git://github.com/feross/safe-buffer.git)

The MIT License (MIT)

Copyright (c) Feross Aboukhadijeh

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


============================================================
cookie@0.7.2
(No repository found)

(The MIT License)

Copyright (c) 2012-2014 Roman Shtylman <shtylman@gmail.com>
Copyright (c) 2015 Douglas Christopher Wilson <doug@somethingdoug.com>

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.



============================================================
cookie-signature@1.0.6
(https://github.com/visionmedia/node-cookie-signature.git)

License text not found.

============================================================
encodeurl@2.0.0
(No repository found)

(The MIT License)

Copyright (c) 2016 Douglas Christopher Wilson

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


============================================================
escape-html@1.0.3
(No repository found)

(The MIT License)

Copyright (c) 2012-2013 TJ Holowaychuk
Copyright (c) 2015 Andreas Lubbe
Copyright (c) 2015 Tiancheng "Timothy" Gu

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


============================================================
etag@1.8.1
(No repository found)

(The MIT License)

Copyright (c) 2014-2016 Douglas Christopher Wilson

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


============================================================
finalhandler@2.1.0
(No repository found)

(The MIT License)

Copyright (c) 2014-2022 Douglas Christopher Wilson <doug@somethingdoug.com>

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


============================================================
parseurl@1.3.3
(No repository found)


(The MIT License)

Copyright (c) 2014 Jonathan Ong <me@jongleberry.com>
Copyright (c) 2014-2017 Douglas Christopher Wilson <doug@somethingdoug.com>

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


============================================================
fresh@0.5.2
(No repository found)

(The MIT License)

Copyright (c) 2012 TJ Holowaychuk <tj@vision-media.ca>
Copyright (c) 2016-2017 Douglas Christopher Wilson <doug@somethingdoug.com>

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


============================================================
merge-descriptors@1.0.3
(No repository found)

(The MIT License)

Copyright (c) 2013 Jonathan Ong <me@jongleberry.com>
Copyright (c) 2015 Douglas Christopher Wilson <doug@somethingdoug.com>

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


============================================================
once@1.4.0
(git://github.com/isaacs/once)

The ISC License

Copyright (c) Isaac Z. Schlueter and Contributors

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.


============================================================
wrappy@1.0.2
(https://github.com/npm/wrappy)

The ISC License

Copyright (c) Isaac Z. Schlueter and Contributors

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.


============================================================
proxy-addr@2.0.7
(No repository found)

(The MIT License)

Copyright (c) 2014-2016 Douglas Christopher Wilson

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


============================================================
forwarded@0.2.0
(No repository found)

(The MIT License)

Copyright (c) 2014-2017 Douglas Christopher Wilson

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


============================================================
ipaddr.js@1.9.1
(No repository found)

Copyright (C) 2011-2017 whitequark <whitequark@whitequark.org>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


============================================================
range-parser@1.2.1
(No repository found)

(The MIT License)

Copyright (c) 2012-2014 TJ Holowaychuk <tj@vision-media.ca>
Copyright (c) 2015-2016 Douglas Christopher Wilson <doug@somethingdoug.com

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


============================================================
router@2.2.0
(No repository found)

(The MIT License)

Copyright (c) 2013 Roman Shtylman
Copyright (c) 2014-2022 Douglas Christopher Wilson

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


============================================================
is-promise@4.0.0
(https://github.com/then/is-promise.git)

Copyright (c) 2014 Forbes Lindesay

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

============================================================
path-to-regexp@0.1.12
(https://github.com/pillarjs/path-to-regexp.git)

The MIT License (MIT)

Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


============================================================
send@1.2.0
(No repository found)

(The MIT License)

Copyright (c) 2012 TJ Holowaychuk
Copyright (c) 2014-2022 Douglas Christopher Wilson

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


============================================================
serve-static@1.16.2
(No repository found)

(The MIT License)

Copyright (c) 2010 Sencha Inc.
Copyright (c) 2011 LearnBoost
Copyright (c) 2011 TJ Holowaychuk
Copyright (c) 2014-2016 Douglas Christopher Wilson

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


============================================================
express-rate-limit@7.5.1
(git+https://github.com/express-rate-limit/express-rate-limit.git)

﻿# MIT License

Copyright 2023 Nathan Friedly, Vedant K

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


============================================================
pkce-challenge@5.0.0
(git+https://github.com/crouchcd/pkce-challenge.git)

MIT License

Copyright (c) 2019 

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


============================================================
zod@3.25.76
(git+https://github.com/colinhacks/zod.git)

MIT License

Copyright (c) 2025 Colin McDonnell

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


============================================================
zod-to-json-schema@3.25.0
(https://github.com/StefanTerdell/zod-to-json-schema)

ISC License

Copyright (c) 2020, Stefan Terdell

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.



## Links discovered
- [contributors](https://github.com/ljharb/qs/graphs/contributors)

--- scripts/build.js ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

//
// 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.

import { execSync } from 'node:child_process';
import { existsSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const root = join(__dirname, '..');

// npm install if node_modules was removed (e.g. via npm run clean or scripts/clean.js)
if (!existsSync(join(root, 'node_modules'))) {
  execSync('npm install', { stdio: 'inherit', cwd: root });
}

// build all workspaces/packages
execSync('npm run generate', { stdio: 'inherit', cwd: root });
execSync('npm run build --workspaces', { stdio: 'inherit', cwd: root });

// also build container image if sandboxing is enabled
// skip (-s) npm install + build since we did that above
try {
  execSync('node scripts/sandbox_command.js -q', {
    stdio: 'inherit',
    cwd: root,
  });
  if (
    process.env.BUILD_SANDBOX === '1' ||
    process.env.BUILD_SANDBOX === 'true'
  ) {
    execSync('node scripts/build_sandbox.js -s', {
      stdio: 'inherit',
      cwd: root,
    });
  }
} catch {
  // ignore
}


--- scripts/build_package.js ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

//
// 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.

import { execSync } from 'node:child_process';
import { writeFileSync, existsSync, cpSync } from 'node:fs';
import { join, basename } from 'node:path';

if (!process.cwd().includes('packages')) {
  console.error('must be invoked from a package directory');
  process.exit(1);
}

const packageName = basename(process.cwd());

// build typescript files
execSync('tsc --build', { stdio: 'inherit' });

// copy .{md,json} files
execSync('node ../../scripts/copy_files.js', { stdio: 'inherit' });

// Copy documentation for the core package
if (packageName === 'core') {
  const docsSource = join(process.cwd(), '..', '..', 'docs');
  const docsTarget = join(process.cwd(), 'dist', 'docs');
  if (existsSync(docsSource)) {
    cpSync(docsSource, docsTarget, { recursive: true, dereference: true });
    console.log('Copied documentation to dist/docs');
  }
}

// touch dist/.last_build
writeFileSync(join(process.cwd(), 'dist', '.last_build'), '');
process.exit(0);


--- scripts/build_sandbox.js ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

//
// 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.

import { execSync } from 'node:child_process';
import {
  chmodSync,
  existsSync,
  readFileSync,
  rmSync,
  writeFileSync,
} from 'node:fs';
import { join } from 'node:path';
import os from 'node:os';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import cliPkgJson from '../packages/cli/package.json' with { type: 'json' };

const argv = yargs(hideBin(process.argv))
  .option('s', {
    alias: 'skip-npm-install-build',
    type: 'boolean',
    default: false,
    description: 'skip npm install + npm run build',
  })
  .option('f', {
    alias: 'dockerfile',
    type: 'string',
    default: 'Dockerfile',
    description: 'use <dockerfile> for custom image',
  })
  .option('i', {
    alias: 'image',
    type: 'string',
    default: cliPkgJson.config.sandboxImageUri,
    description: 'use <image> name for custom image',
  })
  .option('output-file', {
    type: 'string',
    description:
      'Path to write the final image URI. Used for CI/CD pipeline integration.',
  }).argv;

let sandboxCommand;
try {
  sandboxCommand = execSync('node scripts/sandbox_command.js')
    .toString()
    .trim();
} catch (e) {
  console.warn('ERROR: could not detect sandbox container command');
  console.error(e);
  process.exit(process.env.CI ? 1 : 0);
}

if (sandboxCommand === 'sandbox-exec') {
  console.warn(
    'WARNING: container-based sandboxing is disabled (see README.md#sandboxing)',
  );
  process.exit(0);
}

console.log(`using ${sandboxCommand} for sandboxing`);

const image = argv.i;
const dockerFile = argv.f;

if (!image.length) {
  console.warn(
    'No default image tag specified in gemini-cli/packages/cli/package.json',
  );
}

if (!argv.s) {
  execSync('npm install', { stdio: 'inherit' });
  execSync('npm run build --workspaces', { stdio: 'inherit' });
}

console.log('packing @google/gemini-cli ...');
const cliPackageDir = join('packages', 'cli');
rmSync(join(cliPackageDir, 'dist', 'google-gemini-cli-*.tgz'), { force: true });
execSync(
  `npm pack -w @google/gemini-cli --pack-destination ./packages/cli/dist`,
  {
    stdio: 'ignore',
  },
);

console.log('packing @google/gemini-cli-core ...');
const corePackageDir = join('packages', 'core');
rmSync(join(corePackageDir, 'dist', 'google-gemini-cli-core-*.tgz'), {
  force: true,
});
execSync(
  `npm pack -w @google/gemini-cli-core --pack-destination ./packages/core/dist`,
  { stdio: 'ignore' },
);

const packageVersion = JSON.parse(
  readFileSync(join(process.cwd(), 'package.json'), 'utf-8'),
).version;

chmodSync(
  join(cliPackageDir, 'dist', `google-gemini-cli-${packageVersion}.tgz`),
  0o755,
);
chmodSync(
  join(corePackageDir, 'dist', `google-gemini-cli-core-${packageVersion}.tgz`),
  0o755,
);

const buildStdout = process.env.VERBOSE ? 'inherit' : 'ignore';

// Determine the appropriate shell based on OS
const isWindows = os.platform() === 'win32';
const shellToUse = isWindows ? 'powershell.exe' : '/bin/bash';

function buildImage(imageName, dockerfile) {
  console.log(`building ${imageName} ... (can be slow first time)`);

  let buildCommandArgs = '';
  let tempAuthFile = '';

  if (sandboxCommand === 'podman') {
    if (isWindows) {
      // PowerShell doesn't support <() process substitution.
      // Create a temporary auth file that we will clean up after.
      tempAuthFile = join(os.tmpdir(), `gemini-auth-${Date.now()}.json`);
      writeFileSync(tempAuthFile, '{}');
      buildCommandArgs = `--authfile="${tempAuthFile}"`;
    } else {
      // Use bash-specific syntax for Linux/macOS
      buildCommandArgs = `--authfile=<(echo '{}')`;
    }
  }

  const npmPackageVersion = JSON.parse(
    readFileSync(join(process.cwd(), 'package.json'), 'utf-8'),
  ).version;

  const imageTag =
    process.env.GEMINI_SANDBOX_IMAGE_TAG || imageName.split(':')[1];
  const finalImageName = `${imageName.split(':')[0]}:${imageTag}`;

  try {
    execSync(
      `${sandboxCommand} build ${buildCommandArgs} ${
        process.env.BUILD_SANDBOX_FLAGS || ''
      } --build-arg CLI_VERSION_ARG=${npmPackageVersion} -f "${dockerfile}" -t "${finalImageName}" .`,
      { stdio: buildStdout, shell: shellToUse },
    );
    console.log(`built ${finalImageName}`);

    // If an output file path was provided via command-line, write the final image URI to it.
    if (argv.outputFile) {
      console.log(
        `Writing final image URI for CI artifact to: ${argv.outputFile}`,
      );
      // The publish step only supports one image. If we build multiple, only the last one
      // will be published. Throw an error to make this failure explicit if the file already exists.
      if (existsSync(argv.outputFile)) {
        throw new Error(
          `CI artifact file ${argv.outputFile} already exists. Refusing to overwrite.`,
        );
      }
      writeFileSync(argv.outputFile, finalImageName);
    }
  } finally {
    // If we created a temp file, delete it now.
    if (tempAuthFile) {
      rmSync(tempAuthFile, { force: true });
    }
  }
}

buildImage(image, dockerFile);

execSync(`${sandboxCommand} image prune -f`, { stdio: 'ignore' });


--- scripts/build_vscode_companion.js ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

//
// 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.

import { execSync } from 'node:child_process';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const root = join(__dirname, '..');

execSync('npm --workspace=gemini-cli-vscode-ide-companion run package', {
  stdio: 'inherit',
  cwd: root,
});


--- scripts/check-build-status.js ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os'; // Import os module

// --- Configuration ---
const cliPackageDir = path.resolve('packages', 'cli'); // Base directory for the CLI package
const buildTimestampPath = path.join(cliPackageDir, 'dist', '.last_build'); // Path to the timestamp file within the CLI package
const sourceDirs = [path.join(cliPackageDir, 'src')]; // Source directory within the CLI package
const filesToWatch = [
  path.join(cliPackageDir, 'package.json'),
  path.join(cliPackageDir, 'tsconfig.json'),
]; // Specific files within the CLI package
const buildDir = path.join(cliPackageDir, 'dist'); // Build output directory within the CLI package
const warningsFilePath = path.join(os.tmpdir(), 'gemini-cli-warnings.txt'); // Temp file for warnings
// ---------------------

function getMtime(filePath) {
  try {
    return fs.statSync(filePath).mtimeMs; // Use mtimeMs for higher precision
  } catch (err) {
    if (err.code === 'ENOENT') {
      return null; // File doesn't exist
    }
    console.error(`Error getting stats for ${filePath}:`, err);
    process.exit(1); // Exit on unexpected errors getting stats
  }
}

function findSourceFiles(dir, allFiles = []) {
  const entries = fs.readdirSync(dir, { withFileTypes: true });
  for (const entry of entries) {
    const fullPath = path.join(dir, entry.name);
    // Simple check to avoid recursing into node_modules or build dir itself
    if (
      entry.isDirectory() &&
      entry.name !== 'node_modules' &&
      fullPath !== buildDir
    ) {
      findSourceFiles(fullPath, allFiles);
    } else if (entry.isFile()) {
      allFiles.push(fullPath);
    }
  }
  return allFiles;
}

console.log('Checking build status...');

// Clean up old warnings file before check
try {
  if (fs.existsSync(warningsFilePath)) {
    fs.unlinkSync(warningsFilePath);
  }
} catch (err) {
  console.warn(
    `[Check Script] Warning: Could not delete previous warnings file: ${err.message}`,
  );
}

const buildMtime = getMtime(buildTimestampPath);
if (!buildMtime) {
  // If build is missing, write that as a warning and exit(0) so app can display it
  const errorMessage = `ERROR: Build timestamp file (${path.relative(process.cwd(), buildTimestampPath)}) not found. Run \`npm run build\` first.`;
  console.error(errorMessage); // Still log error here
  try {
    fs.writeFileSync(warningsFilePath, errorMessage);
  } catch (writeErr) {
    console.error(
      `[Check Script] Error writing missing build warning file: ${writeErr.message}`,
    );
  }
  process.exit(0); // Allow app to start and show the error
}

let newerSourceFileFound = false;
const warningMessages = []; // Collect warnings here
const allSourceFiles = [];

// Collect files from specified directories
sourceDirs.forEach((dir) => {
  const dirPath = path.resolve(dir);
  if (fs.existsSync(dirPath)) {
    findSourceFiles(dirPath, allSourceFiles);
  } else {
    console.warn(`Warning: Source directory "${dir}" not found.`);
  }
});

// Add specific files
filesToWatch.forEach((file) => {
  const filePath = path.resolve(file);
  if (fs.existsSync(filePath)) {
    allSourceFiles.push(filePath);
  } else {
    console.warn(`Warning: Watched file "${file}" not found.`);
  }
});

// Check modification times
for (const file of allSourceFiles) {
  const sourceMtime = getMtime(file);
  const relativePath = path.relative(process.cwd(), file);
  const isNewer = sourceMtime && sourceMtime > buildMtime;

  if (isNewer) {
    const warning = `Warning: Source file "${relativePath}" has been modified since the last build.`;
    console.warn(warning); // Keep console warning for script debugging
    warningMessages.push(warning);
    newerSourceFileFound = true;
    // break; // Uncomment to stop checking after the first newer file
  }
}

if (newerSourceFileFound) {
  const finalWarning =
    '\nRun "npm run build" to incorporate changes before starting.';
  warningMessages.push(finalWarning);
  console.warn(finalWarning);

  // Write warnings to the temp file
  try {
    fs.writeFileSync(warningsFilePath, warningMessages.join('\n'));
    // Removed debug log
  } catch (err) {
    console.error(`[Check Script] Error writing warnings file: ${err.message}`);
    // Proceed without writing, app won't show warnings
  }
} else {
  console.log('Build is up-to-date.');
  // Ensure no stale warning file exists if build is ok
  try {
    if (fs.existsSync(warningsFilePath)) {
      fs.unlinkSync(warningsFilePath);
    }
  } catch (err) {
    console.warn(
      `[Check Script] Warning: Could not delete previous warnings file: ${err.message}`,
    );
  }
}

process.exit(0); // Always exit successfully so the app starts


--- scripts/check-lockfile.js ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import fs from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const root = join(__dirname, '..');
const lockfilePath = join(root, 'package-lock.json');

function readJsonFile(filePath) {
  try {
    const fileContent = fs.readFileSync(filePath, 'utf-8');
    return JSON.parse(fileContent);
  } catch (error) {
    console.error(`Error reading or parsing ${filePath}:`, error);
    return null;
  }
}

console.log('Checking lockfile...');

const lockfile = readJsonFile(lockfilePath);
if (lockfile === null) {
  process.exit(1);
}
const packages = lockfile.packages || {};
const invalidPackages = [];

for (const [location, details] of Object.entries(packages)) {
  // 1. Skip the root package itself.
  if (location === '') {
    continue;
  }

  // 2. Skip local workspace packages.
  // They are identifiable in two ways:
  // a) As a symlink within node_modules.
  // b) As the source package definition, whose path is not in node_modules.
  if (details.link === true || !location.includes('node_modules')) {
    continue;
  }

  // 3. Any remaining package should be a third-party dependency.
  // 1) Registry package with both "resolved" and "integrity" fields is valid.
  if (details.resolved && details.integrity) {
    continue;
  }
  // 2) Git and file dependencies only need a "resolved" field.
  const isGitOrFileDep =
    details.resolved?.startsWith('git') ||
    details.resolved?.startsWith('file:');
  if (isGitOrFileDep) {
    continue;
  }

  // Mark the left dependency as invalid.
  invalidPackages.push(location);
}

if (invalidPackages.length > 0) {
  console.error(
    '\nError: The following dependencies in package-lock.json are missing the "resolved" or "integrity" field:',
  );
  invalidPackages.forEach((pkg) => console.error(`- ${pkg}`));
  process.exitCode = 1;
} else {
  console.log('Lockfile check passed.');
  process.exitCode = 0;
}


--- scripts/clean.js ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

//
// 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.

import { rmSync, readFileSync, readdirSync, statSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const root = join(__dirname, '..');

// remove npm install/build artifacts
rmSync(join(root, 'node_modules'), { recursive: true, force: true });
rmSync(join(root, 'bundle'), { recursive: true, force: true });
rmSync(join(root, 'packages/cli/src/generated/'), {
  recursive: true,
  force: true,
});
const RMRF_OPTIONS = { recursive: true, force: true };
rmSync(join(root, 'bundle'), RMRF_OPTIONS);
// Dynamically clean dist directories in all workspaces
const rootPackageJson = JSON.parse(
  readFileSync(join(root, 'package.json'), 'utf-8'),
);
for (const workspace of rootPackageJson.workspaces) {
  // Note: this is a simple glob implementation that only supports "packages/*".
  const workspaceDir = join(root, dirname(workspace));
  const packageDirs = readdirSync(workspaceDir);

  for (const pkg of packageDirs) {
    const pkgDir = join(workspaceDir, pkg);
    try {
      if (statSync(pkgDir).isDirectory()) {
        rmSync(join(pkgDir, 'dist'), RMRF_OPTIONS);
      }
    } catch (e) {
      if (e.code !== 'ENOENT') {
        throw e;
      }
    }
  }
}

// Clean up vscode-ide-companion package
rmSync(join(root, 'packages/vscode-ide-companion/node_modules'), {
  recursive: true,
  force: true,
});

const vscodeCompanionDir = join(root, 'packages/vscode-ide-companion');
try {
  const files = readdirSync(vscodeCompanionDir);
  for (const file of files) {
    if (file.endsWith('.vsix')) {
      rmSync(join(vscodeCompanionDir, file), RMRF_OPTIONS);
    }
  }
} catch (e) {
  if (e.code !== 'ENOENT') {
    throw e;
  }
}


--- scripts/copy_bundle_assets.js ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

//
// 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.

import { copyFileSync, existsSync, mkdirSync, cpSync } from 'node:fs';
import { dirname, join, basename } from 'node:path';
import { fileURLToPath } from 'node:url';
import { glob } from 'glob';

const __dirname = dirname(fileURLToPath(import.meta.url));
const root = join(__dirname, '..');
const bundleDir = join(root, 'bundle');

// Create the bundle directory if it doesn't exist
if (!existsSync(bundleDir)) {
  mkdirSync(bundleDir);
}

// 1. Copy Sandbox definitions (.sb)
const sbFiles = glob.sync('packages/**/*.sb', { cwd: root });
for (const file of sbFiles) {
  copyFileSync(join(root, file), join(bundleDir, basename(file)));
}

// 2. Copy Policy definitions (.toml)
const policyDir = join(bundleDir, 'policies');
if (!existsSync(policyDir)) {
  mkdirSync(policyDir);
}

// Locate policy files specifically in the core package
const policyFiles = glob.sync('packages/core/src/policy/policies/*.toml', {
  cwd: root,
});

for (const file of policyFiles) {
  copyFileSync(join(root, file), join(policyDir, basename(file)));
}

console.log(`Copied ${policyFiles.length} policy files to bundle/policies/`);

// 3. Copy Documentation (docs/)
const docsSrc = join(root, 'docs');
const docsDest = join(bundleDir, 'docs');
if (existsSync(docsSrc)) {
  cpSync(docsSrc, docsDest, { recursive: true, dereference: true });
  console.log('Copied docs to bundle/docs/');
}

console.log('Assets copied to bundle/');


--- scripts/copy_files.js ---
#!/usr/bin/env node

/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

// Copyright 2025 Google LLC
//
// 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.

import fs from 'node:fs';
import path from 'node:path';

const sourceDir = path.join('src');
const targetDir = path.join('dist', 'src');

const extensionsToCopy = ['.md', '.json', '.sb', '.toml'];

function copyFilesRecursive(source, target) {
  if (!fs.existsSync(target)) {
    fs.mkdirSync(target, { recursive: true });
  }

  const items = fs.readdirSync(source, { withFileTypes: true });

  for (const item of items) {
    const sourcePath = path.join(source, item.name);
    const targetPath = path.join(target, item.name);

    if (item.isDirectory()) {
      copyFilesRecursive(sourcePath, targetPath);
    } else if (extensionsToCopy.includes(path.extname(item.name))) {
      fs.copyFileSync(sourcePath, targetPath);
    }
  }
}

if (!fs.existsSync(sourceDir)) {
  console.error(`Source directory ${sourceDir} not found.`);
  process.exit(1);
}

copyFilesRecursive(sourceDir, targetDir);

// Copy example extensions into the bundle.
const packageName = path.basename(process.cwd());
if (packageName === 'cli') {
  const examplesSource = path.join(
    sourceDir,
    'commands',
    'extensions',
    'examples',
  );
  const examplesTarget = path.join(
    targetDir,
    'commands',
    'extensions',
    'examples',
  );
  if (fs.existsSync(examplesSource)) {
    fs.cpSync(examplesSource, examplesTarget, { recursive: true });
  }
}

console.log('Successfully copied files.');


--- scripts/deflake.js ---
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { spawn } from 'node:child_process';
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';

// Script to deflake tests
// Ex. npm run deflake -- --command="npm run test:e2e -- --test-name-pattern 'extension'" --runs=3

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(__dirname, '..');
const dockerIgnorePath = path.join(projectRoot, '.dockerignore');

const DOCKERIGNORE_CONTENT = `.integration-tests`.trim();

/**
 * Runs a command and streams its output to the console.
 * @param {string} command The command string to execute (e.g., 'npm run test:e2e -- --watch').
 * @returns {Promise<number>} A Promise that resolves with the exit code of the process.
 */
function runCommand(cmd, args = []) {
  if (!cmd) {
    return Promise.resolve(1);
  }

  return new Promise((resolve, reject) => {
    const child = spawn(cmd, args, {
      shell: true,
      stdio: 'inherit',
      env: { ...process.env },
    });

    child.on('close', (code) => {
      resolve(code ?? 1); // code can be null if the process was killed
    });

    child.on('error', (err) => {
      // An error occurred in spawning the process (e.g., command not found).
      console.error(`Failed to start command: ${err.message}`);
      reject(err);
    });
  });
}
// -------------------------------------------------------------------

async function main() {
  const argv = await yargs(hideBin(process.argv))
    .option('command', {
      type: 'string',
      demandOption: true,
      description: 'The command to run',
    })
    .option('runs', {
      type: 'number',
      default: 5,
      description: 'The number of runs to perform',
    }).argv;

  const NUM_RUNS = argv.runs;
  const COMMAND = argv.command;
  const ARGS = argv._;
  let failures = 0;

  const backupDockerIgnorePath = dockerIgnorePath + '.bak';
  let originalDockerIgnoreRenamed = false;

  console.log(`--- Starting Deflake Run (${NUM_RUNS} iterations) ---`);

  try {
    try {
      // Try to rename to back up an existing .dockerignore
      await fs.rename(dockerIgnorePath, backupDockerIgnorePath);
      originalDockerIgnoreRenamed = true;
    } catch (err) {
      // If the file doesn't exist, that's fine. Otherwise, rethrow.
      if (err.code !== 'ENOENT') throw err;
    }

    // Create the temporary .dockerignore for this run.
    await fs.writeFile(dockerIgnorePath, DOCKERIGNORE_CONTENT);

    for (let i = 1; i <= NUM_RUNS; i++) {
      console.log(`\n[RUN ${i}/${NUM_RUNS}]`);

      try {
        const exitCode = await runCommand(COMMAND, ARGS);

        if (exitCode === 0) {
          console.log('✅ Run PASS');
        } else {
          console.log(`❌ Run FAIL (Exit Code: ${exitCode})`);
          failures++;
        }
      } catch (error) {
        console.error('❌ Run FAIL (Execution Error)', error);
        failures++;
      }
    }
  } finally {
    try {
      // Clean up the temporary .dockerignore
      await fs.unlink(dockerIgnorePath);
    } catch (err) {
      console.error('Failed to remove temporary .dockerignore:', err);
    }

    if (originalDockerIgnoreRenamed) {
      try {
        // Restore the original .dockerignore if it was backed up.
        await fs.rename(backupDockerIgnorePath, dockerIgnorePath);
      } catch (err) {
        console.error('Failed to restore original .dockerignore:', err);
      }
    }
  }

  console.log('\n--- FINAL DEFLAKE SUMMARY ---');
  console.log(`Total Runs: ${NUM_RUNS}`);
  console.log(`Total Failures: ${failures}`);

  process.exit(failures > 0 ? 1 : 0);
}

main().catch((error) => {
  console.error('Error in deflake:', error);
  process.exit(1);
});


--- third_party/get-ripgrep/src/downloadRipGrep.js ---
/* eslint-disable */
/**
 * @license
 * Copyright 2023 Lvce Editor
 * SPDX-License-Identifier: MIT
 */
import { VError } from '@lvce-editor/verror'
import { execa } from 'execa'
import extractZip from 'extract-zip'
import fsExtra from 'fs-extra'
import got from 'got'
import * as os from 'node:os'
import { dirname, join } from 'node:path'
import { pathExists } from 'path-exists'
import { pipeline } from 'node:stream/promises'
import { temporaryFile } from 'tempy'
import { fileURLToPath } from 'node:url'
import { xdgCache } from 'xdg-basedir'

const { mkdir, createWriteStream, move } = fsExtra

const __dirname = dirname(fileURLToPath(import.meta.url))

const REPOSITORY = `microsoft/ripgrep-prebuilt`
const VERSION = process.env.RIPGREP_VERSION || 'v13.0.0-10'
console.log({ VERSION })
const BIN_PATH = join(__dirname, '../bin')

const getTarget = () => {
  const arch = process.env.npm_config_arch || os.arch()
  const platform = process.env.platform || os.platform()
  switch (platform) {
    case 'darwin':
      switch (arch) {
        case 'arm64':
          return 'aarch64-apple-darwin.tar.gz'
        default:
          return 'x86_64-apple-darwin.tar.gz'
      }
    case 'win32':
      switch (arch) {
        case 'x64':
          return 'x86_64-pc-windows-msvc.zip'
        case 'arm':
          return 'aarch64-pc-windows-msvc.zip'
        default:
          return 'i686-pc-windows-msvc.zip'
      }
    case 'linux':
      switch (arch) {
        case 'x64':
          return 'x86_64-unknown-linux-musl.tar.gz'
        case 'arm':
        case 'armv7l':
          return 'arm-unknown-linux-gnueabihf.tar.gz'
        case 'arm64':
          return 'aarch64-unknown-linux-gnu.tar.gz'
        case 'ppc64':
          return 'powerpc64le-unknown-linux-gnu.tar.gz'
        case 's390x':
          return 's390x-unknown-linux-gnu.tar.gz'
        default:
          return 'i686-unknown-linux-musl.tar.gz'
      }
    default:
      throw new VError('Unknown platform: ' + platform)
  }
}

export const downloadFile = async (url, outFile) => {
  try {
    const tmpFile = temporaryFile()
    await pipeline(got.stream(url), createWriteStream(tmpFile))
    await mkdir(dirname(outFile), { recursive: true })
    await move(tmpFile, outFile)
  } catch (error) {
    throw new VError(error, `Failed to download "${url}"`)
  }
}

/**
 * @param {string} inFile
 * @param {string} outDir
 */
const unzip = async (inFile, outDir) => {
  try {
    await mkdir(outDir, { recursive: true })
    await extractZip(inFile, { dir: outDir })
  } catch (error) {
    throw new VError(error, `Failed to unzip "${inFile}"`)
  }
}

/**
 * @param {string} inFile
 * @param {string} outDir
 */
const untarGz = async (inFile, outDir) => {
  try {
    await mkdir(outDir, { recursive: true })
    await execa('tar', ['xvf', inFile, '-C', outDir])
  } catch (error) {
    throw new VError(error, `Failed to extract "${inFile}"`)
  }
}

export const downloadRipGrep = async (binPath = BIN_PATH) => {
  const target = getTarget()
  const url = `https://github.com/${REPOSITORY}/releases/download/${VERSION}/ripgrep-${VERSION}-${target}`
  const downloadPath = `${xdgCache}/vscode-ripgrep/ripgrep-${VERSION}-${target}`
  if (!(await pathExists(downloadPath))) {
    await downloadFile(url, downloadPath)
  } else {
    console.info(`File ${downloadPath} has been cached`)
  }
  if (downloadPath.endsWith('.tar.gz')) {
    await untarGz(downloadPath, binPath)
  } else if (downloadPath.endsWith('.zip')) {
    await unzip(downloadPath, binPath)
  } else {
    throw new VError(`Invalid downloadPath ${downloadPath}`)
  }
}


--- third_party/get-ripgrep/src/index.js ---
/* eslint-disable */
/**
 * @license
 * Copyright 2023 Lvce Editor
 * SPDX-License-Identifier: MIT
 */
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'

const __dirname = dirname(fileURLToPath(import.meta.url))

export const rgPath = join(
  __dirname,
  '..',
  'bin',
  `rg${process.platform === 'win32' ? '.exe' : ''}`,
)
