#
tokens: 49396/50000 34/137 files (page 2/3)
lines: off (toggle) GitHub
raw markdown copy
This is page 2 of 3. Use http://codebase.md/circleci-public/mcp-server-circleci?page={x} to view the full context.

# Directory Structure

```
├── .circleci
│   └── config.yml
├── .dockerignore
├── .github
│   ├── CODEOWNERS
│   ├── ISSUE_TEMPLATE
│   │   ├── BUG.yml
│   │   └── FEATURE_REQUEST.yml
│   └── PULL_REQUEST_TEMPLATE
│       └── PULL_REQUEST.md
├── .gitignore
├── .npmrc
├── .nvmrc
├── .prettierignore
├── .prettierrc
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile
├── eslint.config.js
├── LICENSE
├── package.json
├── pnpm-lock.yaml
├── README.md
├── renovate.json
├── scripts
│   └── create-tool.js
├── smithery.yaml
├── src
│   ├── circleci-tools.ts
│   ├── clients
│   │   ├── circleci
│   │   │   ├── configValidate.ts
│   │   │   ├── deploys.ts
│   │   │   ├── httpClient.test.ts
│   │   │   ├── httpClient.ts
│   │   │   ├── index.ts
│   │   │   ├── insights.ts
│   │   │   ├── jobs.ts
│   │   │   ├── jobsV1.ts
│   │   │   ├── pipelines.ts
│   │   │   ├── projects.ts
│   │   │   ├── tests.ts
│   │   │   ├── usage.ts
│   │   │   └── workflows.ts
│   │   ├── circleci-private
│   │   │   ├── index.ts
│   │   │   ├── jobsPrivate.ts
│   │   │   └── me.ts
│   │   ├── circlet
│   │   │   ├── circlet.ts
│   │   │   └── index.ts
│   │   ├── client.ts
│   │   └── schemas.ts
│   ├── index.ts
│   ├── lib
│   │   ├── flaky-tests
│   │   │   └── getFlakyTests.ts
│   │   ├── getWorkflowIdFromURL.test.ts
│   │   ├── getWorkflowIdFromURL.ts
│   │   ├── latest-pipeline
│   │   │   ├── formatLatestPipelineStatus.ts
│   │   │   └── getLatestPipelineWorkflows.ts
│   │   ├── mcpErrorOutput.test.ts
│   │   ├── mcpErrorOutput.ts
│   │   ├── mcpResponse.test.ts
│   │   ├── mcpResponse.ts
│   │   ├── outputTextTruncated.test.ts
│   │   ├── outputTextTruncated.ts
│   │   ├── pipeline-job-logs
│   │   │   ├── getJobLogs.ts
│   │   │   └── getPipelineJobLogs.ts
│   │   ├── pipeline-job-tests
│   │   │   ├── formatJobTests.ts
│   │   │   └── getJobTests.ts
│   │   ├── project-detection
│   │   │   ├── index.test.ts
│   │   │   ├── index.ts
│   │   │   └── vcsTool.ts
│   │   ├── rateLimitedRequests
│   │   │   ├── index.test.ts
│   │   │   └── index.ts
│   │   └── usage-api
│   │       ├── findUnderusedResourceClasses.test.ts
│   │       ├── findUnderusedResourceClasses.ts
│   │       ├── getUsageApiData.test.ts
│   │       ├── getUsageApiData.ts
│   │       └── parseDateTimeString.ts
│   ├── tools
│   │   ├── analyzeDiff
│   │   │   ├── handler.test.ts
│   │   │   ├── handler.ts
│   │   │   ├── inputSchema.ts
│   │   │   └── tool.ts
│   │   ├── configHelper
│   │   │   ├── handler.test.ts
│   │   │   ├── handler.ts
│   │   │   ├── inputSchema.ts
│   │   │   └── tool.ts
│   │   ├── createPromptTemplate
│   │   │   ├── handler.test.ts
│   │   │   ├── handler.ts
│   │   │   ├── inputSchema.ts
│   │   │   └── tool.ts
│   │   ├── downloadUsageApiData
│   │   │   ├── handler.test.ts
│   │   │   ├── handler.ts
│   │   │   ├── inputSchema.ts
│   │   │   └── tool.ts
│   │   ├── findUnderusedResourceClasses
│   │   │   ├── handler.test.ts
│   │   │   ├── handler.ts
│   │   │   ├── inputSchema.ts
│   │   │   └── tool.ts
│   │   ├── getBuildFailureLogs
│   │   │   ├── handler.test.ts
│   │   │   ├── handler.ts
│   │   │   ├── inputSchema.ts
│   │   │   └── tool.ts
│   │   ├── getFlakyTests
│   │   │   ├── handler.test.ts
│   │   │   ├── handler.ts
│   │   │   ├── inputSchema.ts
│   │   │   └── tool.ts
│   │   ├── getJobTestResults
│   │   │   ├── handler.test.ts
│   │   │   ├── handler.ts
│   │   │   ├── inputSchema.ts
│   │   │   └── tool.ts
│   │   ├── getLatestPipelineStatus
│   │   │   ├── handler.test.ts
│   │   │   ├── handler.ts
│   │   │   ├── inputSchema.ts
│   │   │   └── tool.ts
│   │   ├── listComponentVersions
│   │   │   ├── handler.test.ts
│   │   │   ├── handler.ts
│   │   │   ├── inputSchema.ts
│   │   │   └── tool.ts
│   │   ├── listFollowedProjects
│   │   │   ├── handler.test.ts
│   │   │   ├── handler.ts
│   │   │   ├── inputSchema.ts
│   │   │   └── tool.ts
│   │   ├── recommendPromptTemplateTests
│   │   │   ├── handler.test.ts
│   │   │   ├── handler.ts
│   │   │   ├── inputSchema.ts
│   │   │   └── tool.ts
│   │   ├── rerunWorkflow
│   │   │   ├── handler.test.ts
│   │   │   ├── handler.ts
│   │   │   ├── inputSchema.ts
│   │   │   └── tool.ts
│   │   ├── runEvaluationTests
│   │   │   ├── handler.test.ts
│   │   │   ├── handler.ts
│   │   │   ├── inputSchema.ts
│   │   │   └── tool.ts
│   │   ├── runPipeline
│   │   │   ├── handler.test.ts
│   │   │   ├── handler.ts
│   │   │   ├── inputSchema.ts
│   │   │   └── tool.ts
│   │   ├── runRollbackPipeline
│   │   │   ├── handler.test.ts
│   │   │   ├── handler.ts
│   │   │   ├── inputSchema.ts
│   │   │   └── tool.ts
│   │   └── shared
│   │       └── constants.ts
│   └── transports
│       ├── stdio.ts
│       └── unified.ts
├── tsconfig.json
├── tsconfig.test.json
└── vitest.config.js
```

# Files

--------------------------------------------------------------------------------
/src/tools/downloadUsageApiData/handler.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { downloadUsageApiData } from './handler.js';
import * as getUsageApiDataModule from '../../lib/usage-api/getUsageApiData.js';

vi.mock('../../lib/usage-api/getUsageApiData.js');

describe('downloadUsageApiData handler', () => {
  const ORG_ID = 'org123';
  const OUTPUT_DIR = '/tmp';

  let getUsageApiDataSpy: any;

  beforeEach(() => {
    vi.clearAllMocks();
    getUsageApiDataSpy = vi.spyOn(getUsageApiDataModule, 'getUsageApiData').mockResolvedValue({
      content: [{ type: 'text', text: 'Success' }],
    } as any);
  });

  it('should call getUsageApiData with correctly formatted dates', async () => {
    const startDate = '2024-06-01';
    const endDate = '2024-06-15';
    
    await downloadUsageApiData({ params: { orgId: ORG_ID, startDate, endDate, outputDir: OUTPUT_DIR } }, undefined as any);

    expect(getUsageApiDataSpy).toHaveBeenCalledWith({
      orgId: ORG_ID,
      startDate: '2024-06-01T00:00:00Z',
      endDate: '2024-06-15T23:59:59Z',
      outputDir: OUTPUT_DIR,
      jobId: undefined,
    });
  });

  it('should return an error if the date range is over 32 days', async () => {
    const startDate = '2024-01-01';
    const endDate = '2024-02-02';

    const result = await downloadUsageApiData({ params: { orgId: ORG_ID, startDate, endDate, outputDir: OUTPUT_DIR } }, undefined as any);

    expect(getUsageApiDataSpy).not.toHaveBeenCalled();
    expect((result as any).isError).toBe(true);
    expect((result as any).content[0].text).toContain('maximum allowed date range for the usage API is 32 days');
  });

  it('should return an error for an invalid date format', async () => {
    const startDate = 'not-a-date';
    const endDate = '2024-06-15';

    const result = await downloadUsageApiData({ params: { orgId: ORG_ID, startDate, endDate, outputDir: OUTPUT_DIR } }, undefined as any);

    expect(getUsageApiDataSpy).not.toHaveBeenCalled();
    expect((result as any).isError).toBe(true);
    expect((result as any).content[0].text).toContain('Invalid date format');
  });

  it('should return an error if the end date is before the start date', async () => {
    const startDate = '2024-06-15';
    const endDate = '2024-06-01';

    const result = await downloadUsageApiData({ params: { orgId: ORG_ID, startDate, endDate, outputDir: OUTPUT_DIR } }, undefined as any);

    expect(getUsageApiDataSpy).not.toHaveBeenCalled();
    expect((result as any).isError).toBe(true);
    expect((result as any).content[0].text).toContain('end date must be after or equal to the start date');
  });

  it('should allow polling existing job with only jobId and no dates', async () => {
    const result = await downloadUsageApiData(
      { params: { orgId: ORG_ID, jobId: 'job-abc', outputDir: OUTPUT_DIR } },
      undefined as any,
    );

    expect(getUsageApiDataSpy).toHaveBeenCalledWith({
      orgId: ORG_ID,
      startDate: undefined,
      endDate: undefined,
      outputDir: OUTPUT_DIR,
      jobId: 'job-abc',
    });
    expect((result as any).content[0].text).toContain('Success');
  });

  it('should error when neither jobId nor both dates are provided', async () => {
    const result = await downloadUsageApiData(
      { params: { orgId: ORG_ID, outputDir: OUTPUT_DIR } },
      undefined as any,
    );
    expect(getUsageApiDataSpy).not.toHaveBeenCalled();
    expect((result as any).isError).toBe(true);
    expect((result as any).content[0].text).toContain('Provide either jobId');
  });
}); 
```

--------------------------------------------------------------------------------
/src/tools/createPromptTemplate/handler.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import {
  contextSchemaKey,
  createPromptTemplate,
  promptOriginKey,
  promptTemplateKey,
  modelKey,
} from './handler.js';
import { CircletClient } from '../../clients/circlet/index.js';
import {
  defaultModel,
  PromptOrigin,
  PromptWorkbenchToolName,
} from '../shared/constants.js';

// Mock dependencies
vi.mock('../../clients/circlet/index.js');

describe('createPromptTemplate handler', () => {
  beforeEach(() => {
    vi.resetAllMocks();
  });

  it('should return a valid MCP response with template, context schema, and prompt origin', async () => {
    const mockCreatePromptTemplate = vi.fn().mockResolvedValue({
      template: 'This is a test template with {{variable}}',
      contextSchema: {
        variable: 'Description of the variable',
      },
    });

    const mockCircletInstance = {
      circlet: {
        createPromptTemplate: mockCreatePromptTemplate,
      },
    };

    vi.mocked(CircletClient).mockImplementation(
      () => mockCircletInstance as any,
    );

    const args = {
      params: {
        prompt: 'Create a test prompt template',
        promptOrigin: PromptOrigin.requirements,
        model: defaultModel,
      },
    };

    const controller = new AbortController();
    const response = await createPromptTemplate(args, {
      signal: controller.signal,
    });

    expect(mockCreatePromptTemplate).toHaveBeenCalledWith(
      'Create a test prompt template',
      PromptOrigin.requirements,
    );

    expect(response).toHaveProperty('content');
    expect(Array.isArray(response.content)).toBe(true);
    expect(response.content[0]).toHaveProperty('type', 'text');
    expect(typeof response.content[0].text).toBe('string');

    const responseText = response.content[0].text;

    // Verify promptOrigin is included
    expect(responseText).toContain(
      `${promptOriginKey}: ${PromptOrigin.requirements}`,
    );

    // Verify model is included
    expect(responseText).toContain(`${modelKey}: ${defaultModel}`);

    // Verify template and schema are present
    expect(responseText).toContain(
      `${promptTemplateKey}: This is a test template with {{variable}}`,
    );
    expect(responseText).toContain(`${contextSchemaKey}: {`);
    expect(responseText).toContain('"variable": "Description of the variable"');

    // Verify next steps format
    expect(responseText).toContain('NEXT STEP:');
    expect(responseText).toContain(
      `${PromptWorkbenchToolName.recommend_prompt_template_tests}`,
    );
    expect(responseText).toContain(
      `template: the \`${promptTemplateKey}\` above`,
    );
    expect(responseText).toContain(
      `${contextSchemaKey}: the \`${contextSchemaKey}\` above`,
    );
    expect(responseText).toContain(
      `${promptOriginKey}: the \`${promptOriginKey}\` above`,
    );
    expect(responseText).toContain(`${modelKey}: the \`${modelKey}\` above`);
  });

  it('should handle errors from CircletClient', async () => {
    const mockCircletInstance = {
      circlet: {
        createPromptTemplate: vi.fn().mockRejectedValue(new Error('API error')),
      },
    };

    vi.mocked(CircletClient).mockImplementation(
      () => mockCircletInstance as any,
    );

    const args = {
      params: {
        prompt: 'Create a test prompt template',
        promptOrigin: PromptOrigin.requirements,
        model: defaultModel,
      },
    };

    const controller = new AbortController();

    await expect(
      createPromptTemplate(args, { signal: controller.signal }),
    ).rejects.toThrow('API error');
  });
});

```

--------------------------------------------------------------------------------
/src/clients/circleci/httpClient.ts:
--------------------------------------------------------------------------------

```typescript
export class HTTPClient {
  protected baseURL: string;
  protected headers: HeadersInit;

  constructor(
    baseURL: string,
    apiPath: string,
    options?: {
      headers?: HeadersInit;
    },
  ) {
    const { headers } = options || {};
    this.baseURL = baseURL + apiPath;
    this.headers = headers || {};
  }

  /**
   * Helper method to build URL with query parameters
   */
  protected buildURL(path: string, params?: Record<string, any>): URL {
    const url = new URL(`${this.baseURL}${path}`);
    if (params && typeof params === 'object') {
      Object.entries(params).forEach(([key, value]) => {
        if (value !== undefined) {
          if (Array.isArray(value)) {
            value.forEach((v) => {
              url.searchParams.append(key, String(v));
            });
          } else if (typeof value === 'object') {
            url.searchParams.append(key, JSON.stringify(value));
          } else {
            url.searchParams.append(key, String(value));
          }
        }
      });
    }
    return url;
  }

  /**
   * Helper method to handle API responses
   */
  protected async handleResponse<T>(response: Response): Promise<T> {
    if (!response.ok) {
      const errorData = await response.json().catch(() => ({}));
      if (response.status >= 400 && response.status < 600) {
        throw new Error(
          `CircleCI API Error: ${response.status} \nURL: ${response.url} \nMessage: ${errorData.message || response.statusText}`,
        );
      }
      throw new Error('No response received from CircleCI API');
    }

    return response.text().then((text) => {
      try {
        return JSON.parse(text) as T;
      } catch {
        return text as unknown as T;
      }
    });
  }

  /**
   * Helper method to make GET requests
   */
  async get<T>(path: string, params?: Record<string, any>) {
    const url = this.buildURL(path, params);
    const response = await fetch(url.toString(), {
      method: 'GET',
      headers: this.headers,
    });

    return this.handleResponse<T>(response);
  }

  /**
   * Helper method to make POST requests
   */
  async post<T>(
    path: string,
    data?: Record<string, any>,
    params?: Record<string, any>,
  ) {
    const url = this.buildURL(path, params);
    const response = await fetch(url.toString(), {
      method: 'POST',
      headers: this.headers,
      body: data ? JSON.stringify(data) : undefined,
    });

    return this.handleResponse<T>(response);
  }

  /**
   * Helper method to make DELETE requests
   */
  async delete<T>(path: string, params?: Record<string, any>) {
    const url = this.buildURL(path, params);
    const response = await fetch(url.toString(), {
      method: 'DELETE',
      headers: this.headers,
    });

    return this.handleResponse<T>(response);
  }

  /**
   * Helper method to make PUT requests
   */
  async put<T>(
    path: string,
    data?: Record<string, any>,
    params?: Record<string, any>,
  ) {
    const url = this.buildURL(path, params);
    const response = await fetch(url.toString(), {
      method: 'PUT',
      headers: this.headers,
      body: data ? JSON.stringify(data) : undefined,
    });

    return this.handleResponse<T>(response);
  }

  /**
   * Helper method to make PATCH requests
   */
  async patch<T>(
    path: string,
    data?: Record<string, any>,
    params?: Record<string, any>,
  ) {
    const url = this.buildURL(path, params);
    const response = await fetch(url.toString(), {
      method: 'PATCH',
      headers: this.headers,
      body: data ? JSON.stringify(data) : undefined,
    });

    return this.handleResponse<T>(response);
  }
}

```

--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------

```yaml
version: 2.1

orbs:
  node: circleci/[email protected]
  docker: circleci/[email protected]

commands:
  setup:
    steps:
      - checkout
      - run:
          name: Extract package info
          command: |
            PACKAGE_NAME="$(jq --raw-output .name package.json)"
            PACKAGE_VERSION="$(jq --raw-output .version package.json)"
            FULL_IDENTIFIER="$PACKAGE_NAME@$PACKAGE_VERSION"
            
            echo "export PACKAGE_NAME=$PACKAGE_NAME" >> $BASH_ENV
            echo "export PACKAGE_VERSION=$PACKAGE_VERSION" >> $BASH_ENV
            echo "export FULL_IDENTIFIER=$FULL_IDENTIFIER" >> $BASH_ENV
            
            echo "Package: $PACKAGE_NAME"
            echo "Version: $PACKAGE_VERSION"
            echo "Full identifier: $FULL_IDENTIFIER"

  login:
    steps:
      - run: echo "//registry.npmjs.org/:_authToken=$NPM_ACCESS_TOKEN" >> ~/.npmrc

  install-deps:
    steps:
      - node/install-packages:
          pkg-manager: pnpm
          cache-path: node_modules
          override-ci-command: pnpm install

executors:
  node-executor:
    docker:
      - image: cimg/node:22.14
  
  docker-executor:
    machine:
      image: ubuntu-2404:current
      docker_layer_caching: true

jobs:
  build:
    executor: node-executor
    steps:
      - setup
      - install-deps
      - run:
          name: Build
          command: pnpm build
      - persist_to_workspace:
          root: .
          paths:
            - .

  test:
    executor: node-executor
    steps:
      - attach_workspace:
          at: .
      - install-deps
      - run:
          name: Run Tests
          command: pnpm test:run

  lint:
    executor: node-executor
    steps:
      - attach_workspace:
          at: .
      - install-deps
      - run:
          name: Run Linting
          command: pnpm lint
      - run:
          name: Type Check
          command: pnpm typecheck

  publish-release:
    executor: node-executor
    steps:
      - setup
      - install-deps
      - attach_workspace:
          at: .
      - login
      - run:
          name: Publish npm Package
          command: |
            echo "Checking for published version: $FULL_IDENTIFIER..."
            if ! pnpm view $FULL_IDENTIFIER --json > /dev/null 2>&1; then
              echo "Publishing $FULL_IDENTIFIER…"
              pnpm publish --no-git-checks
            else
              echo "$FULL_IDENTIFIER already published. Doing nothing."
            fi

  publish-docker-image:
    executor: docker-executor
    steps:
      - setup
      - attach_workspace:
          at: .
      - run:
          name: Set up Docker Buildx
          command: |
            docker buildx create --name multiarch --use
            docker buildx inspect --bootstrap
      - docker/check
      - run:
          name: Build and push multi-architecture Docker image
          command: |
            docker buildx build --platform linux/amd64,linux/arm64 \
              -t ${DOCKER_NAMESPACE}/mcp-server-circleci:latest \
              -t ${DOCKER_NAMESPACE}/mcp-server-circleci:${PACKAGE_VERSION} \
              -t ${DOCKER_NAMESPACE}/mcp-server-circleci:${CIRCLE_SHA1} \
              --push .

workflows:
  build-and-test:
    jobs:
      - build
      - test:
          requires:
            - build
      - lint:
          requires:
            - build
      - publish-release:
          context: npm-registry-public
          filters:
            branches:
              only: main
          requires:
            - build
            - lint
            - test
      - publish-docker-image:
          context: mcp-server-docker-publish
          filters:
            branches:
              only: main
          requires:
            - build
            - lint
            - test

```

--------------------------------------------------------------------------------
/src/tools/runPipeline/handler.ts:
--------------------------------------------------------------------------------

```typescript
import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
import {
  getBranchFromURL,
  getProjectSlugFromURL,
  identifyProjectSlug,
} from '../../lib/project-detection/index.js';
import { runPipelineInputSchema } from './inputSchema.js';
import mcpErrorOutput from '../../lib/mcpErrorOutput.js';
import { getCircleCIClient } from '../../clients/client.js';
import { getAppURL } from '../../clients/circleci/index.js';

export const runPipeline: ToolCallback<{
  params: typeof runPipelineInputSchema;
}> = async (args) => {
  const {
    workspaceRoot,
    gitRemoteURL,
    branch,
    configContent,
    projectURL,
    pipelineChoiceName,
    projectSlug: inputProjectSlug,
  } = args.params ?? {};

  let projectSlug: string | undefined;
  let branchFromURL: string | undefined;
  const baseURL = getAppURL();
  if (inputProjectSlug) {
    if (!branch) {
      return mcpErrorOutput(
        'Branch not provided. When using projectSlug, a branch must also be specified.',
      );
    }
    projectSlug = inputProjectSlug;
  } else if (projectURL) {
    projectSlug = getProjectSlugFromURL(projectURL);
    branchFromURL = getBranchFromURL(projectURL);
  } else if (workspaceRoot && gitRemoteURL && branch) {
    projectSlug = await identifyProjectSlug({
      gitRemoteURL,
    });
  } else {
    return mcpErrorOutput(
      'Missing required inputs. Please provide either: 1) projectSlug with branch, 2) projectURL, or 3) workspaceRoot with gitRemoteURL and branch.',
    );
  }

  if (!projectSlug) {
    return mcpErrorOutput(`
          Project not found. Ask the user to provide the inputs user can provide based on the tool description.

          Project slug: ${projectSlug}
          Git remote URL: ${gitRemoteURL}
          Branch: ${branch}
          `);
  }
  const foundBranch = branchFromURL || branch;
  if (!foundBranch) {
    return mcpErrorOutput(
      'No branch provided. Ask the user to provide the branch.',
    );
  }

  const circleci = getCircleCIClient();
  const { id: projectId } = await circleci.projects.getProject({
    projectSlug,
  });
  const pipelineDefinitions = await circleci.pipelines.getPipelineDefinitions({
    projectId,
  });

  const pipelineChoices = [
    ...pipelineDefinitions.map((definition) => ({
      name: definition.name,
      definitionId: definition.id,
    })),
  ];

  if (pipelineChoices.length === 0) {
    return mcpErrorOutput(
      'No pipeline definitions found. Please make sure your project is set up on CircleCI to run pipelines.',
    );
  }

  const formattedPipelineChoices = pipelineChoices
    .map(
      (pipeline, index) =>
        `${index + 1}. ${pipeline.name} (definitionId: ${pipeline.definitionId})`,
    )
    .join('\n');

  if (pipelineChoices.length > 1 && !pipelineChoiceName) {
    return {
      content: [
        {
          type: 'text',
          text: `Multiple pipeline definitions found. Please choose one of the following:\n${formattedPipelineChoices}`,
        },
      ],
    };
  }

  const chosenPipeline = pipelineChoiceName
    ? pipelineChoices.find((pipeline) => pipeline.name === pipelineChoiceName)
    : undefined;

  if (pipelineChoiceName && !chosenPipeline) {
    return mcpErrorOutput(
      `Pipeline definition with name ${pipelineChoiceName} not found. Please choose one of the following:\n${formattedPipelineChoices}`,
    );
  }

  const runPipelineDefinitionId =
    chosenPipeline?.definitionId || pipelineChoices[0].definitionId;

  const runPipelineResponse = await circleci.pipelines.runPipeline({
    projectSlug,
    branch: foundBranch,
    definitionId: runPipelineDefinitionId,
    configContent,
  });

  return {
    content: [
      {
        type: 'text',
        text: `Pipeline run successfully. View it at: ${baseURL}/pipelines/${projectSlug}/${runPipelineResponse.number}`,
      },
    ],
  };
};

```

--------------------------------------------------------------------------------
/src/lib/rateLimitedRequests/index.ts:
--------------------------------------------------------------------------------

```typescript
type BatchState = {
  batchItemsToFire: any[];
  totalRequests: number;
  completedRequests: number;
};

type BatchOptions = {
  batchSize?: number;
  onProgress?: (progress: {
    totalRequests: number;
    completedRequests: number;
  }) => void;
  onBatchComplete?: (result: {
    startIndex: number;
    stopIndex: number;
    results: any[];
  }) => void;
};

type BatchResult = {
  startIndex: number;
  stopIndex: number;
  results: any[];
};

const RATE_LIMIT_INTERVAL = 2000;
const RATE_LIMIT_MAX_REQUESTS = 40;

const ifAllItemsArePopulated = (
  batchState: BatchState,
  startIndex: number,
  endIndex: number,
): boolean => {
  for (let i = startIndex; i < endIndex; i++) {
    if (
      i < batchState.batchItemsToFire.length &&
      batchState.batchItemsToFire[i] === undefined
    ) {
      return false;
    }
  }
  return true;
};

const onProgressFired = (
  batchState: BatchState,
  startIndex: number,
  endIndex: number,
  onProgress: (data: {
    totalRequests: number;
    completedRequests: number;
  }) => void,
): void => {
  batchState.completedRequests += endIndex - startIndex;
  const data = {
    totalRequests: batchState.totalRequests,
    completedRequests: batchState.completedRequests,
  };
  onProgress(data);
};

const onBatchCompleteFired = (
  batchState: BatchState,
  batchItems: any[],
  startIndex: number,
  endIndex: number,
  batchSize: number,
  onBatchComplete: (result: BatchResult) => void,
): void => {
  for (let i = startIndex; i < endIndex; i++) {
    batchState.batchItemsToFire[i] = batchItems[i - startIndex];
  }

  for (let i = 0; i < batchState.batchItemsToFire.length; i = i + batchSize) {
    const batchEndIndex = i + batchSize;
    const allItemsArePopulated = ifAllItemsArePopulated(
      batchState,
      i,
      batchEndIndex,
    );
    if (allItemsArePopulated) {
      const batch = batchState.batchItemsToFire.slice(i, batchEndIndex);
      const result = {
        startIndex: i,
        stopIndex: Math.min(
          batchEndIndex - 1,
          batchState.batchItemsToFire.length - 1,
        ),
        results: batch,
      };
      for (let j = 0; j < batchEndIndex; j++) {
        batchState.batchItemsToFire[j] = undefined;
      }
      onBatchComplete(result);
    }
  }
};

const onBatchFinish = (
  batchState: BatchState,
  batch: Promise<any>[],
  options: BatchOptions | undefined,
  startIndex: number,
  endIndex: number,
): void => {
  Promise.all(batch).then((batchItems) => {
    if (options?.batchSize && options.onBatchComplete) {
      onBatchCompleteFired(
        batchState,
        batchItems,
        startIndex,
        endIndex,
        options.batchSize,
        options.onBatchComplete,
      );
    }
    if (options?.onProgress) {
      onProgressFired(batchState, startIndex, endIndex, options.onProgress);
    }
  });
};

export const rateLimitedRequests = async <T>(
  requests: (() => Promise<T>)[],
  maxRequests = RATE_LIMIT_MAX_REQUESTS,
  interval = RATE_LIMIT_INTERVAL,
  options?: BatchOptions,
): Promise<T[]> => {
  const batchState: BatchState = {
    batchItemsToFire: new Array(requests.length),
    totalRequests: requests.length,
    completedRequests: 0,
  };

  const result = new Array(requests.length);
  const promises: Promise<T>[] = [];

  for (
    let startIndex = 0;
    startIndex < requests.length;
    startIndex += maxRequests
  ) {
    const endIndex = Math.min(startIndex + maxRequests, requests.length);
    const batch = requests.slice(startIndex, endIndex).map((execute, index) =>
      Promise.resolve(execute()).then((res) => {
        result[startIndex + index] = res;
        return res;
      }),
    );

    onBatchFinish(batchState, batch, options, startIndex, endIndex);
    promises.push(...batch);

    if (endIndex < requests.length) {
      await new Promise((resolve) => setTimeout(resolve, interval));
    }
  }

  await Promise.all(promises);
  return result;
};

```

--------------------------------------------------------------------------------
/src/lib/usage-api/findUnderusedResourceClasses.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, vi, type Mock } from 'vitest';
import {
  readAndParseCSV,
  validateCSVColumns,
  groupRecordsByJob,
  analyzeJobGroups,
  generateReport,
} from './findUnderusedResourceClasses.js';
import * as fs from 'fs';

vi.mock('fs');

describe('findUnderusedResourceClasses library functions', () => {

  const CSV_HEADERS = 'project_name,workflow_name,job_name,resource_class,median_cpu_utilization_pct,max_cpu_utilization_pct,median_ram_utilization_pct,max_ram_utilization_pct,compute_credits';
  const CSV_ROW_UNDER = 'proj,flow,build,medium,10,20,15,18,100';
  const CSV_ROW_OVER = 'proj,flow,test,large,50,60,55,58,200';
  const CSV_CONTENT = `${CSV_HEADERS}\n${CSV_ROW_UNDER}\n${CSV_ROW_OVER}`;
  const mockRecords = [
      {
        project_name: 'proj',
        workflow_name: 'flow',
        job_name: 'build',
        resource_class: 'medium',
        median_cpu_utilization_pct: '10',
        max_cpu_utilization_pct: '20',
        median_ram_utilization_pct: '15',
        max_ram_utilization_pct: '18',
        compute_credits: '100'
      },
      {
        project_name: 'proj',
        workflow_name: 'flow',
        job_name: 'test',
        resource_class: 'large',
        median_cpu_utilization_pct: '50',
        max_cpu_utilization_pct: '60',
        median_ram_utilization_pct: '55',
        max_ram_utilization_pct: '58',
        compute_credits: '200'
      }
  ];

  describe('readAndParseCSV', () => {
    it('should read and parse a CSV file correctly', () => {
      (fs.readFileSync as Mock).mockReturnValue(CSV_CONTENT);
      const records = readAndParseCSV('dummy/path.csv');
      expect(records).toHaveLength(2);
      expect(records[0].project_name).toBe('proj');
    });

    it('should throw an error if file read fails', () => {
        (fs.readFileSync as Mock).mockImplementation(() => {
            throw new Error('File not found');
        });
        expect(() => readAndParseCSV('bad/path.csv')).toThrow('Could not read CSV file');
    });
  });

  describe('validateCSVColumns', () => {
    it('should not throw an error for valid records', () => {
      expect(() => validateCSVColumns(mockRecords)).not.toThrow();
    });

    it('should throw an error for missing required columns', () => {
      const invalidRecords = [{ project_name: 'proj' }];
      expect(() => validateCSVColumns(invalidRecords)).toThrow('CSV is missing required columns');
    });
  });

  describe('groupRecordsByJob', () => {
    it('should group records by job identifier', () => {
      const grouped = groupRecordsByJob(mockRecords);
      expect(grouped.size).toBe(2);
      expect(grouped.has('proj|||flow|||build|||medium')).toBe(true);
    });
  });

  describe('analyzeJobGroups', () => {
    it('should identify underused jobs', () => {
      const grouped = groupRecordsByJob(mockRecords);
      const underused = analyzeJobGroups(grouped, 40);
      expect(underused).toHaveLength(1);
      expect(underused[0].job).toBe('build');
    });
    
    it('should return an empty array if no jobs are underused', () => {
        const grouped = groupRecordsByJob([mockRecords[1]]);
        const underused = analyzeJobGroups(grouped, 40);
        expect(underused).toHaveLength(0);
    });
  });

  describe('generateReport', () => {
    it('should generate a report for underused jobs', () => {
      const underusedJobs = [{
        projectName: 'proj',
        workflowName: 'flow',
        job: 'build',
        resourceClass: 'medium',
        avgCpu: 10,
        maxCpu: 20,
        avgRam: 15,
        maxRam: 18,
        count: 1,
        totalComputeCredits: 100
      }];
      const report = generateReport(underusedJobs, 40);
      expect(report).toContain('Underused resource classes');
      expect(report).toContain('Project: proj');
    });

    it('should generate a message when no jobs are underused', () => {
      const report = generateReport([], 40);
      expect(report).toBe('No underused resource classes found (threshold: 40%).');
    });
  });
});

```

--------------------------------------------------------------------------------
/src/tools/recommendPromptTemplateTests/handler.ts:
--------------------------------------------------------------------------------

```typescript
import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
import { recommendPromptTemplateTestsInputSchema } from './inputSchema.js';
import { CircletClient } from '../../clients/circlet/index.js';
import {
  fileExtension,
  fileNameExample1,
  fileNameExample2,
  fileNameExample3,
  fileNameTemplate,
  PromptOrigin,
  promptsOutputDirectory,
} from '../shared/constants.js';

const promptTemplateKey = 'promptTemplate';
const modelKey = 'model';
const temperatureKey = 'temperature';
const contextSchemaKey = 'contextSchema';
const promptOriginKey = 'promptOrigin';
const recommendedTestsKey = 'recommendedTests';

export const recommendPromptTemplateTests: ToolCallback<{
  params: typeof recommendPromptTemplateTestsInputSchema;
}> = async (args) => {
  const { template, contextSchema, promptOrigin } = args.params ?? {};

  const circlet = new CircletClient();
  const result = await circlet.circlet.recommendPromptTemplateTests({
    template,
    contextSchema,
  });

  const baseInstructions = `${recommendedTestsKey}: ${JSON.stringify(result, null, 2)}

NEXT STEP:
- Immediately save the \`${promptTemplateKey}\`, \`${contextSchemaKey}\`, and \`${recommendedTestsKey}\` to a single file containing the prompt template, context schema, and tests in a simple structured format (e.g. YAML, JSON, or whatever is most appropriate for the language of the current repository).
  - The ${fileExtension} file should be named in the format '${fileNameTemplate}' (e.g. '${fileNameExample1}', '${fileNameExample2}', '${fileNameExample3}', etc.)
  - The file should have the following keys:
    - \`name\`: string (the name of the prompt template)
    - \`description\`: string (a description of the prompt template)
    - \`version\`: string (the semantic version of the prompt template, e.g. "1.0.0")
    - \`${promptOriginKey}\`: string (the origin of the prompt template, e.g. "${PromptOrigin.codebase}" or "${PromptOrigin.requirements}")
    - \`${modelKey}\`: string (the model used for generating the prompt template and tests)
    - \`${temperatureKey}\`: number (the temperature used for generating the prompt template and tests)
    - \`template\`: multi-line string (the prompt template)
    - \`${contextSchemaKey}\`: object (the \`${contextSchemaKey}\`)
    - \`tests\`: array of objects (based on the \`${recommendedTestsKey}\`)
      - \`name\`: string (a relevant "Title Case" name for the test, based on the content of the \`${recommendedTestsKey}\` array item)
      - \`description\`: string (taken directly from string array item in \`${recommendedTestsKey}\`)
    - \`sampleInputs\`: object[] (the sample inputs for the \`${promptTemplateKey}\` and any tests within \`${recommendedTestsKey}\`)

RULES FOR SAVING FILES:
- The files should be saved in the \`${promptsOutputDirectory}\` directory at the root of the project.
- Files should be written with respect to the prevailing conventions of the current repository.
- The prompt files should be documented with a README description of what they do, and how they work.
  - If a README already exists in the \`${promptsOutputDirectory}\` directory, update it with the new prompt template information.
  - If a README does not exist in the \`${promptsOutputDirectory}\` directory, create one.
- The files should be formatted using the user's preferred conventions.
- Only save the following files (and nothing else):
  - \`${fileNameTemplate}\`
  - \`README.md\``;

  const integrationInstructions =
    promptOrigin === PromptOrigin.codebase
      ? `

FINALLY, ONCE ALL THE FILES ARE SAVED:
1. Ask user if they want to integrate the new templates into their app as a more tested and trustworthy replacement for their pre-existing prompt implementations. (Yes/No)
2. If yes, import the \`${promptsOutputDirectory}\` files into their app, following codebase conventions
3. Only use existing dependencies - no new imports
4. Ensure integration is error-free and builds successfully`
      : '';

  return {
    content: [
      {
        type: 'text',
        text: baseInstructions + integrationInstructions,
      },
    ],
  };
};

```

--------------------------------------------------------------------------------
/src/lib/pipeline-job-logs/getJobLogs.ts:
--------------------------------------------------------------------------------

```typescript
import { getCircleCIPrivateClient } from '../../clients/client.js';
import { getCircleCIClient } from '../../clients/client.js';
import { rateLimitedRequests } from '../rateLimitedRequests/index.js';
import { JobDetails } from '../../clients/schemas.js';
import outputTextTruncated, { SEPARATOR } from '../outputTextTruncated.js';

export type GetJobLogsParams = {
  projectSlug: string;
  jobNumbers: number[];
  failedStepsOnly?: boolean;
};

type StepLog = {
  stepName: string;
  logs: {
    output: string;
    error: string;
  };
};

type JobWithStepLogs = {
  jobName: string;
  steps: (StepLog | null)[];
};

/**
 * Retrieves job logs from CircleCI
 * @param params Object containing project slug, job numbers, and optional flag to filter for failed steps only
 * @param params.projectSlug The slug of the project to retrieve logs for
 * @param params.jobNumbers The numbers of the jobs to retrieve logs for
 * @param params.failedStepsOnly Whether to filter for failed steps only
 * @returns Array of job logs with step information
 */
const getJobLogs = async ({
  projectSlug,
  jobNumbers,
  failedStepsOnly = true,
}: GetJobLogsParams): Promise<JobWithStepLogs[]> => {
  const circleci = getCircleCIClient();
  const circleciPrivate = getCircleCIPrivateClient();

  const jobsDetails = (
    await rateLimitedRequests(
      jobNumbers.map((jobNumber) => async () => {
        try {
          return await circleci.jobsV1.getJobDetails({
            projectSlug,
            jobNumber,
          });
        } catch (error) {
          if (error instanceof Error && error.message.includes('404')) {
            console.error(`Job ${jobNumber} not found:`, error);
            // some jobs might not be found, return null in that case
            return null;
          } else if (error instanceof Error && error.message.includes('429')) {
            console.error(`Rate limited for job request ${jobNumber}:`, error);
            // some requests might be rate limited, return null in that case
            return null;
          }
          throw error;
        }
      }),
    )
  ).filter((job): job is JobDetails => job !== null);

  const allLogs = await Promise.all(
    jobsDetails.map(async (job) => {
      // Get logs for all steps and their actions
      const stepLogs = await Promise.all(
        job.steps.flatMap((step) => {
          let actions = step.actions;
          if (failedStepsOnly) {
            actions = actions.filter((action) => action.failed === true);
          }
          return actions.map(async (action) => {
            try {
              const logs = await circleciPrivate.jobs.getStepOutput({
                projectSlug,
                jobNumber: job.build_num,
                taskIndex: action.index,
                stepId: action.step,
              });
              return {
                stepName: step.name,
                logs,
              };
            } catch (error) {
              console.error('error in step', step.name, error);
              // Some steps might not have logs, return null in that case
              return null;
            }
          });
        }),
      );

      return {
        jobName: job.workflows.job_name,
        steps: stepLogs.filter(Boolean), // Remove any null entries
      };
    }),
  );

  return allLogs;
};

export default getJobLogs;

/**
 * Formats job logs into a standardized output structure
 * @param logs Array of job logs containing step information
 * @returns Formatted output object with text content
 */
export function formatJobLogs(jobStepLogs: JobWithStepLogs[]) {
  if (jobStepLogs.length === 0) {
    return {
      content: [
        {
          type: 'text' as const,
          text: 'No logs found.',
        },
      ],
    };
  }
  const outputText = jobStepLogs
    .map((log) => `${SEPARATOR}Job: ${log.jobName}\n` + formatSteps(log))
    .join('\n');
  return outputTextTruncated(outputText);
}

const formatSteps = (jobStepLog: JobWithStepLogs) => {
  if (jobStepLog.steps.length === 0) {
    return 'No steps found.';
  }
  return jobStepLog.steps
    .map(
      (step) =>
        `Step: ${step?.stepName}\n` + `Logs: ${JSON.stringify(step?.logs)}`,
    )
    .join('\n');
};

```

--------------------------------------------------------------------------------
/src/circleci-tools.ts:
--------------------------------------------------------------------------------

```typescript
import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
import { getBuildFailureLogsTool } from './tools/getBuildFailureLogs/tool.js';
import { getBuildFailureLogs } from './tools/getBuildFailureLogs/handler.js';
import { getFlakyTestLogsTool } from './tools/getFlakyTests/tool.js';
import { getFlakyTestLogs } from './tools/getFlakyTests/handler.js';
import { getLatestPipelineStatusTool } from './tools/getLatestPipelineStatus/tool.js';
import { getLatestPipelineStatus } from './tools/getLatestPipelineStatus/handler.js';
import { getJobTestResultsTool } from './tools/getJobTestResults/tool.js';
import { getJobTestResults } from './tools/getJobTestResults/handler.js';
import { configHelper } from './tools/configHelper/handler.js';
import { configHelperTool } from './tools/configHelper/tool.js';
import { createPromptTemplate } from './tools/createPromptTemplate/handler.js';
import { createPromptTemplateTool } from './tools/createPromptTemplate/tool.js';
import { recommendPromptTemplateTestsTool } from './tools/recommendPromptTemplateTests/tool.js';
import { recommendPromptTemplateTests } from './tools/recommendPromptTemplateTests/handler.js';
import { runPipeline } from './tools/runPipeline/handler.js';
import { runPipelineTool } from './tools/runPipeline/tool.js';
import { listFollowedProjectsTool } from './tools/listFollowedProjects/tool.js';
import { listFollowedProjects } from './tools/listFollowedProjects/handler.js';
import { runEvaluationTestsTool } from './tools/runEvaluationTests/tool.js';
import { runEvaluationTests } from './tools/runEvaluationTests/handler.js';
import { rerunWorkflowTool } from './tools/rerunWorkflow/tool.js';
import { rerunWorkflow } from './tools/rerunWorkflow/handler.js';
import { downloadUsageApiDataTool } from './tools/downloadUsageApiData/tool.js';
import { downloadUsageApiData } from './tools/downloadUsageApiData/handler.js';
import { findUnderusedResourceClassesTool } from './tools/findUnderusedResourceClasses/tool.js';
import { findUnderusedResourceClasses } from './tools/findUnderusedResourceClasses/handler.js';
import { analyzeDiffTool } from './tools/analyzeDiff/tool.js';
import { analyzeDiff } from './tools/analyzeDiff/handler.js';
import { runRollbackPipelineTool } from './tools/runRollbackPipeline/tool.js';
import { runRollbackPipeline } from './tools/runRollbackPipeline/handler.js';

import { listComponentVersionsTool } from './tools/listComponentVersions/tool.js';
import { listComponentVersions } from './tools/listComponentVersions/handler.js';

// Define the tools with their configurations
export const CCI_TOOLS = [
  getBuildFailureLogsTool,
  getFlakyTestLogsTool,
  getLatestPipelineStatusTool,
  getJobTestResultsTool,
  configHelperTool,
  createPromptTemplateTool,
  recommendPromptTemplateTestsTool,
  runPipelineTool,
  listFollowedProjectsTool,
  runEvaluationTestsTool,
  rerunWorkflowTool,
  downloadUsageApiDataTool,
  findUnderusedResourceClassesTool,
  analyzeDiffTool,
  runRollbackPipelineTool,
  listComponentVersionsTool,
];

// Extract the tool names as a union type
type CCIToolName = (typeof CCI_TOOLS)[number]['name'];

export type ToolHandler<T extends CCIToolName> = ToolCallback<{
  params: Extract<(typeof CCI_TOOLS)[number], { name: T }>['inputSchema'];
}>;

// Create a type for the tool handlers that directly maps each tool to its appropriate input schema
type ToolHandlers = {
  [K in CCIToolName]: ToolHandler<K>;
};

export const CCI_HANDLERS = {
  get_build_failure_logs: getBuildFailureLogs,
  find_flaky_tests: getFlakyTestLogs,
  get_latest_pipeline_status: getLatestPipelineStatus,
  get_job_test_results: getJobTestResults,
  config_helper: configHelper,
  create_prompt_template: createPromptTemplate,
  recommend_prompt_template_tests: recommendPromptTemplateTests,
  run_pipeline: runPipeline,
  list_followed_projects: listFollowedProjects,
  run_evaluation_tests: runEvaluationTests,
  rerun_workflow: rerunWorkflow,
  download_usage_api_data: downloadUsageApiData,
  find_underused_resource_classes: findUnderusedResourceClasses,
  analyze_diff: analyzeDiff,
  run_rollback_pipeline: runRollbackPipeline,
  list_component_versions: listComponentVersions,
} satisfies ToolHandlers;

```

--------------------------------------------------------------------------------
/scripts/create-tool.js:
--------------------------------------------------------------------------------

```javascript
#!/usr/bin/env node
/* eslint-disable no-undef */
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

// Get the current file's directory name
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// Get tool name from command line arguments
const toolName = process.argv[2];

if (!toolName) {
  console.error('Please provide a tool name');
  console.error('Example: node scripts/create-tool.js myNewTool');
  process.exit(1);
}

// Convert toolName to snake_case for tool name and camelCase for variables
const snakeCaseName = toolName
  .replace(/([a-z])([A-Z])/g, '$1_$2')
  .toLowerCase();

const camelCaseName = snakeCaseName.replace(/_([a-z])/g, (_, letter) =>
  letter.toUpperCase(),
);

// Create directory for the tool
const toolDir = path.join(
  path.resolve(__dirname, '..'),
  'src',
  'tools',
  toolName,
);

if (fs.existsSync(toolDir)) {
  console.error(`Tool directory already exists: ${toolDir}`);
  process.exit(1);
}

fs.mkdirSync(toolDir, { recursive: true });

// Create inputSchema.ts
const inputSchemaContent = `import { z } from 'zod';

export const ${camelCaseName}InputSchema = z.object({
  message: z
    .string()
    .describe(
      'A message to echo back to the user.',
    ),
});
`;

// Create tool.ts
const toolContent = `import { ${camelCaseName}InputSchema } from './inputSchema.js';

export const ${camelCaseName}Tool = {
  name: '${snakeCaseName}' as const,
  description: \`
  This tool is a basic "hello world" tool that echoes back a message provided by the user.

  Parameters:
  - params: An object containing:
    - message: string - A message provided by the user that will be echoed back.

  Example usage:
  {
    "params": {
      "message": "Hello, world!"
    }
  }

  Returns:
  - The message provided by the user.
  \`,
  inputSchema: ${camelCaseName}InputSchema,
};
`;

// Create handler.ts
const handlerContent = `import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
import { ${camelCaseName}InputSchema } from './inputSchema.js';

export const ${camelCaseName}: ToolCallback<{
  params: typeof ${camelCaseName}InputSchema;
}> = async (args) => {
  const { message } = args.params;

  return {
    content: [
      {
        type: 'text',
        text: \`Received message: \${message}\`,
      },
    ],
  };
};
`;

// Create handler.test.ts
const testContent = `import { describe, it, expect } from 'vitest';
import { ${camelCaseName} } from './handler.js';

describe('${camelCaseName}', () => {
  it('should return the message provided by the user', async () => {
    const controller = new AbortController();
    const result = await ${camelCaseName}(
      {
        params: {
          message: 'Hello, world!',
        },
      },
      {
        signal: controller.signal,
      }
    );

    expect(result).toEqual({
      content: [
        {
          type: 'text',
          text: 'Received message: Hello, world!',
        },
      ],
    });
  });
});
`;

// Write files
fs.writeFileSync(path.join(toolDir, 'inputSchema.ts'), inputSchemaContent);
fs.writeFileSync(path.join(toolDir, 'tool.ts'), toolContent);
fs.writeFileSync(path.join(toolDir, 'handler.ts'), handlerContent);
fs.writeFileSync(path.join(toolDir, 'handler.test.ts'), testContent);

console.log(`
✅ Tool created successfully!

📂 Location: ${toolDir}

The following files were created:
- inputSchema.ts - Defines the input schema for the tool
- tool.ts - Defines the tool itself, including its name, description, and schema
- handler.ts - Contains the main logic for the tool
- handler.test.ts - Contains unit tests for the tool

Next steps:
1. Implement your tool's logic in handler.ts
2. Add the tool to src/circleci-tools.ts with these steps:
   a. Import your tool:
      import { ${camelCaseName}Tool } from './tools/${toolName}/tool.js';
      import { ${camelCaseName} } from './tools/${toolName}/handler.js';
   b. Add your tool to the CCI_TOOLS array:
      export const CCI_TOOLS = [
        ...,
        ${camelCaseName}Tool,
      ];
   c. Add your handler to the CCI_HANDLERS object:
      export const CCI_HANDLERS = {
        ...,
        ${snakeCaseName}: ${camelCaseName},
      } satisfies ToolHandlers;
3. Write appropriate tests in handler.test.ts
`);

```

--------------------------------------------------------------------------------
/src/clients/circleci/index.ts:
--------------------------------------------------------------------------------

```typescript
import { HTTPClient } from './httpClient.js';
import { JobsAPI } from './jobs.js';
import { JobsV1API } from './jobsV1.js';
import { InsightsAPI } from './insights.js';
import { PipelinesAPI } from './pipelines.js';
import { WorkflowsAPI } from './workflows.js';
import { TestsAPI } from './tests.js';
import { ConfigValidateAPI } from './configValidate.js';
import { ProjectsAPI } from './projects.js';
import { UsageAPI } from './usage.js';
import { DeploysAPI } from './deploys.js';
export type TCircleCIClient = InstanceType<typeof CircleCIClients>;

export const getBaseURL = (useAPISubdomain = false) => {
  let baseURL = process.env.CIRCLECI_BASE_URL || 'https://circleci.com';

  if (useAPISubdomain) {
    baseURL = baseURL.replace('https://', 'https://api.');
  }

  return baseURL;
};

export const getAppURL = () => {
  const baseURL = process.env.CIRCLECI_BASE_URL || 'https://circleci.com';

  return baseURL.replace('https://', 'https://app.');
};

export const defaultPaginationOptions = {
  maxPages: 5,
  timeoutMs: 10000,
  findFirst: false,
} as const;

/**
 * Creates standardized headers for CircleCI API clients
 * @param params Configuration parameters
 * @param params.token CircleCI API token
 * @param params.additionalHeaders Optional headers to merge with defaults (will not override critical headers)
 * @returns Headers object for fetch API
 */
export function createCircleCIHeaders({
  token,
  additionalHeaders = {},
}: {
  token: string;
  additionalHeaders?: HeadersInit;
}): HeadersInit {
  const headers = additionalHeaders;
  Object.assign(headers, {
    'Circle-Token': token,
    'Content-Type': 'application/json',
    'User-Agent': 'CircleCI-MCP-Server/0.1',
  });

  return headers;
}

/**
 * Creates a default HTTP client for the CircleCI API v2
 * @param options Configuration parameters
 * @param options.token CircleCI API token
 * @returns HTTP client for CircleCI API v2
 */
const defaultV2HTTPClient = (options: {
  token: string;
  useAPISubdomain?: boolean;
}) => {
  if (!options.token) {
    throw new Error('Token is required');
  }

  const baseURL = getBaseURL(options.useAPISubdomain);
  const headers = createCircleCIHeaders({ token: options.token });
  return new HTTPClient(baseURL, '/api/v2', {
    headers,
  });
};

/**
 * Creates a default HTTP client for the CircleCI API v1
 * @param options Configuration parameters
 * @param options.token CircleCI API token
 * @returns HTTP client for CircleCI API v1
 */
const defaultV1HTTPClient = (options: {
  token: string;
  useAPISubdomain?: boolean;
}) => {
  if (!options.token) {
    throw new Error('Token is required');
  }

  const baseURL = getBaseURL(options.useAPISubdomain);
  const headers = createCircleCIHeaders({ token: options.token });
  return new HTTPClient(baseURL, '/api/v1.1', {
    headers,
  });
};

/**
 * Creates a default HTTP client for the CircleCI API v2
 * @param options Configuration parameters
 * @param options.token CircleCI API token
 */
export class CircleCIClients {
  protected apiPathV2 = '/api/v2';
  protected apiPathV1 = '/api/v1.1';

  public jobs: JobsAPI;
  public pipelines: PipelinesAPI;
  public workflows: WorkflowsAPI;
  public jobsV1: JobsV1API;
  public insights: InsightsAPI;
  public tests: TestsAPI;
  public configValidate: ConfigValidateAPI;
  public projects: ProjectsAPI;
  public usage: UsageAPI;
  public deploys: DeploysAPI;

  constructor({
    token,
    v2httpClient = defaultV2HTTPClient({
      token,
    }),
    v1httpClient = defaultV1HTTPClient({
      token,
    }),
    apiSubdomainV2httpClient = defaultV2HTTPClient({
      token,
      useAPISubdomain: true,
    }),
  }: {
    token: string;
    v2httpClient?: HTTPClient;
    v1httpClient?: HTTPClient;
    apiSubdomainV2httpClient?: HTTPClient;
  }) {
    this.jobs = new JobsAPI(v2httpClient);
    this.pipelines = new PipelinesAPI(v2httpClient);
    this.workflows = new WorkflowsAPI(v2httpClient);
    this.jobsV1 = new JobsV1API(v1httpClient);
    this.insights = new InsightsAPI(v2httpClient);
    this.tests = new TestsAPI(v2httpClient);
    this.configValidate = new ConfigValidateAPI(apiSubdomainV2httpClient);
    this.projects = new ProjectsAPI(v2httpClient);
    this.usage = new UsageAPI(v2httpClient);
    this.deploys = new DeploysAPI(v2httpClient);
  }
}

```

--------------------------------------------------------------------------------
/src/clients/circleci/pipelines.ts:
--------------------------------------------------------------------------------

```typescript
import {
  PaginatedPipelineResponseSchema,
  Pipeline,
  PipelineDefinitionsResponse,
  RunPipelineResponse,
  PipelineDefinition,
} from '../schemas.js';
import { HTTPClient } from './httpClient.js';
import { defaultPaginationOptions } from './index.js';

export class PipelinesAPI {
  protected client: HTTPClient;

  constructor(httpClient: HTTPClient) {
    this.client = httpClient;
  }

  /**
   * Get recent pipelines until a condition is met
   * @param params Configuration parameters
   * @param params.projectSlug The project slug (e.g., "gh/CircleCI-Public/api-preview-docs")
   * @param params.filterFn Function to filter pipelines and determine when to stop fetching
   * @param params.branch Optional branch name to filter pipelines
   * @param params.options Optional configuration for pagination limits
   * @param params.options.maxPages Maximum number of pages to fetch (default: 5)
   * @param params.options.timeoutMs Timeout in milliseconds (default: 10000)
   * @param params.options.findFirst Whether to find the first pipeline that matches the filterFn (default: false)
   * @returns Filtered pipelines until the stop condition is met
   * @throws Error if timeout or max pages reached
   */
  async getPipelines({
    projectSlug,
    branch,
    options = {},
  }: {
    projectSlug: string;
    branch?: string;
    options?: {
      maxPages?: number;
      timeoutMs?: number;
      findFirst?: boolean;
    };
  }): Promise<Pipeline[]> {
    const {
      maxPages = defaultPaginationOptions.maxPages,
      timeoutMs = defaultPaginationOptions.timeoutMs,
      findFirst = defaultPaginationOptions.findFirst,
    } = options;

    const startTime = Date.now();
    const filteredPipelines: Pipeline[] = [];
    let nextPageToken: string | null = null;
    let pageCount = 0;

    do {
      // Check timeout
      if (Date.now() - startTime > timeoutMs) {
        nextPageToken = null;
        break;
      }

      // Check page limit
      if (pageCount >= maxPages) {
        nextPageToken = null;
        break;
      }

      const params = {
        ...(branch ? { branch } : {}),
        ...(nextPageToken ? { 'page-token': nextPageToken } : {}),
      };

      const rawResult = await this.client.get<unknown>(
        `/project/${projectSlug}/pipeline`,
        params,
      );

      const result = PaginatedPipelineResponseSchema.safeParse(rawResult);

      if (!result.success) {
        throw new Error('Failed to parse pipeline response');
      }

      pageCount++;

      // Using for...of instead of forEach to allow breaking the loop
      for (const pipeline of result.data.items) {
        filteredPipelines.push(pipeline);
        if (findFirst) {
          nextPageToken = null;
          break;
        }
      }

      nextPageToken = result.data.next_page_token;
    } while (nextPageToken);

    return filteredPipelines;
  }

  async getPipelineByNumber({
    projectSlug,
    pipelineNumber,
  }: {
    projectSlug: string;
    pipelineNumber: number;
  }): Promise<Pipeline | undefined> {
    const rawResult = await this.client.get<unknown>(
      `/project/${projectSlug}/pipeline/${pipelineNumber}`,
    );

    const parsedResult = Pipeline.safeParse(rawResult);
    if (!parsedResult.success) {
      throw new Error('Failed to parse pipeline response');
    }

    return parsedResult.data;
  }

  async getPipelineDefinitions({
    projectId,
  }: {
    projectId: string;
  }): Promise<PipelineDefinition[]> {
    const rawResult = await this.client.get<unknown>(
      `/projects/${projectId}/pipeline-definitions`,
    );

    const parsedResult = PipelineDefinitionsResponse.safeParse(rawResult);
    if (!parsedResult.success) {
      throw new Error('Failed to parse pipeline definition response');
    }

    return parsedResult.data.items;
  }

  async runPipeline({
    projectSlug,
    branch,
    definitionId,
    configContent = '',
  }: {
    projectSlug: string;
    branch: string;
    definitionId: string;
    configContent?: string;
  }): Promise<RunPipelineResponse> {
    const rawResult = await this.client.post<unknown>(
      `/project/${projectSlug}/pipeline/run`,
      {
        definition_id: definitionId,
        config: {
          branch,
          content: configContent,
        },
        checkout: {
          branch,
        },
      },
    );

    const parsedResult = RunPipelineResponse.safeParse(rawResult);
    if (!parsedResult.success) {
      throw new Error('Failed to parse pipeline response');
    }

    return parsedResult.data;
  }
}

```

--------------------------------------------------------------------------------
/src/tools/listComponentVersions/tool.ts:
--------------------------------------------------------------------------------

```typescript
import { listComponentVersionsInputSchema } from './inputSchema.js';

export const listComponentVersionsTool = {
   name: 'list_component_versions' as const,
   description: `
     This tool lists all versions for a CircleCI component. It guides you through a multi-step process to gather the required information and provides lists of available options when parameters are missing.

     **Initial Requirements:**
     - You need either a \`projectSlug\` (from \`listFollowedProjects\`) or a \`projectID\`. The tool will automatically resolve the \`orgID\` from either of these.

     **Typical Flow:**
     1. **Start:** User requests component versions or deployment information.
     2. **Project Information:** Provide either \`projectSlug\` or \`projectID\`. The tool will automatically resolve the \`orgID\` and \`projectID\` as needed.
     3. **Environment Selection:** If \`environmentID\` is not provided, the tool will list all available environments for the organization and prompt the user to select one. Always return all available values without categorizing them.
     4. **Component Selection:** If \`componentID\` is not provided, the tool will list all available components for the project and prompt the user to select one. Always return all available values without categorizing them.
     5. **Version Listing:** Once both \`environmentID\` and \`componentID\` are provided, the tool will list all versions for that component in the specified environment.
     6. **Selection:** User selects a version from the list for subsequent operations.

     **Parameters:**
     - \`projectSlug\` (optional): The project slug from \`listFollowedProjects\` (e.g., "gh/organization/project"). Either this or \`projectID\` must be provided.
     - \`projectID\` (optional): The CircleCI project ID (UUID). Either this or \`projectSlug\` must be provided.
     - \`orgID\` (optional): The organization ID. If not provided, it will be automatically resolved from \`projectSlug\` or \`projectID\`.
     - \`environmentID\` (optional): The environment ID. If not provided, available environments will be listed.
     - \`componentID\` (optional): The component ID. If not provided, available components will be listed.

     **Behavior:**
     - The tool will guide you through the selection process step by step.
     - Automatically resolves \`orgID\` from \`projectSlug\` or \`projectID\` when needed.
     - When \`environmentID\` is missing, it lists environments and waits for user selection.
     - When \`componentID\` is missing (but \`environmentID\` is provided), it lists components and waits for user selection.
     - Only when both \`environmentID\` and \`componentID\` are provided will it list the actual component versions.
     - Make multiple calls to this tool as you gather the required parameters.

     **Common Use Cases:**
     - Identify which versions were deployed for a component
     - Identify which versions are live for a component
     - Identify which versions were deployed to an environment for a component
     - Identify which versions are not live for a component in an environment
     - Select a version for rollback or deployment operations
     - Obtain version name, namespace, and environment details for other CircleCI tools

     **Returns:**
     - When missing \`environmentID\`: A list of available environments with their IDs
     - When missing \`componentID\`: A list of available components with their IDs  
     - When both \`environmentID\` and \`componentID\` provided: A list of component versions with version name, namespace, environment ID, and is_live status

     **Important Notes:**
     - This tool requires multiple calls to gather all necessary information.
     - Either \`projectSlug\` or \`projectID\` must be provided; the tool will resolve the missing project information automatically.
     - The tool will prompt for missing \`environmentID\` and \`componentID\` by providing selection lists.
     - Always use the exact IDs returned by the tool in subsequent calls.
     - If pagination limits are reached, the tool will indicate that not all items could be displayed.

     **IMPORTANT:** Do not automatically run additional tools after this tool is called. Wait for explicit user instruction before executing further tool calls. The LLM MUST NOT invoke other CircleCI tools until receiving clear instruction from the user about what to do next, even if the user selects an option. It is acceptable to list out tool call options for the user to choose from, but do not execute them until instructed.
     `,
   inputSchema: listComponentVersionsInputSchema,
 };
 
```

--------------------------------------------------------------------------------
/src/transports/unified.ts:
--------------------------------------------------------------------------------

```typescript
import express from 'express';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import type { Response } from 'express';

// Debug subclass that logs every payload sent over SSE
class DebugSSETransport extends SSEServerTransport {
  constructor(path: string, res: Response) {
    super(path, res);
  }
  override async send(payload: any) {
    if (process.env.debug === 'true') {
      console.error('[DEBUG] SSE out ->', JSON.stringify(payload));
    }
    return super.send(payload);
  }
}

/**
 * Unified MCP transport: Streamable HTTP + SSE on same app/port/session.
 * - POST /mcp: JSON-RPC (single or chunked JSON)
 * - GET /mcp: SSE stream (server-initiated notifications)
 * - DELETE /mcp: Session termination
 */
export const createUnifiedTransport = (server: McpServer) => {
  const app = express();
  app.use(express.json());

  // Stateless: No in-memory session or transport store

  // Health check
  app.get('/ping', (_req, res) => {
    res.json({ result: 'pong' });
  });

  // GET /mcp → open SSE stream, assign session if needed (stateless)
  app.get('/mcp', (req, res) => {
    (async () => {
      if (process.env.debug === 'true') {
        const sessionId =
          req.header('Mcp-Session-Id') ||
          req.header('mcp-session-id') ||
          (req.query.sessionId as string);
        console.error(`[DEBUG] [GET /mcp] Incoming session:`, sessionId);
      }
      // Create SSE transport (stateless)
      const transport = new DebugSSETransport('/mcp', res);
      if (process.env.debug === 'true') {
        console.error(`[DEBUG] [GET /mcp] Created SSE transport.`);
      }
      await server.connect(transport);
      // Notify newly connected client of current tool catalogue
      await server.sendToolListChanged();
      // SSE connection will be closed by client or on disconnect
    })().catch((err) => {
      console.error('GET /mcp error:', err);
      if (!res.headersSent) res.status(500).end();
    });
  });

  // POST /mcp → Streamable HTTP, session-aware
  app.post('/mcp', (req, res) => {
    (async () => {
      try {
        if (process.env.debug === 'true') {
          const names = Object.keys((server as any)._registeredTools ?? {});
          console.error(`[DEBUG] visible tools:`, names);
          console.error(
            `[DEBUG] incoming request body:`,
            JSON.stringify(req.body),
          );
        }

        // For each POST, create a temporary, stateless transport to handle the request/response cycle.
        const httpTransport = new StreamableHTTPServerTransport({
          sessionIdGenerator: undefined, // Ensures stateless operation
        });

        // Connect the server to the transport. This wires the server's internal `_handleRequest`
        // method to the transport's `onmessage` event.
        await server.connect(httpTransport);

        // Handle the request. The transport will receive the request, pass it to the server via
        // `onmessage`, receive the response from the server via its `send` method, and then
        // write the response back to the client over the HTTP connection.
        await httpTransport.handleRequest(req, res, req.body);

        // After responding to initialize, send tool catalogue again so the freshly initialised
        // client is guaranteed to see it (the first notification may have been sent before it
        // started listening on the SSE stream).
        if (req.body?.method === 'initialize') {
          if (process.env.debug === 'true') {
            console.error(
              '[DEBUG] initialize handled -> sending tools/list_changed again',
            );
          }
          await server.sendToolListChanged();
        }
      } catch (error: any) {
        console.error('Error handling MCP request:', error);
        if (!res.headersSent) {
          res.status(500).json({
            jsonrpc: '2.0',
            error: {
              code: -32603,
              message: 'Internal server error',
              data: error.message,
            },
            id: req.body?.id || null,
          });
        }
      }
    })().catch((err) => {
      console.error('POST /mcp error:', err);
      if (!res.headersSent) res.status(500).end();
    });
  });

  // DELETE /mcp → stateless: acknowledge only
  app.delete('/mcp', (req, res) => {
    const sessionId =
      req.header('Mcp-Session-Id') ||
      req.header('mcp-session-id') ||
      (req.query.sessionId as string);
    if (process.env.debug === 'true') {
      console.error(`[DEBUG] [DELETE /mcp] Incoming sessionId:`, sessionId);
    }
    res.status(204).end();
  });

  const port = process.env.port || 8000;
  app.listen(port, () => {
    console.error(
      `CircleCI MCP unified HTTP+SSE server listening on http://0.0.0.0:${port}`,
    );
  });
};

```

--------------------------------------------------------------------------------
/src/tools/createPromptTemplate/tool.ts:
--------------------------------------------------------------------------------

```typescript
import { createPromptTemplateInputSchema } from './inputSchema.js';
import {
  PromptOrigin,
  promptsOutputDirectory,
  PromptWorkbenchToolName,
  fileNameTemplate,
  fileNameExample1,
  fileNameExample2,
  fileNameExample3,
  defaultModel,
  defaultTemperature,
} from '../shared/constants.js';

const paramsKey = 'params';
const promptKey = 'prompt';
const promptOriginKey = 'promptOrigin';
const templateKey = 'template';
const contextSchemaKey = '`contextSchema`';
const modelKey = 'model';
const temperatureKey = 'temperature';

export const createPromptTemplateTool = {
  name: PromptWorkbenchToolName.create_prompt_template,
  description: `
  ABOUT THIS TOOL:
  - This tool is part of a toolchain that generates and provides test cases for a prompt template.
  - This tool helps an AI assistant to generate a prompt template based on one of the following:
    1. feature requirements defined by a user - in which case the tool will generate a new prompt template based on the feature requirements.
    2. a pre-existing prompt or prompt template that a user wants to test, evaluate, or modify - in which case the tool will convert it into a more structured and testable prompt template while leaving the original prompt language relatively unchanged.
  - This tool will return a structured prompt template (e.g. \`${templateKey}\`) along with a context schema (e.g. \`${contextSchemaKey}\`) that defines the expected input parameters for the prompt template.
  - In some cases, a user will want to add test coverage for ALL of the prompts in a given application. In these cases, the AI agent should use this tool to generate a prompt template for each prompt in the application, and should check the entire application for AI prompts that are not already covered by a prompt template in the \`${promptsOutputDirectory}\` directory.

  WHEN SHOULD THIS TOOL BE TRIGGERED?
  - This tool should be triggered whenever the user provides requirements for a new AI-enabled application or a new AI-enabled feature of an existing  application (i.e. one that requires a prompt request to an LLM or any AI model).
  - This tool should also be triggered if the user provides a pre-existing prompt or prompt template from their codebase that they want to test, evaluate, or modify.
  - This tool should be triggered even if there are pre-existing files in the \`${promptsOutputDirectory}\` directory with the \`${fileNameTemplate}\` convention (e.g. \`${fileNameExample1}\`, \`${fileNameExample2}\`, \`${fileNameExample3}\`, etc.). Similar files should NEVER be generated directly by the AI agent. Instead, the AI agent should use this tool to first generate a new prompt template.

  PARAMETERS:
  - ${paramsKey}: object
    - ${promptKey}: string (the feature requirements or pre-existing prompt/prompt template that will be used to generate a prompt template. Can be a multi-line string.)
    - ${promptOriginKey}: "${PromptOrigin.codebase}" | "${PromptOrigin.requirements}" (indicates whether the prompt comes from an existing codebase or from new requirements)
    - ${modelKey}: string (the model that the prompt template will be tested against. Explicitly specify the model if it can be inferred from the codebase. Otherwise, defaults to \`${defaultModel}\`.)
    - ${temperatureKey}: number (the temperature of the prompt template. Explicitly specify the temperature if it can be inferred from the codebase. Otherwise, defaults to ${defaultTemperature}.)

  EXAMPLE USAGE (from new requirements):
  {
    "${paramsKey}": {
      "${promptKey}": "Create an app that takes any topic and an age (in years), then renders a 1-minute bedtime story for a person of that age.",
      "${promptOriginKey}": "${PromptOrigin.requirements}"
      "${modelKey}": "${defaultModel}"
      "${temperatureKey}": 1.0
    }
  }

  EXAMPLE USAGE (from pre-existing prompt/prompt template in codebase):
  {
    "${paramsKey}": {
      "${promptKey}": "The user wants a bedtime story about {{topic}} for a person of age {{age}} years old. Please craft a captivating tale that captivates their imagination and provides a delightful bedtime experience.",
      "${promptOriginKey}": "${PromptOrigin.codebase}"
      "${modelKey}": "claude-3-5-sonnet-latest"
      "${temperatureKey}": 0.7
    }
  }

  TOOL OUTPUT INSTRUCTIONS:
  - The tool will return...
    - a \`${templateKey}\` that reformulates the user's prompt into a more structured format.
    - a \`${contextSchemaKey}\` that defines the expected input parameters for the template.
    - a \`${promptOriginKey}\` that indicates whether the prompt comes from an existing prompt or prompt template in the user's codebase or from new requirements.
  - The tool output -- the \`${templateKey}\`, \`${contextSchemaKey}\`, and \`${promptOriginKey}\` -- will also be used as input to the \`${PromptWorkbenchToolName.recommend_prompt_template_tests}\` tool to generate a list of recommended tests that can be used to test the prompt template.
  `,
  inputSchema: createPromptTemplateInputSchema,
};

```

--------------------------------------------------------------------------------
/src/lib/usage-api/getUsageApiData.ts:
--------------------------------------------------------------------------------

```typescript
import { gunzipSync } from 'zlib';
import { getCircleCIClient } from '../../clients/client.js';
import mcpErrorOutput from '../../lib/mcpErrorOutput.js';
import fs from 'fs';
import path from 'path';
import os from 'os';

type CircleCIClient = ReturnType<typeof getCircleCIClient>;

function resolveOutputDir(outputDir: string): string {
  if (outputDir.startsWith('~')) {
    return path.join(os.homedir(), outputDir.slice(1));
  }
  if (outputDir.includes('%USERPROFILE%')) {
    const userProfile = process.env.USERPROFILE || os.homedir();
    return outputDir.replace('%USERPROFILE%', userProfile);
  }
  return outputDir;
}

export async function downloadAndSaveUsageData(
  downloadUrl: string,
  outputDir: string,
  opts: { startDate?: string; endDate?: string; jobId?: string }
) {
  try {
    const gzippedCsvResponse = await fetch(downloadUrl);
    if (!gzippedCsvResponse.ok) {
      const csvText = await gzippedCsvResponse.text();
      return mcpErrorOutput(`ERROR: Failed to download CSV.\nStatus: ${gzippedCsvResponse.status} ${gzippedCsvResponse.statusText}\nResponse: ${csvText}`);
    }
    const gzBuffer = Buffer.from(await gzippedCsvResponse.arrayBuffer());
    const csv = gunzipSync(gzBuffer);

    const fileName = (() => {
      if (opts.startDate && opts.endDate) {
        return `usage-data-${opts.startDate.slice(0, 10)}_${opts.endDate.slice(0, 10)}.csv`;
      }
      if (opts.jobId) {
        return `usage-data-job-${opts.jobId}.csv`;
      }
      return `usage-data-${Date.now()}.csv`;
    })();
    const usageDataDir = path.resolve(resolveOutputDir(outputDir));
    const filePath = path.join(usageDataDir, fileName);

    if (!fs.existsSync(usageDataDir)) {
      fs.mkdirSync(usageDataDir, { recursive: true });
    }
    fs.writeFileSync(filePath, csv);
    
    return { content: [{ type: 'text' as const, text: `Usage data CSV downloaded and saved to: ${filePath}` }] };
  } catch (e: any) {
    return mcpErrorOutput(`ERROR: Failed to download or save usage data.\nError: ${e?.stack || e}`);
  }
}

export async function handleExistingJob({ client, orgId, jobId, outputDir, startDate, endDate }: { client: CircleCIClient, orgId: string, jobId: string, outputDir: string, startDate?: string, endDate?: string }) {
  let jobStatus: any;
  try {
    jobStatus = await client.usage.getUsageExportJobStatus(orgId, jobId);
  } catch (e: any) {
    return mcpErrorOutput(`ERROR: Could not fetch job status for jobId ${jobId}.\n${e?.stack || e}`);
  }

    const state = jobStatus?.state?.toLowerCase();

  switch (state) {
    case 'completed': {
      const downloadUrls = jobStatus?.download_urls;
      const downloadUrl = Array.isArray(downloadUrls) && downloadUrls.length > 0 ? downloadUrls[0] : null;

      if (!downloadUrl) {
        return mcpErrorOutput(`ERROR: No download_url found in job status.\nJob status: ${JSON.stringify(jobStatus, null, 2)}`);
      }
      return await downloadAndSaveUsageData(downloadUrl, outputDir, { startDate, endDate, jobId });
    }
    case 'created':
    case 'pending':
    case 'processing':
      return {
        content: [
          { type: 'text' as const, text: `Usage export job is still processing. Please try again in a minute. (Job ID: ${jobId})` }
        ],
      };
    default:
      return mcpErrorOutput(`ERROR: Unknown job state: ${state}.\nJob status: ${JSON.stringify(jobStatus, null, 2)}`);
  }
}

export async function startNewUsageExportJob({ client, orgId, startDate, endDate }: { client: CircleCIClient, orgId: string, startDate: string, endDate: string }) {
  let createJson: any;
  try {
    createJson = await client.usage.startUsageExportJob(orgId, startDate, endDate);
  } catch (e: any) {
    return mcpErrorOutput(`ERROR: Failed to start usage export job.\n${e?.stack || e}`);
  }

  const newJobId = createJson?.usage_export_job_id;
  if (!newJobId) {
    return mcpErrorOutput(`ERROR: No usage export id returned.\nResponse: ${JSON.stringify(createJson)}`);
  }

  return {
    content: [
      { type: 'text' as const, text: `Started a new usage export job for your requested date range.\n\nTo check the status or download the file, say "check status".\n\nYou do NOT need to provide the job ID; the system will track it for you automatically.\n\nJob ID: ${newJobId}` }
    ],
    jobId: newJobId
  };
}

export async function getUsageApiData({ orgId, startDate, endDate, jobId, outputDir }: { orgId: string, startDate?: string, endDate?: string, jobId?: string, outputDir: string }) {
  if (!outputDir) {
    return mcpErrorOutput('ERROR: outputDir is required. Please specify a directory to save the usage data CSV.');
  }
  const client = getCircleCIClient();

  if (jobId) {
    return await handleExistingJob({ client, orgId, jobId, outputDir, startDate, endDate });
  } else {
    if (!startDate || !endDate) {
      return mcpErrorOutput('ERROR: startDate and endDate are required when starting a new usage export job.');
    }
    return await startNewUsageExportJob({ client, orgId, startDate, endDate });
  }
} 
```

--------------------------------------------------------------------------------
/src/tools/getFlakyTests/handler.ts:
--------------------------------------------------------------------------------

```typescript
import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
import {
  getProjectSlugFromURL,
  identifyProjectSlug,
} from '../../lib/project-detection/index.js';
import { getFlakyTestLogsInputSchema } from './inputSchema.js';
import getFlakyTests, {
  formatFlakyTests,
} from '../../lib/flaky-tests/getFlakyTests.js';
import mcpErrorOutput from '../../lib/mcpErrorOutput.js';
import { writeFileSync, mkdirSync, rmSync } from 'fs';
import { join } from 'path';
import { Test } from '../../clients/schemas.js';

export const getFlakyTestsOutputDirectory = () =>
  `${process.env.FILE_OUTPUT_DIRECTORY}/flaky-tests-output`;

export const getFlakyTestLogs: ToolCallback<{
  params: typeof getFlakyTestLogsInputSchema;
}> = async (args) => {
  const {
    workspaceRoot,
    gitRemoteURL,
    projectURL,
    projectSlug: inputProjectSlug,
  } = args.params ?? {};

  let projectSlug: string | null | undefined;

  if (inputProjectSlug) {
    projectSlug = inputProjectSlug;
  } else if (projectURL) {
    projectSlug = getProjectSlugFromURL(projectURL);
  } else if (workspaceRoot && gitRemoteURL) {
    projectSlug = await identifyProjectSlug({
      gitRemoteURL,
    });
  } else {
    return mcpErrorOutput(
      'Missing required inputs. Please provide either: 1) projectSlug, 2) projectURL, or 3) workspaceRoot with gitRemoteURL.',
    );
  }

  if (!projectSlug) {
    return mcpErrorOutput(`
          Project not found. Ask the user to provide the inputs user can provide based on the tool description.

          Project slug: ${projectSlug}
          Git remote URL: ${gitRemoteURL}
          `);
  }

  const tests = await getFlakyTests({
    projectSlug,
  });

  if (process.env.FILE_OUTPUT_DIRECTORY) {
    try {
      return await writeTestsToFiles({ tests });
    } catch (error) {
      console.error(error);
      return formatFlakyTests(tests);
    }
  }

  return formatFlakyTests(tests);
};

const generateSafeFilename = ({
  test,
  index,
}: {
  test: Test;
  index: number;
}): string => {
  const safeTestName = (test.name || 'unnamed-test')
    .replace(/[^a-zA-Z0-9\-_]/g, '_')
    .substring(0, 50); // Limit length

  return `flaky-test-${index + 1}-${safeTestName}.txt`;
};

/**
 * Write test data to a file
 */
const writeTestToFile = ({
  test,
  filePath,
  index,
}: {
  test: Test;
  filePath: string;
  index: number;
}): void => {
  const testContent = [
    `Flaky Test #${index + 1}`,
    '='.repeat(50),
    test.file && `File Name: ${test.file}`,
    test.classname && `Classname: ${test.classname}`,
    test.name && `Test name: ${test.name}`,
    test.result && `Result: ${test.result}`,
    test.run_time && `Run time: ${test.run_time}`,
    test.message && `Message: ${test.message}`,
    '',
    'Raw Test Data:',
    '-'.repeat(20),
    JSON.stringify(test, null, 2),
  ]
    .filter(Boolean)
    .join('\n');

  writeFileSync(filePath, testContent, 'utf8');
};

/**
 * Write flaky tests to individual files
 * @param params Configuration parameters
 * @param params.tests Array of test objects to write to files
 * @returns Response object with success message or error
 */
const writeTestsToFiles = async ({
  tests,
}: {
  tests: Test[];
}): Promise<{
  content: {
    type: 'text';
    text: string;
  }[];
}> => {
  if (tests.length === 0) {
    return {
      content: [
        {
          type: 'text' as const,
          text: 'No flaky tests found - no files created',
        },
      ],
    };
  }

  const flakyTestsOutputDirectory = getFlakyTestsOutputDirectory();

  try {
    rmSync(flakyTestsOutputDirectory, { recursive: true, force: true });
    mkdirSync(flakyTestsOutputDirectory, { recursive: true });

    // Create .gitignore to ignore all files in this directory
    const gitignorePath = join(flakyTestsOutputDirectory, '.gitignore');
    const gitignoreContent = '# Ignore all flaky test output files\n*\n';
    writeFileSync(gitignorePath, gitignoreContent, 'utf8');
  } catch (error) {
    throw new Error(
      `Failed to create output directory: ${error instanceof Error ? error.message : String(error)}`,
    );
  }

  const filePaths: string[] = [];

  try {
    tests.forEach((test, index) => {
      const filename = generateSafeFilename({ test, index });
      const filePath = join(flakyTestsOutputDirectory, filename);

      writeTestToFile({ test, filePath, index });
      filePaths.push(filePath);
    });

    return {
      content: [
        {
          type: 'text' as const,
          text: `Found ${tests.length} flaky tests that need stabilization. Each file contains test failure data and metadata - analyze these reports to understand what's causing the flakiness, then locate and fix the actual test code.\n\nFlaky test reports:\n${filePaths.map((path) => `- ${path}`).join('\n')}\n\nFiles are located in: ${flakyTestsOutputDirectory}`,
        },
      ],
    };
  } catch (error) {
    return mcpErrorOutput(
      `Failed to write flaky test files: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
};

```

--------------------------------------------------------------------------------
/src/tools/runRollbackPipeline/tool.ts:
--------------------------------------------------------------------------------

```typescript
import { runRollbackPipelineInputSchema } from './inputSchema.js';

export const runRollbackPipelineTool = {
  name: 'run_rollback_pipeline' as const,
  description: `
    Run a rollback pipeline for a CircleCI project. This tool guides you through the full rollback process, adapting to the information you provide and prompting for any missing details.

    **Initial Requirements:**
    - You need either a \`projectSlug\` (from \`listFollowedProjects\`) or a \`projectID\`. The tool will automatically resolve the project information from either of these.

    **Typical Flow:**
    1. **Start:** User initiates a rollback request.
    2. **Project Selection:** If project id or project slug are not provided, call \`listFollowedProjects\` to get the list of projects the user follows and present the full list of projects to the user so that they can select the project they want to rollback.
    3. **Project Information:** Provide either \`projectSlug\` or \`projectID\`. The tool will automatically resolve the project information as needed.
    4. **Version Selection:** If component environment and version are not provided, call \`listComponentVersions\` to get the list of versions for the selected component and environment. If there is only one version, proceed automatically and do not ask the user to select a version. Otherwise, present the user with the full list of versions and ask them to select one. Always return all available values without categorizing them.
    5. **Rollback Reason** ask the user for an optional reason for the rollback (e.g., "Critical bug fix"). Skip this step is the user explicitly requests a rollback by workflow rerun.
    6. **Rollback pipeline check** if the tool reports that no rollback pipeline is defined, ask the user if they want to trigger a rollback by workflow rerun or suggest to setup a rollback pipeline following the documentation at https://circleci.com/docs/deploy/rollback-a-project-using-the-rollback-pipeline/.
    7. **Confirmation:** Summarize the rollback request and confirm with the user before submitting.
    8. **Pipeline Rollback:**  if the user requested a rollback by pipeline, call \`runRollbackPipeline\` passing all parameters including the namespace associated with the version to the tool.
    9. **Workflow Rerun** If the user requested a rollback by workflow rerun, call \`rerunWorkflow\` passing the workflow ID of the selected version to the tool.
    10.**Completion:** Report the outcome of the operation.

    **Parameters:**
    - \`projectSlug\` (optional): The project slug from \`listFollowedProjects\` (e.g., "gh/organization/project"). Either this or \`projectID\` must be provided.
    - \`projectID\` (optional): The CircleCI project ID (UUID). Either this or \`projectSlug\` must be provided.
    - \`environmentName\` (required): The target environment (e.g., "production", "staging").
    - \`componentName\` (required): The component to rollback (e.g., "frontend", "backend").
    - \`currentVersion\` (required): The currently deployed version.
    - \`targetVersion\` (required): The version to rollback to.
    - \`namespace\` (required): The namespace of the component.
    - \`reason\` (optional): Reason for the rollback.
    - \`parameters\` (optional): Additional rollback parameters as key-value pairs.

    **Behavior:**
    - If there are more than 20 environments or components, ask the user to refine their selection.
    - Never attempt to guess or construct project slugs or URLs; always use values provided by the user or from \`listFollowedProjects\`.
    - Do not prompt for missing parameters until versions have been listed.
    - Do not call this tool with incomplete parameters.
    - If the selected project lacks rollback pipeline configuration, provide a definitive error message without suggesting alternative projects.

    **Returns:**
    - On success: The rollback ID or a confirmation in case of workflow rerun.
    - On error: A clear message describing what is missing or what went wrong.
    - If the selected project does not have a rollback pipeline configured: The tool will provide a clear error message specific to that project and will NOT suggest trying another project.

    **Important Note:**
    - This tool is designed to work only with the specific project provided by the user.
    - If a project does not have rollback capability configured, the tool will NOT recommend trying other projects.
    - The assistant should NOT suggest trying different projects when a project lacks rollback configuration.
    - Each project must have its own rollback pipeline configuration to be eligible for rollback operations.
    - When a project cannot be rolled back, provide only the configuration guidance for THAT specific project.
    - The tool automatically resolves project information from either \`projectSlug\` or \`projectID\`.
    If no version is found, the tool will suggest the user to set up deploy markers following the documentation at:
    https://circleci.com/docs/deploy/configure-deploy-markers/
  `,
  inputSchema: runRollbackPipelineInputSchema,
};

```

--------------------------------------------------------------------------------
/src/tools/getBuildFailureLogs/handler.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { getBuildFailureLogs } from './handler.js';
import * as projectDetection from '../../lib/project-detection/index.js';
import * as getPipelineJobLogsModule from '../../lib/pipeline-job-logs/getPipelineJobLogs.js';
import * as formatJobLogs from '../../lib/pipeline-job-logs/getJobLogs.js';

// Mock dependencies
vi.mock('../../lib/project-detection/index.js');
vi.mock('../../lib/pipeline-job-logs/getPipelineJobLogs.js');
vi.mock('../../lib/pipeline-job-logs/getJobLogs.js');

describe('getBuildFailureLogs handler', () => {
  beforeEach(() => {
    vi.resetAllMocks();
  });

  it('should return a valid MCP error response when no inputs are provided', async () => {
    const args = {
      params: {},
    } as any;

    const controller = new AbortController();
    const response = await getBuildFailureLogs(args, {
      signal: controller.signal,
    });

    expect(response).toHaveProperty('content');
    expect(response).toHaveProperty('isError', true);
    expect(Array.isArray(response.content)).toBe(true);
    expect(response.content[0]).toHaveProperty('type', 'text');
    expect(typeof response.content[0].text).toBe('string');
  });

  it('should return a valid MCP error response when project is not found', async () => {
    vi.spyOn(projectDetection, 'identifyProjectSlug').mockResolvedValue(
      undefined,
    );

    const args = {
      params: {
        workspaceRoot: '/workspace',
        gitRemoteURL: 'https://github.com/org/repo.git',
        branch: 'main',
      },
    } as any;

    const controller = new AbortController();
    const response = await getBuildFailureLogs(args, {
      signal: controller.signal,
    });

    expect(response).toHaveProperty('content');
    expect(response).toHaveProperty('isError', true);
    expect(Array.isArray(response.content)).toBe(true);
    expect(response.content[0]).toHaveProperty('type', 'text');
    expect(typeof response.content[0].text).toBe('string');
  });

  it('should return a valid MCP error response when projectSlug is provided without branch', async () => {
    const args = {
      params: {
        projectSlug: 'gh/org/repo',
      },
    } as any;

    const controller = new AbortController();
    const response = await getBuildFailureLogs(args, {
      signal: controller.signal,
    });

    expect(response).toHaveProperty('content');
    expect(response).toHaveProperty('isError', true);
    expect(Array.isArray(response.content)).toBe(true);
    expect(response.content[0]).toHaveProperty('type', 'text');
    expect(typeof response.content[0].text).toBe('string');
    expect(response.content[0].text).toContain('Branch not provided');
  });

  it('should return a valid MCP success response with logs', async () => {
    vi.spyOn(projectDetection, 'getProjectSlugFromURL').mockReturnValue(
      'gh/org/repo',
    );

    vi.spyOn(getPipelineJobLogsModule, 'default').mockResolvedValue([
      {
        jobName: 'test',
        steps: [
          {
            stepName: 'Run tests',
            logs: { output: 'Test failed', error: '' },
          },
        ],
      },
    ]);

    vi.spyOn(formatJobLogs, 'formatJobLogs').mockReturnValue({
      content: [
        {
          type: 'text',
          text: 'Job logs output',
        },
      ],
    });

    const args = {
      params: {
        projectURL: 'https://app.circleci.com/pipelines/gh/org/repo',
      },
    } as any;

    const controller = new AbortController();
    const response = await getBuildFailureLogs(args, {
      signal: controller.signal,
    });

    expect(response).toHaveProperty('content');
    expect(Array.isArray(response.content)).toBe(true);
    expect(response.content[0]).toHaveProperty('type', 'text');
    expect(typeof response.content[0].text).toBe('string');
  });

  it('should handle projectSlug and branch inputs correctly', async () => {
    const mockLogs = [
      {
        jobName: 'build',
        steps: [
          {
            stepName: 'Build app',
            logs: { output: 'Build failed', error: 'Error: build failed' },
          },
        ],
      },
    ];

    vi.spyOn(getPipelineJobLogsModule, 'default').mockResolvedValue(mockLogs);

    vi.spyOn(formatJobLogs, 'formatJobLogs').mockReturnValue({
      content: [
        {
          type: 'text',
          text: 'Formatted job logs',
        },
      ],
    });

    const args = {
      params: {
        projectSlug: 'gh/org/repo',
        branch: 'feature/new-feature',
      },
    } as any;

    const controller = new AbortController();
    const response = await getBuildFailureLogs(args, {
      signal: controller.signal,
    });

    expect(getPipelineJobLogsModule.default).toHaveBeenCalledWith({
      projectSlug: 'gh/org/repo',
      branch: 'feature/new-feature',
      pipelineNumber: undefined,
      jobNumber: undefined,
    });

    expect(formatJobLogs.formatJobLogs).toHaveBeenCalledWith(mockLogs);
    expect(response).toHaveProperty('content');
    expect(Array.isArray(response.content)).toBe(true);
    expect(response.content[0]).toHaveProperty('type', 'text');
    expect(typeof response.content[0].text).toBe('string');
  });
});

```

--------------------------------------------------------------------------------
/src/tools/listComponentVersions/handler.ts:
--------------------------------------------------------------------------------

```typescript
import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
import { listComponentVersionsInputSchema } from './inputSchema.js';
import { getCircleCIClient } from '../../clients/client.js';
import mcpErrorOutput from '../../lib/mcpErrorOutput.js';

type ProjectInfo = {
  projectID: string;
  orgID: string;
};


export const listComponentVersions: ToolCallback<{
  params: typeof listComponentVersionsInputSchema;
}> = async (args) => {
  const {
    projectSlug,
    projectID: providedProjectID,
    orgID: providedOrgID,
    componentID,
    environmentID,
  } = args.params ?? {};

  try {
    // Resolve project and organization information
    const projectInfoResult = await resolveProjectInfo(projectSlug, providedProjectID, providedOrgID);
    
    if (!projectInfoResult.success) {
      return projectInfoResult.error;
    }

    const { projectID, orgID } = projectInfoResult.data;

    // If environmentID is not provided, list environments
    if (!environmentID) {
      return await listEnvironments(orgID);
    }

    // If componentID is not provided, list components
    if (!componentID) {
      return await listComponents(projectID, orgID);
    }

    // If both componentID and environmentID are provided, list component versions
    return await fetchComponentVersions(componentID, environmentID);

  } catch (error) {
    return mcpErrorOutput(
      `Failed to list component versions: ${error instanceof Error ? error.message : 'Unknown error'}`,
    );
  }
};


/**
 * Resolves project and organization information from the provided parameters
 */
async function resolveProjectInfo(
  projectSlug?: string,
  providedProjectID?: string,
  providedOrgID?: string,
): Promise<{ success: true; data: ProjectInfo } | { success: false; error: any }> {
  const circleci = getCircleCIClient();

  try {
    if (providedProjectID && providedOrgID) {
      // Both IDs provided, use them directly
      return {
        success: true,
        data: {
          projectID: providedProjectID,
          orgID: providedOrgID,
        },
      };
    }

    if (projectSlug) {
      // Use projectSlug to get projectID and orgID
      const { id: resolvedProjectId, organization_id: resolvedOrgId } = await circleci.projects.getProject({
        projectSlug,
      });
      return {
        success: true,
        data: {
          projectID: resolvedProjectId,
          orgID: resolvedOrgId,
        },
      };
    }

    if (providedProjectID) {
      // Use projectID to get orgID
      const { id: resolvedProjectId, organization_id: resolvedOrgId } = await circleci.projects.getProjectByID({
        projectID: providedProjectID,
      });
      return {
        success: true,
        data: {
          projectID: resolvedProjectId,
          orgID: resolvedOrgId,
        },
      };
    }

    return {
      success: false,
      error: mcpErrorOutput(`Invalid request. Please specify either a project slug or a project ID.`),
    };
  } catch (error) {
    const errorMessage = projectSlug
      ? `Failed to resolve project information for ${projectSlug}. Please verify the project slug is correct.`
      : `Failed to resolve project information for project ID ${providedProjectID}. Please verify the project ID is correct.`;

    return {
      success: false,
      error: mcpErrorOutput(`${errorMessage} ${error instanceof Error ? error.message : 'Unknown error'}`),
    };
  }
}

/**
 * Lists available environments for the organization
 */
async function listEnvironments(orgID: string) {
  const circleci = getCircleCIClient();

  const environments = await circleci.deploys.fetchEnvironments({
    orgID,
  });

  if (environments.items.length === 0) {
    return {
      content: [
        {
          type: 'text',
          text: `No environments found`,
        },
      ],
    };
  }

  const environmentsList = environments.items
    .map((env: any, index: number) => `${index + 1}. ${env.name} (ID: ${env.id})`)
    .join('\n');

  return {
    content: [
      {
        type: 'text',
        text: `Please provide an environmentID. Available environments:\n\n${environmentsList}\n\n`,
      },
    ],
  };
}

/**
 * Lists available components for the project
 */
async function listComponents(projectID: string, orgID: string) {
  const circleci = getCircleCIClient();

  const components = await circleci.deploys.fetchProjectComponents({
    projectID,
    orgID,
  });

  if (components.items.length === 0) {
    return {
      content: [
        {
          type: 'text',
          text: `No components found`,
        },
      ],
    };
  }

  const componentsList = components.items
    .map((component: any, index: number) => `${index + 1}. ${component.name} (ID: ${component.id})`)
    .join('\n');

  return {
    content: [
      {
        type: 'text',
        text: `Please provide a componentID. Available components:\n\n${componentsList}\n\n`,
      },
    ],
  };
}

/**
 * Lists component versions for the specified component and environment
 */
async function fetchComponentVersions(componentID: string, environmentID: string) {
  const circleci = getCircleCIClient();

  const componentVersions = await circleci.deploys.fetchComponentVersions({
    componentID,
    environmentID,
  });

  if (componentVersions.items.length === 0) {
    return {
      content: [
        {
          type: 'text',
          text: `No component versions found`,
        },
      ],
    };
  }

  return {
    content: [
      {
        type: 'text',
        text: `Versions for the component: ${JSON.stringify(componentVersions)}`,
      },
    ],
  };
}

```

--------------------------------------------------------------------------------
/src/tools/recommendPromptTemplateTests/handler.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { recommendPromptTemplateTests } from './handler.js';
import { CircletClient } from '../../clients/circlet/index.js';
import {
  defaultModel,
  PromptOrigin,
  promptsOutputDirectory,
  fileNameTemplate,
  fileNameExample1,
  fileNameExample2,
  fileNameExample3,
} from '../shared/constants.js';

// Mock dependencies
vi.mock('../../clients/circlet/index.js');

describe('recommendPromptTemplateTests handler', () => {
  beforeEach(() => {
    vi.resetAllMocks();
  });

  it('should return a valid MCP response with recommended tests for requirements-based prompt', async () => {
    const mockRecommendedTests = [
      'Test with variable = "value1"',
      'Test with variable = "value2"',
      'Test with empty variable',
    ];

    const mockRecommendPromptTemplateTests = vi
      .fn()
      .mockResolvedValue(mockRecommendedTests);

    const mockCircletInstance = {
      circlet: {
        recommendPromptTemplateTests: mockRecommendPromptTemplateTests,
      },
    };

    vi.mocked(CircletClient).mockImplementation(
      () => mockCircletInstance as any,
    );

    const template = 'This is a test template with {{variable}}';
    const contextSchema = {
      variable: 'Description of the variable',
    };

    const args = {
      params: {
        template,
        contextSchema,
        promptOrigin: PromptOrigin.requirements,
        model: defaultModel,
      },
    };

    const controller = new AbortController();
    const response = await recommendPromptTemplateTests(args, {
      signal: controller.signal,
    });

    expect(mockRecommendPromptTemplateTests).toHaveBeenCalledWith({
      template,
      contextSchema,
    });

    expect(response).toHaveProperty('content');
    expect(Array.isArray(response.content)).toBe(true);
    expect(response.content[0]).toHaveProperty('type', 'text');
    expect(typeof response.content[0].text).toBe('string');

    const responseText = response.content[0].text;

    // Verify recommended tests are included
    expect(responseText).toContain('recommendedTests:');
    expect(responseText).toContain(
      JSON.stringify(mockRecommendedTests, null, 2),
    );

    // Verify next steps and file saving instructions
    expect(responseText).toContain('NEXT STEP:');
    expect(responseText).toContain(
      'save the `promptTemplate`, `contextSchema`, and `recommendedTests`',
    );

    // Verify file saving rules
    expect(responseText).toContain('RULES FOR SAVING FILES:');
    expect(responseText).toContain(promptsOutputDirectory);
    expect(responseText).toContain(fileNameTemplate);
    expect(responseText).toContain(fileNameExample1);
    expect(responseText).toContain(fileNameExample2);
    expect(responseText).toContain(fileNameExample3);
    expect(responseText).toContain('`name`: string');
    expect(responseText).toContain('`description`: string');
    expect(responseText).toContain('`version`: string');
    expect(responseText).toContain('`promptOrigin`: string');
    expect(responseText).toContain('`model`: string');
    expect(responseText).toContain('`template`: multi-line string');
    expect(responseText).toContain('`contextSchema`: object');
    expect(responseText).toContain('`tests`: array of objects');
    expect(responseText).toContain('`sampleInputs`: object[]');

    // Should not contain integration instructions for requirements-based prompts
    expect(responseText).not.toContain(
      'FINALLY, ONCE ALL THE FILES ARE SAVED:',
    );
  });

  it('should include integration instructions for codebase-based prompts', async () => {
    const mockRecommendedTests = ['Test case 1'];
    const mockRecommendPromptTemplateTests = vi
      .fn()
      .mockResolvedValue(mockRecommendedTests);

    const mockCircletInstance = {
      circlet: {
        recommendPromptTemplateTests: mockRecommendPromptTemplateTests,
      },
    };

    vi.mocked(CircletClient).mockImplementation(
      () => mockCircletInstance as any,
    );

    const args = {
      params: {
        template: 'Test template',
        contextSchema: { variable: 'description' },
        promptOrigin: PromptOrigin.codebase,
        model: defaultModel,
      },
    };

    const controller = new AbortController();
    const response = await recommendPromptTemplateTests(args, {
      signal: controller.signal,
    });

    const responseText = response.content[0].text;
    expect(responseText).toContain('FINALLY, ONCE ALL THE FILES ARE SAVED:');
    expect(responseText).toContain('1. Ask user if they want to integrate');
    expect(responseText).toContain('(Yes/No)');
    expect(responseText).toContain(
      `2. If yes, import the \`${promptsOutputDirectory}\` files into their app, following codebase conventions`,
    );
    expect(responseText).toContain('3. Only use existing dependencies');
  });

  it('should handle errors from CircletClient', async () => {
    const mockCircletInstance = {
      circlet: {
        recommendPromptTemplateTests: vi
          .fn()
          .mockRejectedValue(new Error('API error')),
      },
    };

    vi.mocked(CircletClient).mockImplementation(
      () => mockCircletInstance as any,
    );

    const args = {
      params: {
        template: 'Test template',
        contextSchema: { variable: 'description' },
        promptOrigin: PromptOrigin.requirements,
        model: defaultModel,
      },
    };

    const controller = new AbortController();

    await expect(
      recommendPromptTemplateTests(args, { signal: controller.signal }),
    ).rejects.toThrow('API error');
  });
});

```

--------------------------------------------------------------------------------
/src/lib/usage-api/findUnderusedResourceClasses.ts:
--------------------------------------------------------------------------------

```typescript
import fs from 'fs';
import path from 'path';
import { parse } from 'csv-parse/sync';

function normalizeHeader(header: string): string {
  return header.trim().toLowerCase().replace(/\s+/g, '_');
}

export function readAndParseCSV(csvFilePath: string): any[] {
  if (!csvFilePath) {
    throw new Error('csvFilePath is required');
  }
  let csvContent: string;
  try {
    csvContent = fs.readFileSync(path.resolve(csvFilePath), 'utf8');
  } catch (e: any) {
    throw new Error(`Could not read CSV file at ${csvFilePath}.\n${e?.stack || e}`);
  }
  try {
    return parse(csvContent, {
      columns: (headers: string[]) => headers.map(normalizeHeader),
      skip_empty_lines: true,
      relax_column_count: true,
      skip_records_with_error: true,
    });
  } catch (e: any) {
    throw new Error(`Failed to parse CSV.\n${e?.stack || e}`);
  }
}

export function validateCSVColumns(records: any[]): void {
  const requiredCols = [
    'project_name',
    'workflow_name',
    'job_name',
    'resource_class',
    'median_cpu_utilization_pct',
    'max_cpu_utilization_pct',
    'median_ram_utilization_pct',
    'max_ram_utilization_pct',
  ];
  const first = records[0];
  if (!first || !requiredCols.every((col) => col in first)) {
    throw new Error('CSV is missing required columns. Required: project_name, workflow_name, job_name, resource_class, median_cpu_utilization_pct, max_cpu_utilization_pct, median_ram_utilization_pct, max_ram_utilization_pct');
  }
}

export function groupRecordsByJob(records: any[]): Map<string, any[]> {
  const groupMap = new Map<string, any[]>();
  for (const row of records) {
    const key = [row.project_name, row.workflow_name, row.job_name, row.resource_class].join('|||');
    if (!groupMap.has(key)) {
      groupMap.set(key, []);
    }
    groupMap.get(key)?.push(row);
  }
  return groupMap;
}

const avg = (arr: number[]) => arr.length > 0 ? arr.reduce((a, b) => a + b, 0) / arr.length : 0;
const sum = (arr: number[]) => arr.reduce((a, b) => a + b, 0);

function calculateAverages(group: any[]): { avgCpu: number; maxCpu: number; avgRam: number; maxRam: number; totalComputeCredits: number, hasData: boolean } {

    const medianCpuArr = group.map((r: any) => parseFloat(r.median_cpu_utilization_pct)).filter(isFinite);
    const maxCpuArr = group.map((r: any) => parseFloat(r.max_cpu_utilization_pct)).filter(isFinite);
    const medianRamArr = group.map((r: any) => parseFloat(r.median_ram_utilization_pct)).filter(isFinite);
    const maxRamArr = group.map((r: any) => parseFloat(r.max_ram_utilization_pct)).filter(isFinite);
    const computeCreditsArr = group.map((r: any) => parseFloat(r.compute_credits)).filter(isFinite);

    if (!medianCpuArr.length || !maxCpuArr.length || !medianRamArr.length || !maxRamArr.length) {
        return { avgCpu: 0, maxCpu: 0, avgRam: 0, maxRam: 0, totalComputeCredits: 0, hasData: false };
    }

    return {
        avgCpu: avg(medianCpuArr),
        maxCpu: avg(maxCpuArr),
        avgRam: avg(medianRamArr),
        maxRam: avg(maxRamArr),
        totalComputeCredits: sum(computeCreditsArr),
        hasData: true
    };
}

export function analyzeJobGroups(groupedRecords: Map<string, any[]>, threshold: number): any[] {
  const underused: any[] = [];
  for (const [key, group] of groupedRecords.entries()) {
    const [projectName, workflowName, jobName, resourceClass] = key.split('|||');
    
    const { avgCpu, maxCpu, avgRam, maxRam, totalComputeCredits, hasData } = calculateAverages(group);

    if(!hasData) continue;

    if (
      avgCpu < threshold &&
      maxCpu < threshold &&
      avgRam < threshold &&
      maxRam < threshold
    ) {
      underused.push({
        projectName,
        workflowName,
        job: jobName,
        resourceClass,
        avgCpu: +avgCpu.toFixed(2),
        maxCpu: +maxCpu.toFixed(2),
        avgRam: +avgRam.toFixed(2),
        maxRam: +maxRam.toFixed(2),
        count: group.length,
        totalComputeCredits: +totalComputeCredits.toFixed(2),
      });
    }
  }
  return underused;
}

export function generateReport(underusedJobs: any[], threshold: number): string {
  if (underusedJobs.length === 0) {
    return `No underused resource classes found (threshold: ${threshold}%).`;
  }

  let report = `Underused resource classes (threshold: ${threshold}%):\n\n`;
  const grouped: Record<string, Record<string, any[]>> = {};
  for (const u of underusedJobs) {
    if (!grouped[u.projectName]) grouped[u.projectName] = {};
    if (!grouped[u.projectName][u.workflowName]) grouped[u.projectName][u.workflowName] = [];
    grouped[u.projectName][u.workflowName].push(u);
  }

  for (const project of Object.keys(grouped).sort()) {
    report += `## Project: ${project}\n`;
    for (const workflow of Object.keys(grouped[project]).sort()) {
      report += `### Workflow: ${workflow}\n`;
      report += 'Job Name | Resource Class | #Runs | Total Compute Credits | Avg CPU% | Max CPU% | Avg RAM% | Max RAM%\n';
      report += '|--------|---------------|-------|----------------------|----------|----------|----------|----------|\n';
      const sortedJobs = grouped[project][workflow].sort((a,b) => a.job.localeCompare(b.job));
      for (const u of sortedJobs) {
        report += `${u.job} | ${u.resourceClass} | ${u.count} | ${u.totalComputeCredits} | ${u.avgCpu} | ${u.maxCpu} | ${u.avgRam} | ${u.maxRam}\n`;
      }
      report += '\n';
    }
    report += '\n';
  }
  return report;
}

export async function findUnderusedResourceClassesFromCSV({ csvFilePath, threshold = 40 }: { csvFilePath: string, threshold?: number }) {
  const records = readAndParseCSV(csvFilePath);
  validateCSVColumns(records);
  const groupedRecords = groupRecordsByJob(records);
  const underusedJobs = analyzeJobGroups(groupedRecords, threshold);
  const report = generateReport(underusedJobs, threshold);
  
  return { report, underused: underusedJobs };
}

```

--------------------------------------------------------------------------------
/src/lib/usage-api/getUsageApiData.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import fs from 'fs';
import { gzipSync } from 'zlib';
import * as clientModule from '../../clients/client.js';
import {
  downloadAndSaveUsageData,
  handleExistingJob,
  startNewUsageExportJob,
  getUsageApiData,
} from './getUsageApiData.js';

vi.mock('fs');
vi.mock('../../clients/client.js');

globalThis.fetch = vi.fn();

describe('Usage API Data Fetching', () => {
  const ORG_ID = 'test-org-id';
  const START_DATE = '2024-08-01T00:00:00Z';
  const END_DATE = '2024-08-31T23:59:59Z';
  const JOB_ID = 'test-job-id';
  const DOWNLOAD_URL = 'https://fake-url.com/usage.csv.gz';
  const OUTPUT_DIR = '/tmp/usage-data';
  const MOCK_CSV_CONTENT = 'col1,col2\nval1,val2';
  const MOCK_GZIPPED_CSV = gzipSync(Buffer.from(MOCK_CSV_CONTENT));

  let mockCircleCIClient: any;
  let startUsageExportJobMock: any;
  let getUsageExportJobStatusMock: any;

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

    startUsageExportJobMock = vi.fn().mockResolvedValue({ usage_export_job_id: JOB_ID });
    getUsageExportJobStatusMock = vi.fn();

    mockCircleCIClient = {
      usage: {
        startUsageExportJob: startUsageExportJobMock,
        getUsageExportJobStatus: getUsageExportJobStatusMock,
      },
    };

    (clientModule.getCircleCIClient as any).mockReturnValue(mockCircleCIClient);
    (fetch as any).mockReset();
    (fs.existsSync as any).mockReturnValue(true);
  });

  describe('downloadAndSaveUsageData', () => {
    it('should download, decompress, and save the CSV file correctly', async () => {
      (fetch as any).mockResolvedValue({
        ok: true,
        arrayBuffer: () => Promise.resolve(MOCK_GZIPPED_CSV),
      });

      const result = await downloadAndSaveUsageData(DOWNLOAD_URL, OUTPUT_DIR, { startDate: START_DATE, endDate: END_DATE });

      expect(fetch).toHaveBeenCalledWith(DOWNLOAD_URL);
      expect(fs.writeFileSync).toHaveBeenCalledWith(
        `${OUTPUT_DIR}/usage-data-2024-08-01_2024-08-31.csv`,
        Buffer.from(MOCK_CSV_CONTENT)
      );
      expect(result.content[0].text).toContain('Usage data CSV downloaded and saved to');
    });

    it('should create output directory if it does not exist', async () => {
      (fs.existsSync as any).mockReturnValue(false);
      (fetch as any).mockResolvedValue({
        ok: true,
        arrayBuffer: () => Promise.resolve(MOCK_GZIPPED_CSV),
      });

      await downloadAndSaveUsageData(DOWNLOAD_URL, OUTPUT_DIR, { startDate: START_DATE, endDate: END_DATE });

      expect(fs.mkdirSync).toHaveBeenCalledWith(OUTPUT_DIR, { recursive: true });
    });
    
    it('should handle fetch failure gracefully', async () => {
        (fetch as any).mockResolvedValue({
            ok: false,
            status: 500,
            statusText: 'Server Error',
            text: async () => 'Internal Server Error'
        });
        
        const result = await downloadAndSaveUsageData(DOWNLOAD_URL, OUTPUT_DIR, { startDate: START_DATE, endDate: END_DATE });
        expect(result.content[0].text).toContain('ERROR: Failed to download CSV');
    });
  });

  describe('handleExistingJob', () => {
    it('should return a "processing" message for pending jobs', async () => {
      getUsageExportJobStatusMock.mockResolvedValue({ state: 'processing' });
      const result = await handleExistingJob({ client: mockCircleCIClient, orgId: ORG_ID, jobId: JOB_ID, outputDir: OUTPUT_DIR, startDate: START_DATE, endDate: END_DATE });
      expect(result.content[0].text).toContain('still processing');
    });

    it('should return an error for a failed job status fetch', async () => {
      getUsageExportJobStatusMock.mockRejectedValue(new Error('API Error'));
      const result = await handleExistingJob({ client: mockCircleCIClient, orgId: ORG_ID, jobId: JOB_ID, outputDir: OUTPUT_DIR, startDate: START_DATE, endDate: END_DATE });
      expect(result.content[0].text).toContain('ERROR: Could not fetch job status');
    });

    it('should return an error for an unknown job state', async () => {
        getUsageExportJobStatusMock.mockResolvedValue({ state: 'exploded' });
        const result = await handleExistingJob({ client: mockCircleCIClient, orgId: ORG_ID, jobId: JOB_ID, outputDir: OUTPUT_DIR, startDate: START_DATE, endDate: END_DATE });
        expect(result.content[0].text).toContain('ERROR: Unknown job state: exploded');
    });
  });

  describe('startNewUsageExportJob', () => {
    it('should return a "started new job" message on success', async () => {
      const result = await startNewUsageExportJob({ client: mockCircleCIClient, orgId: ORG_ID, startDate: START_DATE, endDate: END_DATE });
      expect(startUsageExportJobMock).toHaveBeenCalledWith(ORG_ID, START_DATE, END_DATE);
      expect(result.content[0].text).toContain('Started a new usage export job');
      expect((result as any).jobId).toBe(JOB_ID);
    });

    it('should return an error if job creation fails', async () => {
      startUsageExportJobMock.mockRejectedValue(new Error('Creation Failed'));
      const result = await startNewUsageExportJob({ client: mockCircleCIClient, orgId: ORG_ID, startDate: START_DATE, endDate: END_DATE });
      expect(result.content[0].text).toContain('ERROR: Failed to start usage export job');
    });
  });

  describe('getUsageApiData (main dispatcher)', () => {
    it('should call handleExistingJob if a jobId is provided', async () => {
      getUsageExportJobStatusMock.mockResolvedValue({ state: 'pending' });
      await getUsageApiData({ orgId: ORG_ID, startDate: START_DATE, endDate: END_DATE, jobId: JOB_ID, outputDir: OUTPUT_DIR });
      expect(getUsageExportJobStatusMock).toHaveBeenCalledWith(ORG_ID, JOB_ID);
      expect(startUsageExportJobMock).not.toHaveBeenCalled();
    });

    it('should call startNewUsageExportJob if no jobId is provided', async () => {
      await getUsageApiData({ orgId: ORG_ID, startDate: START_DATE, endDate: END_DATE, outputDir: OUTPUT_DIR });
      expect(startUsageExportJobMock).toHaveBeenCalledWith(ORG_ID, START_DATE, END_DATE);
      expect(getUsageExportJobStatusMock).not.toHaveBeenCalled();
    });
  });
}); 
```

--------------------------------------------------------------------------------
/src/lib/project-detection/index.ts:
--------------------------------------------------------------------------------

```typescript
import { getCircleCIPrivateClient } from '../../clients/client.js';
import { getVCSFromHost, vcses } from './vcsTool.js';
import gitUrlParse from 'parse-github-url';

/**
 * Identify the project slug from the git remote URL
 * @param {string} gitRemoteURL - eg: https://github.com/organization/project.git
 * @returns {string} project slug - eg: gh/organization/project
 */
export const identifyProjectSlug = async ({
  gitRemoteURL,
}: {
  gitRemoteURL: string;
}) => {
  const cciPrivateClients = getCircleCIPrivateClient();

  const parsedGitURL = gitUrlParse(gitRemoteURL);
  if (!parsedGitURL?.host) {
    return undefined;
  }

  const vcs = getVCSFromHost(parsedGitURL.host);
  if (!vcs) {
    throw new Error(`VCS with host ${parsedGitURL.host} is not handled`);
  }

  const { projects: followedProjects } =
    await cciPrivateClients.me.getFollowedProjects();
  if (!followedProjects) {
    throw new Error('Failed to get followed projects');
  }

  const project = followedProjects.find(
    (followedProject) =>
      followedProject.name === parsedGitURL.name &&
      followedProject.vcs_type === vcs.name,
  );

  return project?.slug;
};

/**
 * Get the pipeline number from the URL
 * @param {string} url - CircleCI pipeline URL
 * @returns {number} The pipeline number
 * @example
 * // Standard pipeline URL
 * getPipelineNumberFromURL('https://app.circleci.com/pipelines/gh/organization/project/2/workflows/abc123de-f456-78gh-90ij-klmnopqrstuv')
 * // returns 2
 *
 * @example
 * // Pipeline URL with complex project path
 * getPipelineNumberFromURL('https://app.circleci.com/pipelines/circleci/GM1mbrQEWnNbzLKEnotDo4/5gh9pgQgohHwicwomY5nYQ/123/workflows/abc123de-f456-78gh-90ij-klmnopqrstuv')
 * // returns 123
 *
 * @example
 * // URL without pipelines segment. This is a legacy job URL format.
 * getPipelineNumberFromURL('https://circleci.com/gh/organization/project/2')
 * // returns undefined
 */
export const getPipelineNumberFromURL = (url: string): number | undefined => {
  const parts = url.split('/');
  const pipelineIndex = parts.indexOf('pipelines');
  if (pipelineIndex === -1) {
    return undefined;
  }
  const pipelineNumber = parts[pipelineIndex + 4];

  if (!pipelineNumber) {
    return undefined;
  }
  const parsedNumber = Number(pipelineNumber);
  if (isNaN(parsedNumber)) {
    throw new Error('Pipeline number in URL is not a valid number');
  }
  return parsedNumber;
};

/**
 * Get the job number from the URL
 * @param {string} url - CircleCI job URL
 * @returns {number | undefined} The job number if present in the URL
 * @example
 * // Job URL
 * getJobNumberFromURL('https://app.circleci.com/pipelines/gh/organization/project/123/workflows/abc123de-f456-78gh-90ij-klmnopqrstuv/jobs/456')
 * // returns 456
 *
 * @example
 * // Legacy job URL format
 * getJobNumberFromURL('https://circleci.com/gh/organization/project/123')
 * // returns 123
 *
 * @example
 * // URL without job number
 * getJobNumberFromURL('https://app.circleci.com/pipelines/gh/organization/project/123/workflows/abc123de-f456-78gh-90ij-klmnopqrstuv')
 * // returns undefined
 */
export const getJobNumberFromURL = (url: string): number | undefined => {
  const parts = url.split('/');
  const jobsIndex = parts.indexOf('jobs');
  const pipelineIndex = parts.indexOf('pipelines');

  // Handle legacy URL format (e.g. https://circleci.com/gh/organization/project/123)
  if (jobsIndex === -1 && pipelineIndex === -1) {
    const jobNumber = parts[parts.length - 1];
    if (!jobNumber) {
      return undefined;
    }
    const parsedNumber = Number(jobNumber);
    if (isNaN(parsedNumber)) {
      throw new Error('Job number in URL is not a valid number');
    }
    return parsedNumber;
  }

  if (jobsIndex === -1) {
    return undefined;
  }

  // Handle modern URL format with /jobs/ segment
  if (jobsIndex + 1 >= parts.length) {
    return undefined;
  }

  const jobNumber = parts[jobsIndex + 1];
  if (!jobNumber) {
    return undefined;
  }

  const parsedNumber = Number(jobNumber);
  if (isNaN(parsedNumber)) {
    throw new Error('Job number in URL is not a valid number');
  }

  return parsedNumber;
};

/**
 * Get the project slug from the URL
 * @param {string} url - CircleCI pipeline or project URL
 * @returns {string} project slug - eg: gh/organization/project
 * @example
 * // Pipeline URL with workflow
 * getProjectSlugFromURL('https://app.circleci.com/pipelines/gh/organization/project/2/workflows/abc123de-f456-78gh-90ij-klmnopqrstuv')
 * // returns 'gh/organization/project'
 *
 * @example
 * // Simple project URL with query parameters
 * getProjectSlugFromURL('https://app.circleci.com/pipelines/gh/organization/project?branch=main')
 * // returns 'gh/organization/project'
 */
export const getProjectSlugFromURL = (url: string) => {
  const urlWithoutQuery = url.split('?')[0];
  const parts = urlWithoutQuery.split('/');

  let startIndex = -1;
  const pipelineIndex = parts.indexOf('pipelines');
  if (pipelineIndex !== -1) {
    startIndex = pipelineIndex + 1;
  } else {
    for (const vcs of vcses) {
      const shortIndex = parts.indexOf(vcs.short);
      const nameIndex = parts.indexOf(vcs.name);
      if (shortIndex !== -1) {
        startIndex = shortIndex;
        break;
      }
      if (nameIndex !== -1) {
        startIndex = nameIndex;
        break;
      }
    }
  }

  if (startIndex === -1) {
    throw new Error(
      'Error getting project slug from URL: Invalid CircleCI URL format',
    );
  }

  const [vcs, org, project] = parts.slice(
    startIndex,
    startIndex + 3, // vcs/org/project
  );
  if (!vcs || !org || !project) {
    throw new Error('Unable to extract project information from URL');
  }

  return `${vcs}/${org}/${project}`;
};

/**
 * Get the branch name from the URL's query parameters
 * @param {string} url - CircleCI pipeline URL
 * @returns {string | undefined} The branch name if present in the URL
 * @example
 * // URL with branch parameter
 * getBranchFromURL('https://app.circleci.com/pipelines/gh/organization/project?branch=feature-branch')
 * // returns 'feature-branch'
 *
 * @example
 * // URL without branch parameter
 * getBranchFromURL('https://app.circleci.com/pipelines/gh/organization/project')
 * // returns undefined
 */
export const getBranchFromURL = (url: string): string | undefined => {
  try {
    const urlObj = new URL(url);
    return urlObj.searchParams.get('branch') || undefined;
  } catch {
    throw new Error(
      'Error getting branch from URL: Invalid CircleCI URL format',
    );
  }
};

```

--------------------------------------------------------------------------------
/src/tools/runEvaluationTests/handler.ts:
--------------------------------------------------------------------------------

```typescript
import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
import { gzipSync } from 'zlib';
import {
  getBranchFromURL,
  getProjectSlugFromURL,
  identifyProjectSlug,
} from '../../lib/project-detection/index.js';
import { runEvaluationTestsInputSchema } from './inputSchema.js';
import mcpErrorOutput from '../../lib/mcpErrorOutput.js';
import { getCircleCIClient } from '../../clients/client.js';

export const runEvaluationTests: ToolCallback<{
  params: typeof runEvaluationTestsInputSchema;
}> = async (args) => {
  const {
    workspaceRoot,
    gitRemoteURL,
    branch,
    projectURL,
    pipelineChoiceName,
    projectSlug: inputProjectSlug,
    promptFiles,
  } = args.params ?? {};

  let projectSlug: string | undefined;
  let branchFromURL: string | undefined;

  if (inputProjectSlug) {
    if (!branch) {
      return mcpErrorOutput(
        'Branch not provided. When using projectSlug, a branch must also be specified.',
      );
    }
    projectSlug = inputProjectSlug;
  } else if (projectURL) {
    projectSlug = getProjectSlugFromURL(projectURL);
    branchFromURL = getBranchFromURL(projectURL);
  } else if (workspaceRoot && gitRemoteURL && branch) {
    projectSlug = await identifyProjectSlug({
      gitRemoteURL,
    });
  } else {
    return mcpErrorOutput(
      'Missing required inputs. Please provide either: 1) projectSlug with branch, 2) projectURL, or 3) workspaceRoot with gitRemoteURL and branch.',
    );
  }

  if (!projectSlug) {
    return mcpErrorOutput(`
          Project not found. Ask the user to provide the inputs user can provide based on the tool description.

          Project slug: ${projectSlug}
          Git remote URL: ${gitRemoteURL}
          Branch: ${branch}
          `);
  }
  const foundBranch = branchFromURL || branch;
  if (!foundBranch) {
    return mcpErrorOutput(
      'No branch provided. Try using the current git branch.',
    );
  }

  if (!promptFiles || promptFiles.length === 0) {
    return mcpErrorOutput(
      'No prompt template files provided. Please ensure you have prompt template files in the ./prompts directory (e.g. <relevant-name>.prompt.yml) and include them in the promptFiles parameter.',
    );
  }

  const circleci = getCircleCIClient();
  const { id: projectId } = await circleci.projects.getProject({
    projectSlug,
  });
  const pipelineDefinitions = await circleci.pipelines.getPipelineDefinitions({
    projectId,
  });

  const pipelineChoices = [
    ...pipelineDefinitions.map((definition) => ({
      name: definition.name,
      definitionId: definition.id,
    })),
  ];

  if (pipelineChoices.length === 0) {
    return mcpErrorOutput(
      'No pipeline definitions found. Please make sure your project is set up on CircleCI to run pipelines.',
    );
  }

  const formattedPipelineChoices = pipelineChoices
    .map(
      (pipeline, index) =>
        `${index + 1}. ${pipeline.name} (definitionId: ${pipeline.definitionId})`,
    )
    .join('\n');

  if (pipelineChoices.length > 1 && !pipelineChoiceName) {
    return {
      content: [
        {
          type: 'text',
          text: `Multiple pipeline definitions found. Please choose one of the following:\n${formattedPipelineChoices}`,
        },
      ],
    };
  }

  const chosenPipeline = pipelineChoiceName
    ? pipelineChoices.find((pipeline) => pipeline.name === pipelineChoiceName)
    : undefined;

  if (pipelineChoiceName && !chosenPipeline) {
    return mcpErrorOutput(
      `Pipeline definition with name ${pipelineChoiceName} not found. Please choose one of the following:\n${formattedPipelineChoices}`,
    );
  }

  const runPipelineDefinitionId =
    chosenPipeline?.definitionId || pipelineChoices[0].definitionId;

  // Process each file for compression and encoding
  const processedFiles = promptFiles.map((promptFile) => {
    const fileExtension = promptFile.fileName.toLowerCase();
    let processedPromptFileContent: string;

    if (fileExtension.endsWith('.json')) {
      // For JSON files, parse and re-stringify to ensure proper formatting
      const json = JSON.parse(promptFile.fileContent);
      processedPromptFileContent = JSON.stringify(json, null);
    } else if (
      fileExtension.endsWith('.yml') ||
      fileExtension.endsWith('.yaml')
    ) {
      // For YAML files, keep as-is
      processedPromptFileContent = promptFile.fileContent;
    } else {
      // Default to treating as text content
      processedPromptFileContent = promptFile.fileContent;
    }

    // Gzip compress the content and then base64 encode for compact transport
    const gzippedContent = gzipSync(processedPromptFileContent);
    const base64GzippedContent = gzippedContent.toString('base64');

    return {
      fileName: promptFile.fileName,
      base64GzippedContent,
    };
  });

  // Generate file creation commands with conditional logic for parallelism
  const fileCreationCommands = processedFiles
    .map(
      (file, index) =>
        `          if [ "$CIRCLE_NODE_INDEX" = "${index}" ]; then
            sudo mkdir -p /prompts
            echo "${file.base64GzippedContent}" | base64 -d | gzip -d | sudo tee /prompts/${file.fileName} > /dev/null
          fi`,
    )
    .join('\n');

  // Generate individual evaluation commands with conditional logic for parallelism
  const evaluationCommands = processedFiles
    .map(
      (file, index) =>
        `          if [ "$CIRCLE_NODE_INDEX" = "${index}" ]; then
            python eval.py ${file.fileName}
          fi`,
    )
    .join('\n');

  const configContent = `
version: 2.1

jobs:
  evaluate-prompt-template-tests:
    parallelism: ${processedFiles.length}
    docker:
      - image: cimg/python:3.12.0
    steps:
      - run: |
          curl https://gist.githubusercontent.com/jvincent42/10bf3d2d2899033ae1530cf429ed03f8/raw/acf07002d6bfcfb649c913b01a203af086c1f98d/eval.py > eval.py
          echo "deepeval>=3.0.3
          openai>=1.84.0
          anthropic>=0.54.0
          PyYAML>=6.0.2
          " > requirements.txt
          pip install -r requirements.txt
      - run: |
${fileCreationCommands}
      - run: |
${evaluationCommands}

workflows:
  mcp-run-evaluation-tests:
    jobs:
      - evaluate-prompt-template-tests
`;

  const runPipelineResponse = await circleci.pipelines.runPipeline({
    projectSlug,
    branch: foundBranch,
    definitionId: runPipelineDefinitionId,
    configContent,
  });

  return {
    content: [
      {
        type: 'text',
        text: `Pipeline run successfully. View it at: https://app.circleci.com/pipelines/${projectSlug}/${runPipelineResponse.number}`,
      },
    ],
  };
};

```

--------------------------------------------------------------------------------
/src/tools/getFlakyTests/handler.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { getFlakyTestLogs, getFlakyTestsOutputDirectory } from './handler.js';
import * as projectDetection from '../../lib/project-detection/index.js';
import * as getFlakyTestsModule from '../../lib/flaky-tests/getFlakyTests.js';
import * as formatFlakyTestsModule from '../../lib/flaky-tests/getFlakyTests.js';

// Mock dependencies
vi.mock('../../lib/project-detection/index.js');
vi.mock('../../lib/flaky-tests/getFlakyTests.js');

// Define mock functions using vi.hoisted() to make them available everywhere
const { mockWriteFileSync, mockMkdirSync, mockRmSync, mockJoin } = vi.hoisted(
  () => ({
    mockWriteFileSync: vi.fn(),
    mockMkdirSync: vi.fn(),
    mockRmSync: vi.fn(),
    mockJoin: vi.fn(),
  }),
);

vi.mock('fs', () => ({
  writeFileSync: mockWriteFileSync,
  mkdirSync: mockMkdirSync,
  rmSync: mockRmSync,
}));

vi.mock('path', () => ({
  join: mockJoin,
}));

describe('getFlakyTestLogs handler', () => {
  beforeEach(() => {
    vi.resetAllMocks();
    delete process.env.FILE_OUTPUT_DIRECTORY;
  });

  it('should return a valid MCP error response when no inputs are provided', async () => {
    const args = {
      params: {},
    };

    const controller = new AbortController();
    const response = await getFlakyTestLogs(args, {
      signal: controller.signal,
    });

    expect(response).toHaveProperty('content');
    expect(response).toHaveProperty('isError', true);
    expect(Array.isArray(response.content)).toBe(true);
    expect(response.content[0]).toHaveProperty('type', 'text');
    expect(typeof response.content[0].text).toBe('string');
  });

  it('should return a valid MCP error response when project is not found', async () => {
    vi.spyOn(projectDetection, 'identifyProjectSlug').mockResolvedValue(
      undefined,
    );

    const args = {
      params: {
        workspaceRoot: '/workspace',
        gitRemoteURL: 'https://github.com/org/repo.git',
      },
    };

    const controller = new AbortController();
    const response = await getFlakyTestLogs(args, {
      signal: controller.signal,
    });

    expect(response).toHaveProperty('content');
    expect(response).toHaveProperty('isError', true);
    expect(Array.isArray(response.content)).toBe(true);
    expect(response.content[0]).toHaveProperty('type', 'text');
    expect(typeof response.content[0].text).toBe('string');
  });

  it('should use projectSlug directly when provided', async () => {
    vi.spyOn(getFlakyTestsModule, 'default').mockResolvedValue([
      {
        name: 'flakyTest',
        message: 'Test failure message',
        run_time: '1.5',
        result: 'failure',
        classname: 'TestClass',
        file: 'path/to/file.js',
      },
    ]);

    vi.spyOn(formatFlakyTestsModule, 'formatFlakyTests').mockReturnValue({
      content: [
        {
          type: 'text',
          text: 'Flaky test output',
        },
      ],
    });

    const args = {
      params: {
        projectSlug: 'gh/org/repo',
      },
    };

    const controller = new AbortController();
    await getFlakyTestLogs(args, {
      signal: controller.signal,
    });

    expect(getFlakyTestsModule.default).toHaveBeenCalledWith({
      projectSlug: 'gh/org/repo',
    });
    // Verify that no project detection methods were called
    expect(projectDetection.getProjectSlugFromURL).not.toHaveBeenCalled();
    expect(projectDetection.identifyProjectSlug).not.toHaveBeenCalled();
  });

  it('should return a valid MCP success response with flaky tests', async () => {
    vi.spyOn(projectDetection, 'getProjectSlugFromURL').mockReturnValue(
      'gh/org/repo',
    );

    vi.spyOn(getFlakyTestsModule, 'default').mockResolvedValue([
      {
        name: 'flakyTest',
        message: 'Test failure message',
        run_time: '1.5',
        result: 'failure',
        classname: 'TestClass',
        file: 'path/to/file.js',
      },
    ]);

    vi.spyOn(formatFlakyTestsModule, 'formatFlakyTests').mockReturnValue({
      content: [
        {
          type: 'text',
          text: 'Flaky test output',
        },
      ],
    });

    const args = {
      params: {
        projectURL: 'https://app.circleci.com/pipelines/gh/org/repo',
      },
    };

    const controller = new AbortController();
    const response = await getFlakyTestLogs(args, {
      signal: controller.signal,
    });

    expect(response).toHaveProperty('content');
    expect(Array.isArray(response.content)).toBe(true);
    expect(response.content[0]).toHaveProperty('type', 'text');
    expect(typeof response.content[0].text).toBe('string');
  });

  it('should write flaky tests to files when FILE_OUTPUT_DIRECTORY is set', async () => {
    process.env.FILE_OUTPUT_DIRECTORY = '/tmp/test-output';

    // Mock path.join to return predictable file paths for cross-platform test consistency
    // This ensures the same path format regardless of OS (Windows uses \, Unix uses /)
    mockJoin.mockImplementation((dir, filename) => `${dir}/${filename}`);

    vi.spyOn(getFlakyTestsModule, 'default').mockResolvedValue([
      {
        name: 'flakyTest',
        message: 'Test failure message',
        run_time: '1.5',
        result: 'failure',
        classname: 'TestClass',
        file: 'path/to/file.js',
      },
      {
        name: 'anotherFlakyTest',
        message: 'Another test failure',
        run_time: '2.1',
        result: 'failure',
        classname: 'AnotherClass',
        file: 'path/to/another.js',
      },
    ]);

    const args = {
      params: {
        projectSlug: 'gh/org/repo',
      },
    };

    const controller = new AbortController();
    const response = await getFlakyTestLogs(args, {
      signal: controller.signal,
    });

    expect(mockMkdirSync).toHaveBeenCalledWith(getFlakyTestsOutputDirectory(), {
      recursive: true,
    });
    expect(mockWriteFileSync).toHaveBeenCalledTimes(3);

    expect(response).toHaveProperty('content');
    expect(Array.isArray(response.content)).toBe(true);
    expect(response.content[0]).toHaveProperty('type', 'text');
    expect(response.content[0].text).toContain(
      'Found 2 flaky tests that need stabilization',
    );
    expect(response.content[0].text).toContain(getFlakyTestsOutputDirectory());
  });

  it('should handle no flaky tests found in file output mode', async () => {
    process.env.FILE_OUTPUT_DIRECTORY = '/tmp/test-output';

    vi.spyOn(getFlakyTestsModule, 'default').mockResolvedValue([]);

    const args = {
      params: {
        projectSlug: 'gh/org/repo',
      },
    };

    const controller = new AbortController();
    const response = await getFlakyTestLogs(args, {
      signal: controller.signal,
    });

    expect(response).toHaveProperty('content');
    expect(response.content[0].text).toBe(
      'No flaky tests found - no files created',
    );
  });
});

```

--------------------------------------------------------------------------------
/src/tools/getLatestPipelineStatus/handler.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { getLatestPipelineStatus } from './handler.js';
import * as projectDetection from '../../lib/project-detection/index.js';
import * as getLatestPipelineWorkflowsModule from '../../lib/latest-pipeline/getLatestPipelineWorkflows.js';
import * as formatLatestPipelineStatusModule from '../../lib/latest-pipeline/formatLatestPipelineStatus.js';
import { McpSuccessResponse } from '../../lib/mcpResponse.js';

// Mock dependencies
vi.mock('../../lib/project-detection/index.js');
vi.mock('../../lib/latest-pipeline/getLatestPipelineWorkflows.js');
vi.mock('../../lib/latest-pipeline/formatLatestPipelineStatus.js');

describe('getLatestPipelineStatus handler', () => {
  const mockWorkflows = [
    {
      id: 'workflow-id-1',
      name: 'build-and-test',
      status: 'success',
      created_at: '2023-01-01T12:00:00Z',
      stopped_at: '2023-01-01T12:05:00Z',
      pipeline_number: 123,
      project_slug: 'gh/circleci/project',
    },
  ];

  const mockFormattedResponse: McpSuccessResponse = {
    content: [
      {
        type: 'text' as const,
        text: 'Formatted pipeline status',
      },
    ],
  };

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

    // Setup default mocks
    vi.mocked(projectDetection.getProjectSlugFromURL).mockReturnValue(
      'gh/circleci/project',
    );
    vi.mocked(projectDetection.getBranchFromURL).mockReturnValue('main');
    vi.mocked(projectDetection.identifyProjectSlug).mockResolvedValue(
      'gh/circleci/project',
    );
    vi.mocked(
      getLatestPipelineWorkflowsModule.getLatestPipelineWorkflows,
    ).mockResolvedValue(mockWorkflows);
    vi.mocked(
      formatLatestPipelineStatusModule.formatLatestPipelineStatus,
    ).mockReturnValue(mockFormattedResponse);
  });

  it('should get latest pipeline status using projectURL', async () => {
    const args = {
      params: {
        projectURL:
          'https://app.circleci.com/pipelines/github/circleci/project',
      },
    };

    const controller = new AbortController();
    const response = await getLatestPipelineStatus(args as any, {
      signal: controller.signal,
    });

    expect(projectDetection.getProjectSlugFromURL).toHaveBeenCalledWith(
      args.params.projectURL,
    );
    expect(projectDetection.getBranchFromURL).toHaveBeenCalledWith(
      args.params.projectURL,
    );
    expect(
      getLatestPipelineWorkflowsModule.getLatestPipelineWorkflows,
    ).toHaveBeenCalledWith({
      projectSlug: 'gh/circleci/project',
      branch: 'main',
    });
    expect(
      formatLatestPipelineStatusModule.formatLatestPipelineStatus,
    ).toHaveBeenCalledWith(mockWorkflows);
    expect(response).toEqual(mockFormattedResponse);
  });

  it('should return a valid MCP error response when projectSlug is provided without branch', async () => {
    const args = {
      params: {
        projectSlug: 'gh/circleci/project',
      },
    };

    const controller = new AbortController();
    const response = await getLatestPipelineStatus(args as any, {
      signal: controller.signal,
    });

    expect(response).toHaveProperty('content');
    expect(response).toHaveProperty('isError', true);
    expect(Array.isArray(response.content)).toBe(true);
    expect(response.content[0]).toHaveProperty('type', 'text');
    expect(typeof response.content[0].text).toBe('string');
    expect(response.content[0].text).toContain('Branch not provided');
  });

  it('should get latest pipeline status using workspace and git info', async () => {
    const args = {
      params: {
        workspaceRoot: '/path/to/workspace',
        gitRemoteURL: 'https://github.com/circleci/project.git',
        branch: 'feature/branch',
      },
    };

    const controller = new AbortController();
    const response = await getLatestPipelineStatus(args as any, {
      signal: controller.signal,
    });

    expect(projectDetection.identifyProjectSlug).toHaveBeenCalledWith({
      gitRemoteURL: args.params.gitRemoteURL,
    });
    expect(
      getLatestPipelineWorkflowsModule.getLatestPipelineWorkflows,
    ).toHaveBeenCalledWith({
      projectSlug: 'gh/circleci/project',
      branch: 'feature/branch',
    });
    expect(
      formatLatestPipelineStatusModule.formatLatestPipelineStatus,
    ).toHaveBeenCalledWith(mockWorkflows);
    expect(response).toEqual(mockFormattedResponse);
  });

  it('should get latest pipeline status using projectSlug and branch', async () => {
    const args = {
      params: {
        projectSlug: 'gh/circleci/project',
        branch: 'feature/branch',
      },
    };

    const controller = new AbortController();
    const response = await getLatestPipelineStatus(args as any, {
      signal: controller.signal,
    });

    // Verify that project detection functions were not called
    expect(projectDetection.getProjectSlugFromURL).not.toHaveBeenCalled();
    expect(projectDetection.identifyProjectSlug).not.toHaveBeenCalled();

    expect(
      getLatestPipelineWorkflowsModule.getLatestPipelineWorkflows,
    ).toHaveBeenCalledWith({
      projectSlug: 'gh/circleci/project',
      branch: 'feature/branch',
    });
    expect(
      formatLatestPipelineStatusModule.formatLatestPipelineStatus,
    ).toHaveBeenCalledWith(mockWorkflows);
    expect(response).toEqual(mockFormattedResponse);
  });

  it('should return error when no valid inputs are provided', async () => {
    const args = {
      params: {},
    };

    const controller = new AbortController();
    const response = await getLatestPipelineStatus(args as any, {
      signal: controller.signal,
    });

    expect(response).toHaveProperty('content');
    expect(response.content[0]).toHaveProperty('type', 'text');
    expect(response.content[0].text).toContain('Missing required inputs');
  });

  it('should return error when project slug cannot be identified', async () => {
    // Return null to simulate project not found
    vi.mocked(projectDetection.identifyProjectSlug).mockResolvedValue(
      null as unknown as string,
    );

    const args = {
      params: {
        workspaceRoot: '/path/to/workspace',
        gitRemoteURL: 'https://github.com/circleci/project.git',
        branch: 'feature/branch',
      },
    };

    const controller = new AbortController();
    const response = await getLatestPipelineStatus(args as any, {
      signal: controller.signal,
    });

    expect(response).toHaveProperty('content');
    expect(response.content[0]).toHaveProperty('type', 'text');
    expect(response.content[0].text).toContain('Project not found');
  });

  it('should get pipeline status when branch is provided from URL but not in params', async () => {
    const args = {
      params: {
        projectURL:
          'https://app.circleci.com/pipelines/github/circleci/project?branch=develop',
      },
    };

    const controller = new AbortController();
    const response = await getLatestPipelineStatus(args as any, {
      signal: controller.signal,
    });

    expect(
      getLatestPipelineWorkflowsModule.getLatestPipelineWorkflows,
    ).toHaveBeenCalledWith({
      projectSlug: 'gh/circleci/project',
      branch: 'main', // This is what our mock returns
    });
    expect(response).toEqual(mockFormattedResponse);
  });

  it('should handle errors from getLatestPipelineWorkflows', async () => {
    vi.mocked(
      getLatestPipelineWorkflowsModule.getLatestPipelineWorkflows,
    ).mockRejectedValue(new Error('Failed to fetch workflows'));

    const args = {
      params: {
        projectURL:
          'https://app.circleci.com/pipelines/github/circleci/project',
      },
    };

    // We expect the handler to throw the error so we can catch it
    const controller = new AbortController();
    await expect(
      getLatestPipelineStatus(args as any, { signal: controller.signal }),
    ).rejects.toThrow('Failed to fetch workflows');
  });
});

```

--------------------------------------------------------------------------------
/src/lib/project-detection/index.test.ts:
--------------------------------------------------------------------------------

```typescript
import {
  getPipelineNumberFromURL,
  getProjectSlugFromURL,
  getBranchFromURL,
  getJobNumberFromURL,
} from './index.js';
import { describe, it, expect } from 'vitest';

describe('getPipelineNumberFromURL', () => {
  it.each([
    // Workflow URL
    {
      url: 'https://app.circleci.com/pipelines/gh/organization/project/2/workflows/abc123de-f456-78gh-90ij-klmnopqrstuv',
      expected: 2,
    },
    // Workflow URL
    {
      url: 'https://app.circleci.com/pipelines/circleci/GM1mbrQEWnNbzLKEnotDo4/5gh9pgQgohHwicwomY5nYQ/123/workflows/abc123de-f456-78gh-90ij-klmnopqrstuv',
      expected: 123,
    },
    // Project URL (no pipeline number)
    {
      url: 'https://app.circleci.com/pipelines/gh/organization/project',
      expected: undefined,
    },
    // Project URL (missing all info)
    {
      url: 'https://app.circleci.com/gh/organization/project',
      expected: undefined,
    },
    // Project URL (Legacy job URL format with job number returns undefined for pipeline number)
    {
      url: 'https://circleci.com/gh/organization/project/123',
      expected: undefined,
    },
    // Project URL (Legacy job URL format with job number returns undefined for pipeline number)
    {
      url: 'https://circleci.server.customdomain.com/gh/organization/project/123',
      expected: undefined,
    },
  ])('extracts pipeline number $expected from URL', ({ url, expected }) => {
    expect(getPipelineNumberFromURL(url)).toBe(expected);
  });

  it('should not throw error for invalid CircleCI URL format. Returns undefined for pipeline number', () => {
    expect(() =>
      getPipelineNumberFromURL('https://app.circleci.com/invalid/url'),
    ).not.toThrow();
  });

  it('throws error when pipeline number is not a valid number', () => {
    expect(() =>
      getPipelineNumberFromURL(
        'https://app.circleci.com/pipelines/gh/organization/project/abc/workflows/abc123de-f456-78gh-90ij-klmnopqrstuv',
      ),
    ).toThrow('Pipeline number in URL is not a valid number');
  });
});

describe('getProjectSlugFromURL', () => {
  it.each([
    // Workflow URL
    {
      url: 'https://app.circleci.com/pipelines/gh/organization/project/2/workflows/abc123de-f456-78gh-90ij-klmnopqrstuv',
      expected: 'gh/organization/project',
    },
    // Workflow URL
    {
      url: 'https://app.circleci.com/pipelines/circleci/GM1mbrQEWnNbzLKEnotDo4/5gh9pgQgohHwicwomY5nYQ/123/workflows/abc123de-f456-78gh-90ij-klmnopqrstuv',
      expected: 'circleci/GM1mbrQEWnNbzLKEnotDo4/5gh9pgQgohHwicwomY5nYQ',
    },
    // Pipeline URL
    {
      url: 'https://app.circleci.com/pipelines/gh/organization/project/123',
      expected: 'gh/organization/project',
    },
    // Legacy Pipeline URL for gh
    {
      url: 'https://circleci.com/gh/organization/project/123',
      expected: 'gh/organization/project',
    },
    // Legacy Pipeline URL for Github
    {
      url: 'https://circleci.com/github/organization/project/123',
      expected: 'github/organization/project',
    },
    // Legacy Pipeline URL for bb
    {
      url: 'https://circleci.com/bb/organization/project/123',
      expected: 'bb/organization/project',
    },
    // Legacy Pipeline URL for Bitbucket
    {
      url: 'https://circleci.com/bitbucket/organization/project/123',
      expected: 'bitbucket/organization/project',
    },
    // Legacy Pipeline URL for CircleCI
    {
      url: 'https://circleci.com/circleci/GM1mbrQEWnNbzLKEnotDo4/5gh9pgQgohHwicwomY5nYQ/456',
      expected: 'circleci/GM1mbrQEWnNbzLKEnotDo4/5gh9pgQgohHwicwomY5nYQ',
    },
    // Pipeline URL
    {
      url: 'https://app.circleci.com/pipelines/circleci/GM1mbrQEWnNbzLKEnotDo4/5gh9pgQgohHwicwomY5nYQ/456',
      expected: 'circleci/GM1mbrQEWnNbzLKEnotDo4/5gh9pgQgohHwicwomY5nYQ',
    },
    // Job URL
    {
      url: 'https://app.circleci.com/pipelines/gh/organization/project/2/workflows/abc123de-f456-78gh-90ij-klmnopqrstuv/jobs/xyz789',
      expected: 'gh/organization/project',
    },
    // Job URL
    {
      url: 'https://app.circleci.com/pipelines/circleci/GM1mbrQEWnNbzLKEnotDo4/5gh9pgQgohHwicwomY5nYQ/123/workflows/abc123de-f456-78gh-90ij-klmnopqrstuv/jobs/def456',
      expected: 'circleci/GM1mbrQEWnNbzLKEnotDo4/5gh9pgQgohHwicwomY5nYQ',
    },
    // Project URL
    {
      url: 'https://app.circleci.com/pipelines/gh/organization/project',
      expected: 'gh/organization/project',
    },
    // Project URL
    {
      url: 'https://app.circleci.com/pipelines/circleci/GM1mbrQEWnNbzLKEnotDo4/5gh9pgQgohHwicwomY5nYQ',
      expected: 'circleci/GM1mbrQEWnNbzLKEnotDo4/5gh9pgQgohHwicwomY5nYQ',
    },
    // Project URL with query parameters
    {
      url: 'https://app.circleci.com/pipelines/github/CircleCI-Public/hungry-panda?branch=splitting',
      expected: 'github/CircleCI-Public/hungry-panda',
    },
  ])('extracts project slug $expected from URL', ({ url, expected }) => {
    expect(getProjectSlugFromURL(url)).toBe(expected);
  });

  it('throws error for invalid CircleCI URL format', () => {
    expect(() =>
      getProjectSlugFromURL('https://app.circleci.com/invalid/url'),
    ).toThrow(
      'Error getting project slug from URL: Invalid CircleCI URL format',
    );
  });

  it('throws error when project information is incomplete', () => {
    expect(() =>
      getProjectSlugFromURL('https://app.circleci.com/pipelines/gh'),
    ).toThrow('Unable to extract project information from URL');
  });
});

describe('getBranchFromURL', () => {
  it.each([
    // URL with branch parameter
    {
      url: 'https://app.circleci.com/pipelines/gh/organization/project?branch=feature-branch',
      expected: 'feature-branch',
    },
    // URL with branch parameter and other params
    {
      url: 'https://app.circleci.com/pipelines/gh/organization/project?branch=fix%2Fbug-123&filter=mine',
      expected: 'fix/bug-123',
    },
    // URL without branch parameter
    {
      url: 'https://app.circleci.com/pipelines/gh/organization/project',
      expected: undefined,
    },
    // URL with other parameters but no branch
    {
      url: 'https://app.circleci.com/pipelines/gh/organization/project?filter=mine',
      expected: undefined,
    },
  ])('extracts branch $expected from URL', ({ url, expected }) => {
    expect(getBranchFromURL(url)).toBe(expected);
  });

  it('throws error for invalid CircleCI URL format', () => {
    expect(() => getBranchFromURL('not-a-url')).toThrow(
      'Error getting branch from URL: Invalid CircleCI URL format',
    );
  });
});

describe('getJobNumberFromURL', () => {
  it.each([
    // Job URL with numeric job number
    {
      url: 'https://app.circleci.com/pipelines/gh/organization/project/123/workflows/abc123de-f456-78gh-90ij-klmnopqrstuv/jobs/456',
      expected: 456,
    },
    // Job URL with complex project path
    {
      url: 'https://app.circleci.com/pipelines/circleci/GM1mbrQEWnNbzLKEnotDo4/5gh9pgQgohHwicwomY5nYQ/123/workflows/abc123de-f456-78gh-90ij-klmnopqrstuv/jobs/789',
      expected: 789,
    },
    // Job URL with legacy format
    {
      url: 'https://circleci.com/gh/organization/project/123',
      expected: 123,
    },
    // Job URL with legacy format with custom domain
    {
      url: 'https://circleci.server.customdomain.com/gh/organization/project/123',
      expected: 123,
    },
    // Workflow URL (no job number)
    {
      url: 'https://app.circleci.com/pipelines/gh/organization/project/123/workflows/abc123de-f456-78gh-90ij-klmnopqrstuv',
      expected: undefined,
    },
    // Pipeline URL (no job number)
    {
      url: 'https://app.circleci.com/pipelines/gh/organization/project/123',
      expected: undefined,
    },
    // Project URL (no job number)
    {
      url: 'https://app.circleci.com/pipelines/gh/organization/project',
      expected: undefined,
    },
  ])('extracts job number $expected from URL', ({ url, expected }) => {
    expect(getJobNumberFromURL(url)).toBe(expected);
  });

  it('throws error when job number is not a valid number', () => {
    expect(() =>
      getJobNumberFromURL(
        'https://app.circleci.com/pipelines/gh/organization/project/123/workflows/abc123de-f456-78gh-90ij-klmnopqrstuv/jobs/abc',
      ),
    ).toThrow('Job number in URL is not a valid number');
  });
});

```

--------------------------------------------------------------------------------
/src/lib/rateLimitedRequests/index.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, test, expect, vi, beforeAll } from 'vitest';
import { rateLimitedRequests } from '.';

type MockResponse = {
  ok: boolean;
  json: () => Promise<{ args: Record<string, string | string[]> }>;
};

const mockFetch = (url: string): Promise<MockResponse> => {
  return Promise.resolve({
    ok: true,
    json: () => {
      const params = url.split('?')[1].split('&');
      const paramsMap = params.reduce<Record<string, string | string[]>>(
        (map, paramPair) => {
          const values = paramPair.split('=');
          if (map[values[0]] && Array.isArray(map[values[0]])) {
            (map[values[0]] as string[]).push(values[1]);
          } else if (map[values[0]] && !Array.isArray(map[values[0]])) {
            map[values[0]] = [map[values[0]] as string, values[1]];
          } else {
            map[values[0]] = values[1];
          }
          return map;
        },
        {},
      );
      return Promise.resolve({ args: paramsMap });
    },
  });
};

// Test configuration
beforeAll(() => {
  vi.setConfig({ testTimeout: 60000 });
});

const maxRetries = 2;
const retryDelayInMillis = 500;
const requestURL = `https://httpbin.org/get`;

// Helper functions
function generateRequests(numberOfRequests: number): (() => Promise<any>)[] {
  return Array.from(
    { length: numberOfRequests },
    (_, i) => () => makeRequest(i),
  );
}

async function makeRequest(
  requestId: number,
  attempt = 1,
): Promise<{ args: any } | { error: any }> {
  try {
    const response = await mockFetch(requestURL + '?id=' + requestId);
    if (!response.ok) {
      throw new Error(`HTTP error occurred`);
    }
    return await response.json();
  } catch (error) {
    if (attempt <= maxRetries) {
      await new Promise((resolve) => setTimeout(resolve, retryDelayInMillis));
      return makeRequest(requestId, attempt + 1);
    } else {
      return {
        error: error instanceof Error ? error.toString() : String(error),
      };
    }
  }
}

function isResponseContainData(
  result: any[],
  startIndex: number,
  endIndex: number,
): boolean {
  for (let i = startIndex; i <= endIndex; i++) {
    const resultItem = JSON.stringify(result[i - startIndex]);
    if (!resultItem || !resultItem.includes(`{"args":{"id":"${i}"}`)) {
      return false;
    }
  }
  return true;
}

function isBatchResponseContainData(
  batchItems: any[],
  startIndex: number,
  endIndex: number,
): boolean {
  return batchItems.some(
    (batchItem) =>
      batchItem.startIndex === startIndex &&
      batchItem.stopIndex === endIndex &&
      isResponseContainData(batchItem.results, startIndex, endIndex),
  );
}

function checkProgressItems(
  progressItems: any[],
  maxRequests: number,
  totalRequests: number,
  expectedProgressItemsCount: number,
): void {
  expect(progressItems.length).toBe(expectedProgressItemsCount);
  for (let i = 0; i < expectedProgressItemsCount; i++) {
    expect(progressItems[i].totalRequests).toBe(totalRequests);
    expect(progressItems[i].completedRequests).toBe((1 + i) * maxRequests);
  }
}

function checkBatchItems(
  batchItems: any[],
  batchSize: number,
  totalRequests: number,
): void {
  expect(batchItems.length).toBe(Math.ceil(totalRequests / batchSize));
  for (const batch of batchItems) {
    const expectedSize = Math.min(batchSize, totalRequests - batch.startIndex);
    expect(batch.results.length).toBe(expectedSize);
    const result = isResponseContainData(
      batch.results,
      batch.startIndex,
      batch.stopIndex,
    );
    if (!result) {
      console.log('items ' + JSON.stringify(batchItems));
      console.log(
        `startIndex ${batch.startIndex}, endIndex ${batch.stopIndex}`,
      );
    }
    expect(result).toBe(true);
  }
}

// Options creator
function createOptions(
  batchSize?: number,
  onProgress?: (progress: {
    totalRequests: number;
    completedRequests: number;
  }) => void,
  onBatchComplete?: (batch: {
    startIndex: number;
    stopIndex: number;
    results: any[];
  }) => void,
) {
  return { batchSize, onProgress, onBatchComplete };
}

describe('rateLimitedRequests', () => {
  test('execute 50 requests', async () => {
    const requests = generateRequests(50);
    const result = await rateLimitedRequests(
      requests,
      25,
      1000,
      createOptions(),
    );

    expect(result.length).toBe(50);
    expect(isResponseContainData(result, 0, 49)).toBe(true);
  });

  test('execute 1000 requests', async () => {
    const requests = generateRequests(1000);
    const batchItems: any[] = [];
    const progressItems: any[] = [];

    const result = await rateLimitedRequests(
      requests,
      100,
      100,
      createOptions(
        50,
        (progress) => progressItems.push(progress),
        (batch) => batchItems.push(batch),
      ),
    );

    expect(result.length).toBe(1000);
    expect(isResponseContainData(result, 0, 999)).toBe(true);
    expect(batchItems.length).toBe(20);

    for (const batch of batchItems) {
      expect(batch.results.length).toBe(50);
      expect(
        isResponseContainData(batch.results, batch.startIndex, batch.stopIndex),
      ).toBe(true);
    }

    checkProgressItems(progressItems, 100, 1000, 10);
  }, 30000);

  describe('with batch', () => {
    test('execute 50 requests with batch', async () => {
      const requests = generateRequests(50);
      const batchItems: any[] = [];

      const result = await rateLimitedRequests(
        requests,
        25,
        1000,
        createOptions(10, undefined, (batch) => batchItems.push(batch)),
      );

      expect(result.length).toBe(50);
      expect(batchItems.length).toBe(5);
      checkBatchItems(batchItems, 10, 50);
    });

    test('batchSize bigger than total requests', async () => {
      const requests = generateRequests(50);
      const batchItems: any[] = [];

      const result = await rateLimitedRequests(
        requests,
        25,
        1000,
        createOptions(60, undefined, (batch) => batchItems.push(batch)),
      );

      expect(result.length).toBe(50);
      expect(batchItems.length).toBe(1);
      expect(batchItems[0].results.length).toBe(50);
    });

    test('batchSize equals to total requests', async () => {
      const requests = generateRequests(50);
      const batchItems: any[] = [];

      const result = await rateLimitedRequests(
        requests,
        25,
        1000,
        createOptions(50, undefined, (batch) => batchItems.push(batch)),
      );

      expect(result.length).toBe(50);
      expect(batchItems.length).toBe(1);
      isBatchResponseContainData(batchItems, 0, 49);
    });

    test('with onProgress callback', async () => {
      const requests = generateRequests(50);
      const progressItems: any[] = [];

      const result = await rateLimitedRequests(
        requests,
        25,
        1000,
        createOptions(50, (progress) => progressItems.push(progress)),
      );

      expect(result.length).toBe(50);
      checkProgressItems(progressItems, 25, 50, 2);
    });
  });

  describe('batch processing', () => {
    test('should process empty batch items correctly', async () => {
      const requests = generateRequests(30);
      const batchItems: any[] = [];

      const result = await rateLimitedRequests(
        requests,
        10,
        1000,
        createOptions(10, undefined, (batch) => batchItems.push(batch)),
      );

      expect(result.length).toBe(30);
      expect(batchItems.length).toBe(3);
      for (const batchItem of batchItems) {
        expect(batchItem.results.length).toBe(10);
      }
    });

    test('should handle partial batch correctly', async () => {
      const requests = generateRequests(25);
      const batchItems: any[] = [];

      const result = await rateLimitedRequests(
        requests,
        10,
        1000,
        createOptions(10, undefined, (batch) => batchItems.push(batch)),
      );

      expect(result.length).toBe(25);
      expect(batchItems.length).toBe(3);
      expect(batchItems[2].results.length).toBe(5); // Last batch should have 5 items
    });
  });

  describe('progress tracking', () => {
    test('should track progress correctly with uneven batches', async () => {
      const requests = generateRequests(25);
      const progressItems: any[] = [];

      const result = await rateLimitedRequests(
        requests,
        10,
        1000,
        createOptions(undefined, (progress) => progressItems.push(progress)),
      );

      expect(result.length).toBe(25);
      expect(progressItems.length).toBe(3);
      expect(progressItems[0].completedRequests).toBe(10);
      expect(progressItems[1].completedRequests).toBe(20);
      expect(progressItems[2].completedRequests).toBe(25);
      expect(progressItems[2].totalRequests).toBe(25);
    });
  });

  describe('error cases', () => {
    test('options not passed', async () => {
      const requests = generateRequests(5);
      const result = await rateLimitedRequests(requests, 25, 1000);
      expect(result.length).toBe(5);
    });

    test('should handle undefined options gracefully', async () => {
      const requests = generateRequests(5);
      const result = await rateLimitedRequests(requests, 25, 1000, undefined);
      expect(result.length).toBe(5);
    });

    test('should handle empty batch size gracefully', async () => {
      const requests = generateRequests(5);
      const result = await rateLimitedRequests(
        requests,
        25,
        1000,
        createOptions(0),
      );
      expect(result.length).toBe(5);
    });
  });
});

```

--------------------------------------------------------------------------------
/src/clients/schemas.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from 'zod';

type ContextSchema = {
  [k: string]: 'string' | 'number' | 'boolean' | 'date' | ContextSchema;
};

const contextSchemaSchema: z.ZodSchema<ContextSchema> = z.lazy(() =>
  z
    .record(
      z.union([
        contextSchemaSchema,
        z
          .enum(['string', 'number', 'boolean', 'date'])
          .describe('a primitive data type: string, number, boolean, or date'),
      ]),
    )
    .describe(
      'a schema structure, mapping keys to a primitive type (string, number, boolean, or date) or recursively to a nested schema',
    ),
);

const promptObjectSchema = z
  .object({
    template: z.string().describe('a mustache template string'),
    contextSchema: contextSchemaSchema.describe(
      'an arbitrarily nested map of variable names from the mustache template to primitive types (string, number, or boolean)',
    ),
  })
  .describe(
    'a complete prompt template with a template string and a context schema',
  );

const RuleReviewSchema = z.object({
  isRuleCompliant: z.boolean(),
  relatedRules: z.object({
    compliant: z.array(
      z.object({
        rule: z.string(),
        reason: z.string(),
        confidenceScore: z.number(),
      }),
    ),
    violations: z.array(
      z.object({
        rule: z.string(),
        reason: z.string(),
        confidenceScore: z.number(),
        violationInstances: z.array(
          z.object({
            file: z.string(),
            lineNumbersInDiff: z.array(z.string()),
            violatingCodeSnippet: z.string(),
            explanationOfViolation: z.string(),
          }),
        ),
      }),
    ),
    requiresHumanReview: z.array(
      z.object({
        rule: z.string(),
        reason: z.string(),
        confidenceScore: z.number(),
        humanReviewRequired: z.object({
          pointsOfAmbiguity: z.array(z.string()),
          questionsForManualReviewer: z.array(z.string()),
        }),
      }),
    ),
  }),
  unrelatedRules: z.array(z.string()).optional(),
});

const FollowedProjectSchema = z.object({
  name: z.string(),
  slug: z.string(),
  vcs_type: z.string(),
});

const PipelineSchema = z.object({
  id: z.string(),
  project_slug: z.string(),
  number: z.number(),
});

const WorkflowSchema = z.object({
  id: z.string(),
  name: z.string(),
  status: z.string().nullable(),
  created_at: z.string(),
  stopped_at: z.string().nullable().optional(),
  pipeline_number: z.number(),
  project_slug: z.string(),
  pipeline_id: z.string(),
});

const RerunWorkflowSchema = z.object({
  workflow_id: z.string(),
});

const JobSchema = z.object({
  job_number: z.number().optional(),
  id: z.string(),
});

const JobDetailsSchema = z.object({
  build_num: z.number(),
  steps: z.array(
    z.object({
      name: z.string(),
      actions: z.array(
        z.object({
          index: z.number(),
          step: z.number(),
          failed: z.boolean().nullable(),
        }),
      ),
    }),
  ),
  workflows: z.object({
    job_name: z.string(),
  }),
});

const FlakyTestSchema = z.object({
  flaky_tests: z.array(
    z.object({
      job_number: z.number(),
      test_name: z.string(),
    }),
  ),
  total_flaky_tests: z.number(),
});

const TestSchema = z.object({
  message: z.string(),
  run_time: z.union([z.string(), z.number()]),
  file: z.string().optional(),
  result: z.string(),
  name: z.string(),
  classname: z.string(),
});

const PaginatedTestResponseSchema = z.object({
  items: z.array(TestSchema),
  next_page_token: z.string().nullable(),
});

const ConfigValidateSchema = z.object({
  valid: z.boolean(),
  errors: z
    .array(
      z.object({
        message: z.string(),
      }),
    )
    .nullable(),
  'output-yaml': z.string(),
  'source-yaml': z.string(),
});

const RunPipelineResponseSchema = z.object({
  number: z.number(),
});

const RollbackProjectRequestSchema = z.object({
  component_name: z.string().describe('The component name'),
  current_version: z.string().describe('The current version'),
  environment_name: z.string().describe('The environment name'),
  namespace: z.string().describe('The namespace').optional(),
  parameters: z.record(z.any()).describe('The extra parameters for the rollback pipeline').optional(),
  reason: z.string().describe('The reason for the rollback').optional(),
  target_version: z.string().describe('The target version'),
});

const RollbackProjectResponseSchema = z.object({
  id: z.string().describe('The ID of the rollback pipeline or the command created to handle the rollback'),
  rollback_type: z.string().describe('The type of the rollback'),
});

const DeploySettingsResponseSchema = z.object({
  create_autogenerated_releases: z.boolean().optional().describe('Whether to create autogenerated releases'),
  rollback_pipeline_definition_id: z.string().optional().describe('The rollback pipeline definition ID, if configured for this project'),
}).passthrough(); // Allow additional properties we might not know about

const DeployComponentsResponseSchema = z.object({
  items: z.array(z.object({
    id: z.string(),
    project_id: z.string(),
    name: z.string(),
    release_count: z.number(),
    labels: z.array(z.string()),
    created_at: z.string(),
    updated_at: z.string(),
  })),
  next_page_token: z.string().nullable(),
});

const DeployComponentVersionsResponseSchema = z.object({
  items: z.array(z.object({
    name: z.string(),
    namespace: z.string(),
    environment_id: z.string(),
    is_live: z.boolean(),
    pipeline_id: z.string(),
    workflow_id: z.string(),
    job_id: z.string(),
    job_number: z.number(),
    last_deployed_at: z.string(),
  })),
  next_page_token: z.string().nullable(),
});

export const DeployEnvironmentResponseSchema = z.object({
  items: z.array(z.object({
  id: z.string(),
  name: z.string(),
  created_at: z.string(),
  updated_at: z.string(),
  labels: z.array(z.string()),
  })),
  next_page_token: z.string().nullable(),
});

const ProjectSchema = z.object({
  id: z.string(),
  organization_id: z.string(),
});

const PipelineDefinitionSchema = z.object({
  id: z.string(),
  name: z.string(),
});

const PipelineDefinitionsResponseSchema = z.object({
  items: z.array(PipelineDefinitionSchema),
});

export const PipelineDefinition = PipelineDefinitionSchema;
export type PipelineDefinition = z.infer<typeof PipelineDefinitionSchema>;

export const PipelineDefinitionsResponse = PipelineDefinitionsResponseSchema;
export type PipelineDefinitionsResponse = z.infer<
  typeof PipelineDefinitionsResponseSchema
>;

export const Test = TestSchema;
export type Test = z.infer<typeof TestSchema>;

export const PaginatedTestResponse = PaginatedTestResponseSchema;
export type PaginatedTestResponse = z.infer<typeof PaginatedTestResponseSchema>;

export const FlakyTest = FlakyTestSchema;
export type FlakyTest = z.infer<typeof FlakyTestSchema>;

export const ConfigValidate = ConfigValidateSchema;
export type ConfigValidate = z.infer<typeof ConfigValidateSchema>;

// Export the schemas and inferred types with the same names as the original types
export const Pipeline = PipelineSchema;
export type Pipeline = z.infer<typeof PipelineSchema>;

export const RunPipelineResponse = RunPipelineResponseSchema;
export type RunPipelineResponse = z.infer<typeof RunPipelineResponseSchema>;

export const Project = ProjectSchema;
export type Project = z.infer<typeof ProjectSchema>;

export const PaginatedPipelineResponseSchema = z.object({
  items: z.array(Pipeline),
  next_page_token: z.string().nullable(),
});
export type PaginatedPipelineResponse = z.infer<
  typeof PaginatedPipelineResponseSchema
>;

export const Workflow = WorkflowSchema;
export type Workflow = z.infer<typeof WorkflowSchema>;

export const Job = JobSchema;
export type Job = z.infer<typeof JobSchema>;

export const JobDetails = JobDetailsSchema;
export type JobDetails = z.infer<typeof JobDetailsSchema>;

export const FollowedProject = FollowedProjectSchema;
export type FollowedProject = z.infer<typeof FollowedProjectSchema>;

export const PromptObject = promptObjectSchema;
export type PromptObject = z.infer<typeof PromptObject>;

export const RerunWorkflow = RerunWorkflowSchema;
export type RerunWorkflow = z.infer<typeof RerunWorkflowSchema>;

export const RuleReview = RuleReviewSchema;
export type RuleReview = z.infer<typeof RuleReviewSchema>;

export const RollbackProjectRequest = RollbackProjectRequestSchema;
export type RollbackProjectRequest = z.infer<typeof RollbackProjectRequestSchema>;

export const RollbackProjectResponse = RollbackProjectResponseSchema;
export type RollbackProjectResponse = z.infer<typeof RollbackProjectResponseSchema>;

export const DeploySettingsResponse = DeploySettingsResponseSchema;
export type DeploySettingsResponse = z.infer<typeof DeploySettingsResponseSchema>;

export const DeployComponentsResponse = DeployComponentsResponseSchema;
export type DeployComponentsResponse = z.infer<typeof DeployComponentsResponseSchema>;

export const DeployEnvironmentResponse = DeployEnvironmentResponseSchema;
export type DeployEnvironmentResponse = z.infer<typeof DeployEnvironmentResponseSchema>;

export const DeployComponentVersionsResponse = DeployComponentVersionsResponseSchema;
export type DeployComponentVersionsResponse = z.infer<typeof DeployComponentVersionsResponseSchema>;

const UsageExportJobStartSchema = z.object({
  usage_export_job_id: z.string().uuid(),
});

const UsageExportJobStatusSchema = z.object({
  state: z.string(),
  download_urls: z.array(z.string().url()).optional().nullable(),
});

export const UsageExportJobStart = UsageExportJobStartSchema;
export type UsageExportJobStart = z.infer<typeof UsageExportJobStartSchema>;

export const UsageExportJobStatus = UsageExportJobStatusSchema;
export type UsageExportJobStatus = z.infer<typeof UsageExportJobStatusSchema>;

```

--------------------------------------------------------------------------------
/src/tools/getJobTestResults/handler.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { getJobTestResults } from './handler.js';
import * as projectDetection from '../../lib/project-detection/index.js';
import * as getJobTestsModule from '../../lib/pipeline-job-tests/getJobTests.js';
import * as formatJobTestsModule from '../../lib/pipeline-job-tests/formatJobTests.js';

// Mock dependencies
vi.mock('../../lib/project-detection/index.js');
vi.mock('../../lib/pipeline-job-tests/getJobTests.js');
vi.mock('../../lib/pipeline-job-tests/formatJobTests.js');

describe('getJobTestResults handler', () => {
  beforeEach(() => {
    vi.resetAllMocks();
  });

  it('should return a valid MCP error response when no inputs are provided', async () => {
    const args = {
      params: {},
    };

    const controller = new AbortController();
    const response = await getJobTestResults(args, {
      signal: controller.signal,
    });

    expect(response).toHaveProperty('content');
    expect(response).toHaveProperty('isError', true);
    expect(Array.isArray(response.content)).toBe(true);
    expect(response.content[0]).toHaveProperty('type', 'text');
    expect(typeof response.content[0].text).toBe('string');
  });

  it('should return a valid MCP error response when project is not found', async () => {
    vi.spyOn(projectDetection, 'identifyProjectSlug').mockResolvedValue(
      undefined,
    );

    const args = {
      params: {
        workspaceRoot: '/workspace',
        gitRemoteURL: 'https://github.com/org/repo.git',
        branch: 'main',
      },
    } as any;

    const controller = new AbortController();
    const response = await getJobTestResults(args, {
      signal: controller.signal,
    });

    expect(response).toHaveProperty('content');
    expect(response).toHaveProperty('isError', true);
    expect(Array.isArray(response.content)).toBe(true);
    expect(response.content[0]).toHaveProperty('type', 'text');
    expect(typeof response.content[0].text).toBe('string');
  });

  it('should return a valid MCP error response when projectSlug is provided without branch', async () => {
    const args = {
      params: {
        projectSlug: 'gh/org/repo',
      },
    } as any;

    const controller = new AbortController();
    const response = await getJobTestResults(args, {
      signal: controller.signal,
    });

    expect(response).toHaveProperty('content');
    expect(response).toHaveProperty('isError', true);
    expect(Array.isArray(response.content)).toBe(true);
    expect(response.content[0]).toHaveProperty('type', 'text');
    expect(typeof response.content[0].text).toBe('string');
    expect(response.content[0].text).toContain('Branch not provided');
  });

  it('should return a valid MCP success response with test results for a specific job', async () => {
    vi.spyOn(projectDetection, 'getProjectSlugFromURL').mockReturnValue(
      'gh/org/repo',
    );
    vi.spyOn(projectDetection, 'getJobNumberFromURL').mockReturnValue(123);

    const mockTests = [
      {
        message: 'No failures',
        run_time: 0.5,
        file: 'src/test.js',
        result: 'success',
        name: 'should pass the test',
        classname: 'TestClass',
      },
    ];

    vi.spyOn(getJobTestsModule, 'getJobTests').mockResolvedValue(mockTests);

    vi.spyOn(formatJobTestsModule, 'formatJobTests').mockReturnValue({
      content: [
        {
          type: 'text',
          text: 'Test results output',
        },
      ],
    });

    const args = {
      params: {
        projectURL:
          'https://app.circleci.com/pipelines/gh/org/repo/123/workflows/abc-def/jobs/123',
      },
    } as any;

    const controller = new AbortController();
    const response = await getJobTestResults(args, {
      signal: controller.signal,
    });

    expect(response).toHaveProperty('content');
    expect(Array.isArray(response.content)).toBe(true);
    expect(response.content[0]).toHaveProperty('type', 'text');
    expect(typeof response.content[0].text).toBe('string');

    expect(getJobTestsModule.getJobTests).toHaveBeenCalledWith({
      projectSlug: 'gh/org/repo',
      branch: undefined,
      jobNumber: 123,
    });
  });

  it('should return a valid MCP success response with test results for projectSlug and branch', async () => {
    const mockTests = [
      {
        message: 'No failures',
        run_time: 0.5,
        file: 'src/test.js',
        result: 'success',
        name: 'should pass the test',
        classname: 'TestClass',
      },
    ];

    vi.spyOn(getJobTestsModule, 'getJobTests').mockResolvedValue(mockTests);

    vi.spyOn(formatJobTestsModule, 'formatJobTests').mockReturnValue({
      content: [
        {
          type: 'text',
          text: 'Test results output',
        },
      ],
    });

    const args = {
      params: {
        projectSlug: 'gh/org/repo',
        branch: 'feature/new-feature',
      },
    } as any;

    const controller = new AbortController();
    const response = await getJobTestResults(args, {
      signal: controller.signal,
    });

    expect(response).toHaveProperty('content');
    expect(Array.isArray(response.content)).toBe(true);
    expect(response.content[0]).toHaveProperty('type', 'text');
    expect(typeof response.content[0].text).toBe('string');

    expect(getJobTestsModule.getJobTests).toHaveBeenCalledWith({
      projectSlug: 'gh/org/repo',
      branch: 'feature/new-feature',
      jobNumber: undefined,
    });
  });

  it('should return a valid MCP success response with test results for a branch', async () => {
    vi.spyOn(projectDetection, 'identifyProjectSlug').mockResolvedValue(
      'gh/org/repo',
    );

    const mockTests = [
      {
        message: 'No failures',
        run_time: 0.5,
        file: 'src/test.js',
        result: 'success',
        name: 'should pass the test',
        classname: 'TestClass',
      },
    ];

    vi.spyOn(getJobTestsModule, 'getJobTests').mockResolvedValue(mockTests);

    vi.spyOn(formatJobTestsModule, 'formatJobTests').mockReturnValue({
      content: [
        {
          type: 'text',
          text: 'Test results output',
        },
      ],
    });

    const args = {
      params: {
        workspaceRoot: '/workspace',
        gitRemoteURL: 'https://github.com/org/repo.git',
        branch: 'main',
      },
    } as any;

    const controller = new AbortController();
    const response = await getJobTestResults(args, {
      signal: controller.signal,
    });

    expect(response).toHaveProperty('content');
    expect(Array.isArray(response.content)).toBe(true);
    expect(response.content[0]).toHaveProperty('type', 'text');
    expect(typeof response.content[0].text).toBe('string');

    expect(getJobTestsModule.getJobTests).toHaveBeenCalledWith({
      projectSlug: 'gh/org/repo',
      branch: 'main',
      jobNumber: undefined,
    });
  });

  it('should filter test results by success when filterByTestsResult is success', async () => {
    vi.spyOn(projectDetection, 'getProjectSlugFromURL').mockReturnValue(
      'gh/org/repo',
    );
    vi.spyOn(projectDetection, 'getJobNumberFromURL').mockReturnValue(123);

    const mockTests = [
      {
        message: 'No failures',
        run_time: 0.5,
        file: 'src/test1.js',
        result: 'success',
        name: 'should pass test 1',
        classname: 'TestClass1',
      },
      {
        message: 'Test failed',
        run_time: 0.3,
        file: 'src/test2.js',
        result: 'failure',
        name: 'should fail test 2',
        classname: 'TestClass2',
      },
      {
        message: 'No failures',
        run_time: 0.4,
        file: 'src/test3.js',
        result: 'success',
        name: 'should pass test 3',
        classname: 'TestClass3',
      },
    ];

    vi.spyOn(getJobTestsModule, 'getJobTests').mockResolvedValue(mockTests);

    vi.spyOn(formatJobTestsModule, 'formatJobTests').mockReturnValue({
      content: [
        {
          type: 'text',
          text: 'Test results output',
        },
      ],
    });

    const args = {
      params: {
        projectURL:
          'https://app.circleci.com/pipelines/gh/org/repo/123/workflows/abc-def/jobs/123',
        filterByTestsResult: 'success',
      },
    } as any;

    const controller = new AbortController();
    const response = await getJobTestResults(args, {
      signal: controller.signal,
    });

    expect(response).toHaveProperty('content');
    expect(Array.isArray(response.content)).toBe(true);
    expect(response.content[0]).toHaveProperty('type', 'text');
    expect(typeof response.content[0].text).toBe('string');

    expect(getJobTestsModule.getJobTests).toHaveBeenCalledWith({
      projectSlug: 'gh/org/repo',
      branch: undefined,
      jobNumber: 123,
      filterByTestsResult: 'success',
    });
  });

  it('should filter test results by failure when filterByTestsResult is failure', async () => {
    vi.spyOn(projectDetection, 'getProjectSlugFromURL').mockReturnValue(
      'gh/org/repo',
    );
    vi.spyOn(projectDetection, 'getJobNumberFromURL').mockReturnValue(123);

    const mockTests = [
      {
        message: 'No failures',
        run_time: 0.5,
        file: 'src/test1.js',
        result: 'success',
        name: 'should pass test 1',
        classname: 'TestClass1',
      },
      {
        message: 'Test failed',
        run_time: 0.3,
        file: 'src/test2.js',
        result: 'failure',
        name: 'should fail test 2',
        classname: 'TestClass2',
      },
      {
        message: 'Test failed',
        run_time: 0.4,
        file: 'src/test3.js',
        result: 'failure',
        name: 'should fail test 3',
        classname: 'TestClass3',
      },
    ];

    vi.spyOn(getJobTestsModule, 'getJobTests').mockResolvedValue(mockTests);

    vi.spyOn(formatJobTestsModule, 'formatJobTests').mockReturnValue({
      content: [
        {
          type: 'text',
          text: 'Test results output',
        },
      ],
    });

    const args = {
      params: {
        projectURL:
          'https://app.circleci.com/pipelines/gh/org/repo/123/workflows/abc-def/jobs/123',
        filterByTestsResult: 'failure',
      },
    } as any;

    const controller = new AbortController();
    const response = await getJobTestResults(args, {
      signal: controller.signal,
    });

    expect(response).toHaveProperty('content');
    expect(Array.isArray(response.content)).toBe(true);
    expect(response.content[0]).toHaveProperty('type', 'text');
    expect(typeof response.content[0].text).toBe('string');

    expect(getJobTestsModule.getJobTests).toHaveBeenCalledWith({
      projectSlug: 'gh/org/repo',
      branch: undefined,
      jobNumber: 123,
      filterByTestsResult: 'failure',
    });
  });
});

```

--------------------------------------------------------------------------------
/src/tools/rerunWorkflow/handler.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { rerunWorkflow } from './handler.js';
import * as client from '../../clients/client.js';

vi.mock('../../clients/client.js');

const failedWorkflowId = '00000000-0000-0000-0000-000000000000';
const successfulWorkflowId = '11111111-1111-1111-1111-111111111111';
const newWorkflowId = '11111111-1111-1111-1111-111111111111';

function setupMockClient(
  workflowStatus,
  rerunResult = { workflow_id: newWorkflowId },
) {
  const mockCircleCIClient = {
    workflows: {
      getWorkflow: vi
        .fn()
        .mockResolvedValue(
          workflowStatus !== undefined ? { status: workflowStatus } : undefined,
        ),
      rerunWorkflow: vi.fn().mockResolvedValue(rerunResult),
    },
  };
  vi.spyOn(client, 'getCircleCIClient').mockReturnValue(
    mockCircleCIClient as any,
  );
  return mockCircleCIClient;
}

describe('rerunWorkflow', () => {
  describe('when rerunning a failed workflow', () => {
    beforeEach(() => {
      vi.resetAllMocks();
    });
    it('should return the new workflowId and url to the user if requested to be rerun with a given workflowId', async () => {
      const mockCircleCIClient = setupMockClient('failed');
      const controller = new AbortController();
      const result = await rerunWorkflow(
        {
          params: {
            workflowId: failedWorkflowId,
          },
        },
        {
          signal: controller.signal,
        },
      );
      expect(mockCircleCIClient.workflows.rerunWorkflow).toHaveBeenCalledWith({
        workflowId: failedWorkflowId,
        fromFailed: true,
      });
      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: `New workflowId is ${newWorkflowId} and [View Workflow in CircleCI](https://app.circleci.com/pipelines/workflows/11111111-1111-1111-1111-111111111111)`,
          },
        ],
      });
    });

    it('should return the new workflowId and url to the user if requested to be rerun with a given workflowURL', async () => {
      const mockCircleCIClient = setupMockClient('failed');
      const controller = new AbortController();
      const result = await rerunWorkflow(
        {
          params: {
            workflowURL: `https://app.circleci.com/pipelines/workflows/${failedWorkflowId}`,
          },
        },
        {
          signal: controller.signal,
        },
      );
      expect(mockCircleCIClient.workflows.rerunWorkflow).toHaveBeenCalledWith({
        workflowId: failedWorkflowId,
        fromFailed: true,
      });
      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: `New workflowId is ${newWorkflowId} and [View Workflow in CircleCI](https://app.circleci.com/pipelines/workflows/11111111-1111-1111-1111-111111111111)`,
          },
        ],
      });
    });

    it('should return the new workflowId and url to the user if requested to be rerun from start with a given workflowId', async () => {
      const mockCircleCIClient = setupMockClient('failed');
      const controller = new AbortController();
      const result = await rerunWorkflow(
        {
          params: {
            workflowId: failedWorkflowId,
            fromFailed: false,
          },
        },
        {
          signal: controller.signal,
        },
      );
      expect(mockCircleCIClient.workflows.rerunWorkflow).toHaveBeenCalledWith({
        workflowId: failedWorkflowId,
        fromFailed: false,
      });
      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: `New workflowId is ${newWorkflowId} and [View Workflow in CircleCI](https://app.circleci.com/pipelines/workflows/11111111-1111-1111-1111-111111111111)`,
          },
        ],
      });
    });
  });

  describe('when rerunning a successful workflow', () => {
    beforeEach(() => {
      vi.resetAllMocks();
    });
    it('should return an error if requested to be rerun from failed with a given workflowId', async () => {
      const mockCircleCIClient = setupMockClient('success');
      mockCircleCIClient.workflows.rerunWorkflow.mockResolvedValue(undefined);
      const controller = new AbortController();
      const response = await rerunWorkflow(
        {
          params: {
            workflowId: successfulWorkflowId,
            fromFailed: true,
          },
        },
        {
          signal: controller.signal,
        },
      );
      expect(mockCircleCIClient.workflows.rerunWorkflow).not.toHaveBeenCalled();
      expect(response).toEqual({
        isError: true,
        content: [
          {
            type: 'text',
            text: 'Workflow is not failed, cannot rerun from failed',
          },
        ],
      });
    });
    it('should return the new workflowId and url to the user if requested to be rerun from start with a given workflowId', async () => {
      const mockCircleCIClient = setupMockClient('success');
      const controller = new AbortController();
      const response = await rerunWorkflow(
        {
          params: {
            workflowId: successfulWorkflowId,
            fromFailed: false,
          },
        },
        {
          signal: controller.signal,
        },
      );
      expect(mockCircleCIClient.workflows.rerunWorkflow).toHaveBeenCalledWith({
        workflowId: successfulWorkflowId,
        fromFailed: false,
      });
      expect(response).toEqual({
        content: [
          {
            type: 'text',
            text: `New workflowId is ${newWorkflowId} and [View Workflow in CircleCI](https://app.circleci.com/pipelines/workflows/11111111-1111-1111-1111-111111111111)`,
          },
        ],
      });
    });
    it('should return the new workflowId and url to the user if requested to be rerun from start with a given workflowURL', async () => {
      const mockCircleCIClient = setupMockClient('success');
      const controller = new AbortController();
      const response = await rerunWorkflow(
        {
          params: {
            workflowURL: `https://app.circleci.com/pipelines/workflows/${successfulWorkflowId}`,
            fromFailed: false,
          },
        },
        {
          signal: controller.signal,
        },
      );
      expect(mockCircleCIClient.workflows.rerunWorkflow).toHaveBeenCalledWith({
        workflowId: successfulWorkflowId,
        fromFailed: false,
      });
      expect(response).toEqual({
        content: [
          {
            type: 'text',
            text: `New workflowId is ${newWorkflowId} and [View Workflow in CircleCI](https://app.circleci.com/pipelines/workflows/11111111-1111-1111-1111-111111111111)`,
          },
        ],
      });
    });
    it('should return the new workflowId and url to the user if requested to be rerun with a given workflowId and no explicit fromFailed', async () => {
      const mockCircleCIClient = setupMockClient('success');
      const controller = new AbortController();
      const response = await rerunWorkflow(
        {
          params: {
            workflowId: successfulWorkflowId,
          },
        },
        {
          signal: controller.signal,
        },
      );
      expect(mockCircleCIClient.workflows.rerunWorkflow).toHaveBeenCalledWith({
        workflowId: successfulWorkflowId,
        fromFailed: false,
      });
      expect(response).toEqual({
        content: [
          {
            type: 'text',
            text: `New workflowId is ${newWorkflowId} and [View Workflow in CircleCI](https://app.circleci.com/pipelines/workflows/11111111-1111-1111-1111-111111111111)`,
          },
        ],
      });
    });
    it('should return the new workflowId and url to the user if requested to be rerun from start with a given workflowURL and no explicit fromFailed', async () => {
      const mockCircleCIClient = setupMockClient('success');
      const controller = new AbortController();
      const response = await rerunWorkflow(
        {
          params: {
            workflowURL: `https://app.circleci.com/pipelines/workflows/${successfulWorkflowId}`,
          },
        },
        {
          signal: controller.signal,
        },
      );
      expect(mockCircleCIClient.workflows.rerunWorkflow).toHaveBeenCalledWith({
        workflowId: successfulWorkflowId,
        fromFailed: false,
      });
      expect(response).toEqual({
        content: [
          {
            type: 'text',
            text: `New workflowId is ${newWorkflowId} and [View Workflow in CircleCI](https://app.circleci.com/pipelines/workflows/11111111-1111-1111-1111-111111111111)`,
          },
        ],
      });
    });
    it('should return the new workflowId and url to the user if requested to be rerun with fromFailed: undefined with a given workflowId', async () => {
      const mockCircleCIClient = setupMockClient('success');
      const controller = new AbortController();
      const response = await rerunWorkflow(
        {
          params: {
            workflowId: successfulWorkflowId,
            fromFailed: undefined,
          },
        },
        {
          signal: controller.signal,
        },
      );
      expect(mockCircleCIClient.workflows.rerunWorkflow).toHaveBeenCalledWith({
        workflowId: successfulWorkflowId,
        fromFailed: false,
      });
      expect(response).toEqual({
        content: [
          {
            type: 'text',
            text: `New workflowId is ${newWorkflowId} and [View Workflow in CircleCI](https://app.circleci.com/pipelines/workflows/11111111-1111-1111-1111-111111111111)`,
          },
        ],
      });
    });
  });

  describe('edge cases and errors', () => {
    it('should return an error if both workflowId and workflowURL are missing', async () => {
      setupMockClient(undefined);
      const controller = new AbortController();
      const response = await rerunWorkflow(
        { params: {} },
        { signal: controller.signal },
      );
      expect(response).toEqual({
        isError: true,
        content: [
          {
            type: 'text',
            text: 'workflowId is required and could not be determined from workflowURL.',
          },
        ],
      });
    });

    it('should return an error if workflow is not found', async () => {
      setupMockClient(undefined);
      const controller = new AbortController();
      const response = await rerunWorkflow(
        { params: { workflowId: 'nonexistent-id' } },
        { signal: controller.signal },
      );
      expect(response).toEqual({
        isError: true,
        content: [
          {
            type: 'text',
            text: 'Workflow not found',
          },
        ],
      });
    });

    it('should return an error if workflowURL is invalid and cannot extract workflowId', async () => {
      const getWorkflowIdFromURL = await import(
        '../../lib/getWorkflowIdFromURL.js'
      );
      const spy = vi
        .spyOn(getWorkflowIdFromURL, 'getWorkflowIdFromURL')
        .mockReturnValue(undefined);
      setupMockClient(undefined);
      const controller = new AbortController();
      const response = await rerunWorkflow(
        { params: { workflowURL: 'invalid-url' } },
        { signal: controller.signal },
      );
      expect(response).toEqual({
        isError: true,
        content: [
          {
            type: 'text',
            text: 'workflowId is required and could not be determined from workflowURL.',
          },
        ],
      });
      spy.mockRestore();
    });
  });
});

```

--------------------------------------------------------------------------------
/src/tools/runRollbackPipeline/handler.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { runRollbackPipeline } from './handler.js';
import * as clientModule from '../../clients/client.js';

vi.mock('../../clients/client.js');

describe('runRollbackPipeline handler', () => {
  const mockCircleCIClient = {
    deploys: {
      runRollbackPipeline: vi.fn(),
      fetchProjectDeploySettings: vi.fn(),
    },
    projects: {
      getProject: vi.fn(),
    },
  };

  const mockExtra = {
    signal: new AbortController().signal,
    requestId: 'test-id',
    sendNotification: vi.fn(),
    sendRequest: vi.fn(),
  };

  beforeEach(() => {
    vi.resetAllMocks();
    vi.spyOn(clientModule, 'getCircleCIClient').mockReturnValue(
      mockCircleCIClient as any,
    );
  });

  describe('successful rollback pipeline execution', () => {
    it('should initiate rollback pipeline with all required parameters', async () => {
      const mockRollbackResponse = {
        id: 'rollback-123',
        rollback_type: 'PIPELINE',
      };

      mockCircleCIClient.deploys.fetchProjectDeploySettings.mockResolvedValue({
        rollback_pipeline_definition_id: 'rollback-def-123',
      });
      mockCircleCIClient.deploys.runRollbackPipeline.mockResolvedValue(mockRollbackResponse);

      const args = {
        params: {
          projectID: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
          environmentName: 'production',
          componentName: 'frontend',
          currentVersion: 'v1.2.0',
          targetVersion: 'v1.1.0',
          namespace: 'web-app',
        },
      } as any;

      const response = await runRollbackPipeline(args, mockExtra);

      expect(response).toHaveProperty('content');
      expect(Array.isArray(response.content)).toBe(true);
      expect(response.content[0]).toHaveProperty('type', 'text');
      expect(response.content[0].text).toBe('Rollback initiated successfully. ID: rollback-123, Type: PIPELINE');
      
      expect(mockCircleCIClient.deploys.runRollbackPipeline).toHaveBeenCalledTimes(1);
      expect(mockCircleCIClient.deploys.runRollbackPipeline).toHaveBeenCalledWith({
        projectID: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
        rollbackRequest: {
          environment_name: 'production',
          component_name: 'frontend',
          current_version: 'v1.2.0',
          target_version: 'v1.1.0',
          namespace: 'web-app',
        },
      });
    });

    it('should initiate rollback pipeline with optional reason parameter', async () => {
      const mockRollbackResponse = {
        id: 'rollback-456',
        rollback_type: 'PIPELINE',
      };

      mockCircleCIClient.deploys.fetchProjectDeploySettings.mockResolvedValue({
        rollback_pipeline_definition_id: 'rollback-def-456',
      });
      mockCircleCIClient.deploys.runRollbackPipeline.mockResolvedValue(mockRollbackResponse);

      const args = {
        params: {
          projectID: 'b2c3d4e5-f6g7-8901-bcde-fg2345678901',
          environmentName: 'staging',
          componentName: 'backend',
          currentVersion: 'v2.1.0',
          targetVersion: 'v2.0.0',
          namespace: 'api-service',
          reason: 'Critical bug fix required',
        },
      } as any;

      const response = await runRollbackPipeline(args, mockExtra);

      expect(response).toHaveProperty('content');
      expect(response.content[0].text).toBe('Rollback initiated successfully. ID: rollback-456, Type: PIPELINE');
      
      expect(mockCircleCIClient.deploys.runRollbackPipeline).toHaveBeenCalledWith({
        projectID: 'b2c3d4e5-f6g7-8901-bcde-fg2345678901',
        rollbackRequest: {
          environment_name: 'staging',
          component_name: 'backend',
          current_version: 'v2.1.0',
          target_version: 'v2.0.0',
          namespace: 'api-service',
          reason: 'Critical bug fix required',
        },
      });
    });

    it('should initiate rollback pipeline with optional parameters object', async () => {
      const mockRollbackResponse = {
        id: 'rollback-789',
        rollback_type: 'PIPELINE',
      };

      mockCircleCIClient.deploys.fetchProjectDeploySettings.mockResolvedValue({
        rollback_pipeline_definition_id: 'rollback-def-789',
      });
      mockCircleCIClient.deploys.runRollbackPipeline.mockResolvedValue(mockRollbackResponse);

      const args = {
        params: {
          projectID: 'c3d4e5f6-g7h8-9012-cdef-gh3456789012',
          environmentName: 'production',
          componentName: 'database',
          currentVersion: 'v3.2.0',
          targetVersion: 'v3.1.0',
          namespace: 'db-cluster',
          reason: 'Performance regression',
          parameters: {
            skip_migration: true,
            notify_team: 'devops',
          },
        },
      } as any;

      const response = await runRollbackPipeline(args, mockExtra);

      expect(response).toHaveProperty('content');
      expect(response.content[0].text).toBe('Rollback initiated successfully. ID: rollback-789, Type: PIPELINE');
      
      expect(mockCircleCIClient.deploys.runRollbackPipeline).toHaveBeenCalledWith({
        projectID: 'c3d4e5f6-g7h8-9012-cdef-gh3456789012',
        rollbackRequest: {
          environment_name: 'production',
          component_name: 'database',
          current_version: 'v3.2.0',
          target_version: 'v3.1.0',
          namespace: 'db-cluster',
          reason: 'Performance regression',
          parameters: {
            skip_migration: true,
            notify_team: 'devops',
          },
        },
      });
    });

    it('should initiate rollback pipeline using projectSlug', async () => {
      const mockRollbackResponse = {
        id: 'rollback-slug-123',
        rollback_type: 'PIPELINE',
      };

      mockCircleCIClient.projects.getProject.mockResolvedValue({
        id: 'resolved-project-id-123',
        organization_id: 'org-id-123',
      });
      mockCircleCIClient.deploys.fetchProjectDeploySettings.mockResolvedValue({
        rollback_pipeline_definition_id: 'rollback-def-slug-123',
      });
      mockCircleCIClient.deploys.runRollbackPipeline.mockResolvedValue(mockRollbackResponse);

      const args = {
        params: {
          projectSlug: 'gh/organization/project',
          environmentName: 'production',
          componentName: 'frontend',
          currentVersion: 'v1.2.0',
          targetVersion: 'v1.1.0',
          namespace: 'web-app',
        },
      } as any;

      const response = await runRollbackPipeline(args, mockExtra);

      expect(response).toHaveProperty('content');
      expect(Array.isArray(response.content)).toBe(true);
      expect(response.content[0]).toHaveProperty('type', 'text');
      expect(response.content[0].text).toBe('Rollback initiated successfully. ID: rollback-slug-123, Type: PIPELINE');
      
      expect(mockCircleCIClient.projects.getProject).toHaveBeenCalledWith({
        projectSlug: 'gh/organization/project',
      });
      expect(mockCircleCIClient.deploys.runRollbackPipeline).toHaveBeenCalledWith({
        projectID: 'resolved-project-id-123',
        rollbackRequest: {
          environment_name: 'production',
          component_name: 'frontend',
          current_version: 'v1.2.0',
          target_version: 'v1.1.0',
          namespace: 'web-app',
        },
      });
    });
  });

  describe('error handling', () => {
    it('should return error when API call fails with Error object', async () => {
      const errorMessage = 'Rollback pipeline not configured for this project';
      mockCircleCIClient.deploys.fetchProjectDeploySettings.mockResolvedValue({
        rollback_pipeline_definition_id: 'rollback-def-error',
      });
      mockCircleCIClient.deploys.runRollbackPipeline.mockRejectedValue(new Error(errorMessage));

      const args = {
        params: {
          projectID: 'e5f6g7h8-i9j0-1234-efgh-ij5678901234',
          environment_name: 'production',
          componentName: 'frontend',
          currentVersion: 'v2.0.0',
          targetVersion: 'v1.9.0',
          namespace: 'app',
        },
      } as any;

      const response = await runRollbackPipeline(args, mockExtra);

      expect(response).toHaveProperty('content');
      expect(Array.isArray(response.content)).toBe(true);
      expect(response.content[0]).toHaveProperty('type', 'text');
      expect(response.content[0].text).toContain('Failed to initiate rollback:');
      expect(response.content[0].text).toContain('Rollback pipeline not configured for this project');
    });

    it('should return error when API call fails with non-Error object', async () => {
      mockCircleCIClient.deploys.fetchProjectDeploySettings.mockResolvedValue({
        rollback_pipeline_definition_id: 'rollback-def-error2',
      });
      mockCircleCIClient.deploys.runRollbackPipeline.mockRejectedValue('String error');

      const args = {
        params: {
          projectID: 'f6g7h8i9-j0k1-2345-fghi-jk6789012345',
          environment_name: 'staging',
          component_name: 'backend',
          current_version: 'v3.0.0',
          target_version: 'v2.9.0',
          namespace: 'api',
        },
      } as any;

      const response = await runRollbackPipeline(args, mockExtra);

      expect(response).toHaveProperty('content');
      expect(response.content[0].text).toContain('Failed to initiate rollback:');
      expect(response.content[0].text).toContain('Unknown error');
    });

    it('should return error when projectSlug resolution fails', async () => {
      const errorMessage = 'Project not found';
      mockCircleCIClient.projects.getProject.mockRejectedValue(new Error(errorMessage));

      const args = {
        params: {
          projectSlug: 'gh/invalid/project',
          environmentName: 'production',
          componentName: 'frontend',
          currentVersion: 'v1.2.0',
          targetVersion: 'v1.1.0',
          namespace: 'web-app',
        },
      } as any;

      const response = await runRollbackPipeline(args, mockExtra);

      expect(response).toHaveProperty('content');
      expect(response.content[0].text).toContain('Failed to resolve project information for gh/invalid/project');
      expect(response.content[0].text).toContain('Project not found');
    });

    it('should return error when neither projectSlug nor projectID provided', async () => {
      const args = {
        params: {
          environmentName: 'production',
          componentName: 'frontend',
          currentVersion: 'v1.2.0',
          targetVersion: 'v1.1.0',
          namespace: 'web-app',
        },
      } as any;

      const response = await runRollbackPipeline(args, mockExtra);

      expect(response).toHaveProperty('content');
      expect(response.content[0].text).toContain('Either projectSlug or projectID must be provided');
    });

    it('should return the appropriate message when no rollback pipeline definition is configured', async () => {
      mockCircleCIClient.deploys.fetchProjectDeploySettings.mockResolvedValue({
        rollback_pipeline_definition_id: null,
      });

      const args = {
        params: {
          projectID: 'test-project-id',
          environmentName: 'production',
          componentName: 'frontend',
          currentVersion: 'v1.2.0',
          targetVersion: 'v1.1.0',
          namespace: 'web-app',
        },
      } as any;

      const response = await runRollbackPipeline(args, mockExtra);

      expect(response).toHaveProperty('content');
      expect(response.content[0].text).toContain('No rollback pipeline definition found for this project');
      expect(response.content[0].text).toContain('https://circleci.com/docs/deploy/rollback-a-project-using-the-rollback-pipeline/');
    });
  });
});

```
Page 2/3FirstPrevNextLast