chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import type {
|
||||
GitLabApproveMergeRequestParams,
|
||||
GitLabApproveMergeRequestResponse,
|
||||
} from '@/tools/gitlab/types'
|
||||
import { getGitLabApiBase } from '@/tools/gitlab/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const gitlabApproveMergeRequestTool: ToolConfig<
|
||||
GitLabApproveMergeRequestParams,
|
||||
GitLabApproveMergeRequestResponse
|
||||
> = {
|
||||
id: 'gitlab_approve_merge_request',
|
||||
name: 'GitLab Approve Merge Request',
|
||||
description: 'Approve a GitLab merge request',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'GitLab Personal Access Token',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or URL-encoded path',
|
||||
},
|
||||
mergeRequestIid: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Merge request internal ID (IID)',
|
||||
},
|
||||
sha: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'HEAD SHA of the merge request to approve',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const encodedId = encodeURIComponent(String(params.projectId).trim())
|
||||
return `${getGitLabApiBase(params.host)}/projects/${encodedId}/merge_requests/${params.mergeRequestIid}/approve`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
'PRIVATE-TOKEN': params.accessToken,
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {}
|
||||
|
||||
if (params.sha) body.sha = params.sha
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return {
|
||||
success: false,
|
||||
error: `GitLab API error: ${response.status} ${errorText}`,
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
approvalsRequired: data.approvals_required ?? null,
|
||||
approvalsLeft: data.approvals_left ?? null,
|
||||
approvedBy: data.approved_by ?? [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
approvalsRequired: {
|
||||
type: 'number',
|
||||
description: 'Number of approvals required',
|
||||
},
|
||||
approvalsLeft: {
|
||||
type: 'number',
|
||||
description: 'Number of approvals still needed',
|
||||
},
|
||||
approvedBy: {
|
||||
type: 'array',
|
||||
description: 'List of approvers',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { GitLabCancelPipelineParams, GitLabCancelPipelineResponse } from '@/tools/gitlab/types'
|
||||
import { getGitLabApiBase } from '@/tools/gitlab/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const gitlabCancelPipelineTool: ToolConfig<
|
||||
GitLabCancelPipelineParams,
|
||||
GitLabCancelPipelineResponse
|
||||
> = {
|
||||
id: 'gitlab_cancel_pipeline',
|
||||
name: 'GitLab Cancel Pipeline',
|
||||
description: 'Cancel a running GitLab pipeline',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'GitLab Personal Access Token',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or URL-encoded path',
|
||||
},
|
||||
pipelineId: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Pipeline ID',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const encodedId = encodeURIComponent(String(params.projectId).trim())
|
||||
return `${getGitLabApiBase(params.host)}/projects/${encodedId}/pipelines/${params.pipelineId}/cancel`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'PRIVATE-TOKEN': params.accessToken,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return {
|
||||
success: false,
|
||||
error: `GitLab API error: ${response.status} ${errorText}`,
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const pipeline = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
pipeline,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
pipeline: {
|
||||
type: 'object',
|
||||
description: 'The cancelled GitLab pipeline',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import type {
|
||||
GitLabCompareBranchesParams,
|
||||
GitLabCompareBranchesResponse,
|
||||
} from '@/tools/gitlab/types'
|
||||
import { getGitLabApiBase } from '@/tools/gitlab/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const gitlabCompareBranchesTool: ToolConfig<
|
||||
GitLabCompareBranchesParams,
|
||||
GitLabCompareBranchesResponse
|
||||
> = {
|
||||
id: 'gitlab_compare_branches',
|
||||
name: 'GitLab Compare Branches',
|
||||
description: 'Compare two branches, tags, or commits in a GitLab project repository',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'GitLab Personal Access Token',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or URL-encoded path',
|
||||
},
|
||||
from: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Commit SHA or branch/tag name to compare from',
|
||||
},
|
||||
to: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Commit SHA or branch/tag name to compare to',
|
||||
},
|
||||
straight: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Compare directly from..to instead of using the merge base (defaults to false)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const encodedId = encodeURIComponent(String(params.projectId).trim())
|
||||
const queryParams = new URLSearchParams()
|
||||
queryParams.append('from', params.from)
|
||||
queryParams.append('to', params.to)
|
||||
if (params.straight) queryParams.append('straight', 'true')
|
||||
|
||||
return `${getGitLabApiBase(params.host)}/projects/${encodedId}/repository/compare?${queryParams.toString()}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
'PRIVATE-TOKEN': params.accessToken,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return {
|
||||
success: false,
|
||||
error: `GitLab API error: ${response.status} ${errorText}`,
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
commit: data.commit ?? null,
|
||||
commits: data.commits ?? [],
|
||||
diffs: data.diffs ?? [],
|
||||
compareTimeout: data.compare_timeout ?? null,
|
||||
compareSameRef: data.compare_same_ref ?? null,
|
||||
webUrl: data.web_url ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
commit: {
|
||||
type: 'object',
|
||||
description: 'The latest commit in the comparison',
|
||||
},
|
||||
commits: {
|
||||
type: 'array',
|
||||
description: 'Commits between the two references',
|
||||
},
|
||||
diffs: {
|
||||
type: 'array',
|
||||
description: 'File diffs between the two references',
|
||||
},
|
||||
compareTimeout: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the comparison exceeded size limits or timed out',
|
||||
},
|
||||
compareSameRef: {
|
||||
type: 'boolean',
|
||||
description: 'Whether both references point to the same commit',
|
||||
},
|
||||
webUrl: {
|
||||
type: 'string',
|
||||
description: 'The web URL for viewing the comparison',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import type { GitLabCreateBranchParams, GitLabCreateBranchResponse } from '@/tools/gitlab/types'
|
||||
import { getGitLabApiBase } from '@/tools/gitlab/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const gitlabCreateBranchTool: ToolConfig<
|
||||
GitLabCreateBranchParams,
|
||||
GitLabCreateBranchResponse
|
||||
> = {
|
||||
id: 'gitlab_create_branch',
|
||||
name: 'GitLab Create Branch',
|
||||
description: 'Create a new branch in a GitLab project repository',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'GitLab Personal Access Token',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or URL-encoded path',
|
||||
},
|
||||
branch: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Name of the new branch',
|
||||
},
|
||||
ref: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Source branch/tag/SHA',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const encodedId = encodeURIComponent(String(params.projectId).trim())
|
||||
const queryParams = new URLSearchParams()
|
||||
queryParams.append('branch', String(params.branch))
|
||||
queryParams.append('ref', String(params.ref))
|
||||
return `${getGitLabApiBase(params.host)}/projects/${encodedId}/repository/branches?${queryParams.toString()}`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'PRIVATE-TOKEN': params.accessToken,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return {
|
||||
success: false,
|
||||
error: `GitLab API error: ${response.status} ${errorText}`,
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
name: data.name ?? null,
|
||||
webUrl: data.web_url ?? null,
|
||||
protected: data.protected ?? null,
|
||||
commit: data.commit ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'The created branch name',
|
||||
},
|
||||
webUrl: {
|
||||
type: 'string',
|
||||
description: 'The web URL of the branch',
|
||||
},
|
||||
protected: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the branch is protected',
|
||||
},
|
||||
commit: {
|
||||
type: 'object',
|
||||
description: 'The commit the branch points to',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import type { GitLabCreateFileParams, GitLabCreateFileResponse } from '@/tools/gitlab/types'
|
||||
import { getGitLabApiBase } from '@/tools/gitlab/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const gitlabCreateFileTool: ToolConfig<GitLabCreateFileParams, GitLabCreateFileResponse> = {
|
||||
id: 'gitlab_create_file',
|
||||
name: 'GitLab Create File',
|
||||
description: 'Create a new file in a GitLab project repository',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'GitLab Personal Access Token',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or URL-encoded path',
|
||||
},
|
||||
filePath: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Path to the file in the repository',
|
||||
},
|
||||
branch: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Branch to commit the new file to',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'File content',
|
||||
},
|
||||
commitMessage: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Commit message',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const encodedId = encodeURIComponent(String(params.projectId).trim())
|
||||
const encodedPath = encodeURIComponent(String(params.filePath))
|
||||
return `${getGitLabApiBase(params.host)}/projects/${encodedId}/repository/files/${encodedPath}`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
'PRIVATE-TOKEN': params.accessToken,
|
||||
}),
|
||||
body: (params) => ({
|
||||
branch: params.branch,
|
||||
content: params.content,
|
||||
commit_message: params.commitMessage,
|
||||
encoding: 'text',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return {
|
||||
success: false,
|
||||
error: `GitLab API error: ${response.status} ${errorText}`,
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
filePath: data.file_path ?? null,
|
||||
branch: data.branch ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
filePath: {
|
||||
type: 'string',
|
||||
description: 'The created file path',
|
||||
},
|
||||
branch: {
|
||||
type: 'string',
|
||||
description: 'The branch the file was committed to',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import type { GitLabCreateIssueParams, GitLabCreateIssueResponse } from '@/tools/gitlab/types'
|
||||
import { getGitLabApiBase } from '@/tools/gitlab/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const gitlabCreateIssueTool: ToolConfig<GitLabCreateIssueParams, GitLabCreateIssueResponse> =
|
||||
{
|
||||
id: 'gitlab_create_issue',
|
||||
name: 'GitLab Create Issue',
|
||||
description: 'Create a new issue in a GitLab project',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'GitLab Personal Access Token',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or URL-encoded path',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Issue title',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Issue description (Markdown supported)',
|
||||
},
|
||||
labels: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated list of label names',
|
||||
},
|
||||
assigneeIds: {
|
||||
type: 'array',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Array of user IDs to assign',
|
||||
},
|
||||
milestoneId: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Milestone ID to assign',
|
||||
},
|
||||
dueDate: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Due date in YYYY-MM-DD format',
|
||||
},
|
||||
confidential: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether the issue is confidential',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const encodedId = encodeURIComponent(String(params.projectId).trim())
|
||||
return `${getGitLabApiBase(params.host)}/projects/${encodedId}/issues`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
'PRIVATE-TOKEN': params.accessToken,
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, any> = {
|
||||
title: params.title,
|
||||
}
|
||||
|
||||
if (params.description) body.description = params.description
|
||||
if (params.labels) body.labels = params.labels
|
||||
if (params.assigneeIds) body.assignee_ids = params.assigneeIds
|
||||
if (params.milestoneId) body.milestone_id = params.milestoneId
|
||||
if (params.dueDate) body.due_date = params.dueDate
|
||||
if (params.confidential !== undefined) body.confidential = params.confidential
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return {
|
||||
success: false,
|
||||
error: `GitLab API error: ${response.status} ${errorText}`,
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const issue = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
issue,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
issue: {
|
||||
type: 'object',
|
||||
description: 'The created GitLab issue',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import type { GitLabCreateIssueNoteParams, GitLabCreateNoteResponse } from '@/tools/gitlab/types'
|
||||
import { getGitLabApiBase } from '@/tools/gitlab/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const gitlabCreateIssueNoteTool: ToolConfig<
|
||||
GitLabCreateIssueNoteParams,
|
||||
GitLabCreateNoteResponse
|
||||
> = {
|
||||
id: 'gitlab_create_issue_note',
|
||||
name: 'GitLab Create Issue Comment',
|
||||
description: 'Add a comment to a GitLab issue',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'GitLab Personal Access Token',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or URL-encoded path',
|
||||
},
|
||||
issueIid: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Issue internal ID (IID)',
|
||||
},
|
||||
body: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comment body (Markdown supported)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const encodedId = encodeURIComponent(String(params.projectId).trim())
|
||||
return `${getGitLabApiBase(params.host)}/projects/${encodedId}/issues/${params.issueIid}/notes`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
'PRIVATE-TOKEN': params.accessToken,
|
||||
}),
|
||||
body: (params) => ({
|
||||
body: params.body,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return {
|
||||
success: false,
|
||||
error: `GitLab API error: ${response.status} ${errorText}`,
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const note = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
note,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
note: {
|
||||
type: 'object',
|
||||
description: 'The created comment',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import type {
|
||||
GitLabCreateMergeRequestParams,
|
||||
GitLabCreateMergeRequestResponse,
|
||||
} from '@/tools/gitlab/types'
|
||||
import { getGitLabApiBase } from '@/tools/gitlab/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const gitlabCreateMergeRequestTool: ToolConfig<
|
||||
GitLabCreateMergeRequestParams,
|
||||
GitLabCreateMergeRequestResponse
|
||||
> = {
|
||||
id: 'gitlab_create_merge_request',
|
||||
name: 'GitLab Create Merge Request',
|
||||
description: 'Create a new merge request in a GitLab project',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'GitLab Personal Access Token',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or URL-encoded path',
|
||||
},
|
||||
sourceBranch: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Source branch name',
|
||||
},
|
||||
targetBranch: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Target branch name',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Merge request title',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Merge request description (Markdown supported)',
|
||||
},
|
||||
labels: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated list of label names',
|
||||
},
|
||||
assigneeIds: {
|
||||
type: 'array',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Array of user IDs to assign',
|
||||
},
|
||||
milestoneId: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Milestone ID to assign',
|
||||
},
|
||||
removeSourceBranch: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Delete source branch after merge',
|
||||
},
|
||||
squash: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Squash commits on merge',
|
||||
},
|
||||
draft: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Mark as draft (work in progress)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const encodedId = encodeURIComponent(String(params.projectId).trim())
|
||||
return `${getGitLabApiBase(params.host)}/projects/${encodedId}/merge_requests`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
'PRIVATE-TOKEN': params.accessToken,
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, any> = {
|
||||
source_branch: params.sourceBranch,
|
||||
target_branch: params.targetBranch,
|
||||
title: params.title,
|
||||
}
|
||||
|
||||
if (params.description) body.description = params.description
|
||||
if (params.labels) body.labels = params.labels
|
||||
if (params.assigneeIds && params.assigneeIds.length > 0)
|
||||
body.assignee_ids = params.assigneeIds
|
||||
if (params.milestoneId) body.milestone_id = params.milestoneId
|
||||
if (params.removeSourceBranch !== undefined)
|
||||
body.remove_source_branch = params.removeSourceBranch
|
||||
if (params.squash !== undefined) body.squash = params.squash
|
||||
if (params.draft !== undefined) body.draft = params.draft
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return {
|
||||
success: false,
|
||||
error: `GitLab API error: ${response.status} ${errorText}`,
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const mergeRequest = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
mergeRequest,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
mergeRequest: {
|
||||
type: 'object',
|
||||
description: 'The created GitLab merge request',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import type {
|
||||
GitLabCreateMergeRequestNoteParams,
|
||||
GitLabCreateNoteResponse,
|
||||
} from '@/tools/gitlab/types'
|
||||
import { getGitLabApiBase } from '@/tools/gitlab/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const gitlabCreateMergeRequestNoteTool: ToolConfig<
|
||||
GitLabCreateMergeRequestNoteParams,
|
||||
GitLabCreateNoteResponse
|
||||
> = {
|
||||
id: 'gitlab_create_merge_request_note',
|
||||
name: 'GitLab Create Merge Request Comment',
|
||||
description: 'Add a comment to a GitLab merge request',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'GitLab Personal Access Token',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or URL-encoded path',
|
||||
},
|
||||
mergeRequestIid: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Merge request internal ID (IID)',
|
||||
},
|
||||
body: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comment body (Markdown supported)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const encodedId = encodeURIComponent(String(params.projectId).trim())
|
||||
return `${getGitLabApiBase(params.host)}/projects/${encodedId}/merge_requests/${params.mergeRequestIid}/notes`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
'PRIVATE-TOKEN': params.accessToken,
|
||||
}),
|
||||
body: (params) => ({
|
||||
body: params.body,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return {
|
||||
success: false,
|
||||
error: `GitLab API error: ${response.status} ${errorText}`,
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const note = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
note,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
note: {
|
||||
type: 'object',
|
||||
description: 'The created comment',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import type { GitLabCreatePipelineParams, GitLabCreatePipelineResponse } from '@/tools/gitlab/types'
|
||||
import { getGitLabApiBase } from '@/tools/gitlab/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const gitlabCreatePipelineTool: ToolConfig<
|
||||
GitLabCreatePipelineParams,
|
||||
GitLabCreatePipelineResponse
|
||||
> = {
|
||||
id: 'gitlab_create_pipeline',
|
||||
name: 'GitLab Create Pipeline',
|
||||
description: 'Trigger a new pipeline in a GitLab project',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'GitLab Personal Access Token',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or URL-encoded path',
|
||||
},
|
||||
ref: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Branch or tag to run the pipeline on',
|
||||
},
|
||||
variables: {
|
||||
type: 'array',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Array of variables for the pipeline (each with key, value, and optional variable_type)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const encodedId = encodeURIComponent(String(params.projectId).trim())
|
||||
return `${getGitLabApiBase(params.host)}/projects/${encodedId}/pipeline`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
'PRIVATE-TOKEN': params.accessToken,
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, any> = {
|
||||
ref: params.ref,
|
||||
}
|
||||
|
||||
if (params.variables && params.variables.length > 0) {
|
||||
body.variables = params.variables
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return {
|
||||
success: false,
|
||||
error: `GitLab API error: ${response.status} ${errorText}`,
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const pipeline = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
pipeline,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
pipeline: {
|
||||
type: 'object',
|
||||
description: 'The created GitLab pipeline',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import type { GitLabCreateReleaseParams, GitLabCreateReleaseResponse } from '@/tools/gitlab/types'
|
||||
import { getGitLabApiBase } from '@/tools/gitlab/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const gitlabCreateReleaseTool: ToolConfig<
|
||||
GitLabCreateReleaseParams,
|
||||
GitLabCreateReleaseResponse
|
||||
> = {
|
||||
id: 'gitlab_create_release',
|
||||
name: 'GitLab Create Release',
|
||||
description: 'Create a new release in a GitLab project',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'GitLab Personal Access Token',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or URL-encoded path',
|
||||
},
|
||||
tagName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The Git tag for the release',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The release name',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Release description/notes (Markdown supported)',
|
||||
},
|
||||
ref: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Commit SHA, branch, or tag to create the tag from if it does not already exist',
|
||||
},
|
||||
releasedAt: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ISO 8601 date for an upcoming or historical release',
|
||||
},
|
||||
milestones: {
|
||||
type: 'array',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Array of milestone titles to associate with the release',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const encodedId = encodeURIComponent(String(params.projectId).trim())
|
||||
return `${getGitLabApiBase(params.host)}/projects/${encodedId}/releases`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
'PRIVATE-TOKEN': params.accessToken,
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {
|
||||
tag_name: params.tagName,
|
||||
}
|
||||
|
||||
if (params.name) body.name = params.name
|
||||
if (params.description) body.description = params.description
|
||||
if (params.ref) body.ref = params.ref
|
||||
if (params.releasedAt) body.released_at = params.releasedAt
|
||||
if (params.milestones && params.milestones.length > 0) body.milestones = params.milestones
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return {
|
||||
success: false,
|
||||
error: `GitLab API error: ${response.status} ${errorText}`,
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const release = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
release,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
release: {
|
||||
type: 'object',
|
||||
description: 'The created GitLab release',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { GitLabDeleteBranchParams, GitLabDeleteBranchResponse } from '@/tools/gitlab/types'
|
||||
import { getGitLabApiBase } from '@/tools/gitlab/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const gitlabDeleteBranchTool: ToolConfig<
|
||||
GitLabDeleteBranchParams,
|
||||
GitLabDeleteBranchResponse
|
||||
> = {
|
||||
id: 'gitlab_delete_branch',
|
||||
name: 'GitLab Delete Branch',
|
||||
description: 'Delete a branch from a GitLab project repository',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'GitLab Personal Access Token',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or URL-encoded path',
|
||||
},
|
||||
branch: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Name of the branch to delete',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const encodedId = encodeURIComponent(String(params.projectId).trim())
|
||||
const encodedBranch = encodeURIComponent(String(params.branch).trim())
|
||||
return `${getGitLabApiBase(params.host)}/projects/${encodedId}/repository/branches/${encodedBranch}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
'PRIVATE-TOKEN': params.accessToken,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return {
|
||||
success: false,
|
||||
error: `GitLab API error: ${response.status} ${errorText}`,
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the branch was deleted successfully',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { GitLabDeleteIssueParams, GitLabDeleteIssueResponse } from '@/tools/gitlab/types'
|
||||
import { getGitLabApiBase } from '@/tools/gitlab/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const gitlabDeleteIssueTool: ToolConfig<GitLabDeleteIssueParams, GitLabDeleteIssueResponse> =
|
||||
{
|
||||
id: 'gitlab_delete_issue',
|
||||
name: 'GitLab Delete Issue',
|
||||
description: 'Delete an issue from a GitLab project',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'GitLab Personal Access Token',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or URL-encoded path',
|
||||
},
|
||||
issueIid: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Issue internal ID (IID)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const encodedId = encodeURIComponent(String(params.projectId).trim())
|
||||
return `${getGitLabApiBase(params.host)}/projects/${encodedId}/issues/${params.issueIid}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
'PRIVATE-TOKEN': params.accessToken,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return {
|
||||
success: false,
|
||||
error: `GitLab API error: ${response.status} ${errorText}`,
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the issue was deleted successfully',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import type { GitLabGetFileParams, GitLabGetFileResponse } from '@/tools/gitlab/types'
|
||||
import { getGitLabApiBase } from '@/tools/gitlab/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const gitlabGetFileTool: ToolConfig<GitLabGetFileParams, GitLabGetFileResponse> = {
|
||||
id: 'gitlab_get_file',
|
||||
name: 'GitLab Get File',
|
||||
description: 'Get the contents of a file from a GitLab project repository',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'GitLab Personal Access Token',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or URL-encoded path',
|
||||
},
|
||||
filePath: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Path to the file in the repository',
|
||||
},
|
||||
ref: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Branch, tag, or commit SHA',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const encodedId = encodeURIComponent(String(params.projectId).trim())
|
||||
const encodedPath = encodeURIComponent(String(params.filePath))
|
||||
const ref = encodeURIComponent(String(params.ref))
|
||||
return `${getGitLabApiBase(params.host)}/projects/${encodedId}/repository/files/${encodedPath}?ref=${ref}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
'PRIVATE-TOKEN': params.accessToken,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return {
|
||||
success: false,
|
||||
error: `GitLab API error: ${response.status} ${errorText}`,
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const decoded = Buffer.from(data.content ?? '', 'base64').toString('utf-8')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
filePath: data.file_path ?? null,
|
||||
fileName: data.file_name ?? null,
|
||||
size: data.size ?? null,
|
||||
ref: data.ref ?? null,
|
||||
blobId: data.blob_id ?? null,
|
||||
lastCommitId: data.last_commit_id ?? null,
|
||||
content: decoded,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
filePath: {
|
||||
type: 'string',
|
||||
description: 'The file path',
|
||||
},
|
||||
fileName: {
|
||||
type: 'string',
|
||||
description: 'The file name',
|
||||
},
|
||||
size: {
|
||||
type: 'number',
|
||||
description: 'The file size in bytes',
|
||||
},
|
||||
ref: {
|
||||
type: 'string',
|
||||
description: 'The branch, tag, or commit SHA',
|
||||
},
|
||||
blobId: {
|
||||
type: 'string',
|
||||
description: 'The blob ID',
|
||||
},
|
||||
lastCommitId: {
|
||||
type: 'string',
|
||||
description: 'The last commit ID that modified the file',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'The decoded file content',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { GitLabGetIssueParams, GitLabGetIssueResponse } from '@/tools/gitlab/types'
|
||||
import { getGitLabApiBase } from '@/tools/gitlab/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const gitlabGetIssueTool: ToolConfig<GitLabGetIssueParams, GitLabGetIssueResponse> = {
|
||||
id: 'gitlab_get_issue',
|
||||
name: 'GitLab Get Issue',
|
||||
description: 'Get details of a specific GitLab issue',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'GitLab Personal Access Token',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or URL-encoded path',
|
||||
},
|
||||
issueIid: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Issue number within the project (the # shown in GitLab UI)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const encodedId = encodeURIComponent(String(params.projectId).trim())
|
||||
return `${getGitLabApiBase(params.host)}/projects/${encodedId}/issues/${params.issueIid}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
'PRIVATE-TOKEN': params.accessToken,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return {
|
||||
success: false,
|
||||
error: `GitLab API error: ${response.status} ${errorText}`,
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const issue = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
issue,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
issue: {
|
||||
type: 'object',
|
||||
description: 'The GitLab issue details',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { GitLabGetJobLogParams, GitLabGetJobLogResponse } from '@/tools/gitlab/types'
|
||||
import { getGitLabApiBase } from '@/tools/gitlab/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const gitlabGetJobLogTool: ToolConfig<GitLabGetJobLogParams, GitLabGetJobLogResponse> = {
|
||||
id: 'gitlab_get_job_log',
|
||||
name: 'GitLab Get Job Log',
|
||||
description: 'Get the log (trace) of a GitLab job',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'GitLab Personal Access Token',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or URL-encoded path',
|
||||
},
|
||||
jobId: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Job ID',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const encodedId = encodeURIComponent(String(params.projectId).trim())
|
||||
return `${getGitLabApiBase(params.host)}/projects/${encodedId}/jobs/${params.jobId}/trace`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
'PRIVATE-TOKEN': params.accessToken,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return {
|
||||
success: false,
|
||||
error: `GitLab API error: ${response.status} ${errorText}`,
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const log = await response.text()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
log,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
log: {
|
||||
type: 'string',
|
||||
description: 'The job log (trace) output',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import type {
|
||||
GitLabGetMergeRequestParams,
|
||||
GitLabGetMergeRequestResponse,
|
||||
} from '@/tools/gitlab/types'
|
||||
import { getGitLabApiBase } from '@/tools/gitlab/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const gitlabGetMergeRequestTool: ToolConfig<
|
||||
GitLabGetMergeRequestParams,
|
||||
GitLabGetMergeRequestResponse
|
||||
> = {
|
||||
id: 'gitlab_get_merge_request',
|
||||
name: 'GitLab Get Merge Request',
|
||||
description: 'Get details of a specific GitLab merge request',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'GitLab Personal Access Token',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or URL-encoded path',
|
||||
},
|
||||
mergeRequestIid: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Merge request internal ID (IID)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const encodedId = encodeURIComponent(String(params.projectId).trim())
|
||||
return `${getGitLabApiBase(params.host)}/projects/${encodedId}/merge_requests/${params.mergeRequestIid}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
'PRIVATE-TOKEN': params.accessToken,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return {
|
||||
success: false,
|
||||
error: `GitLab API error: ${response.status} ${errorText}`,
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const mergeRequest = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
mergeRequest,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
mergeRequest: {
|
||||
type: 'object',
|
||||
description: 'The GitLab merge request details',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import type {
|
||||
GitLabGetMergeRequestChangesParams,
|
||||
GitLabGetMergeRequestChangesResponse,
|
||||
} from '@/tools/gitlab/types'
|
||||
import { getGitLabApiBase } from '@/tools/gitlab/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const gitlabGetMergeRequestChangesTool: ToolConfig<
|
||||
GitLabGetMergeRequestChangesParams,
|
||||
GitLabGetMergeRequestChangesResponse
|
||||
> = {
|
||||
id: 'gitlab_get_merge_request_changes',
|
||||
name: 'GitLab Get Merge Request Changes',
|
||||
description: 'Get the file changes (diffs) of a GitLab merge request',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'GitLab Personal Access Token',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or URL-encoded path',
|
||||
},
|
||||
mergeRequestIid: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Merge request internal ID (IID)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
/**
|
||||
* Uses the `/diffs` endpoint (the `/changes` endpoint was deprecated in
|
||||
* GitLab 15.7, with removal scheduled for API v5). `/diffs` returns the diff array directly
|
||||
* and is paginated; we request the max page size (100) to return the changes
|
||||
* in a single call, which covers the vast majority of merge requests.
|
||||
*/
|
||||
url: (params) => {
|
||||
const encodedId = encodeURIComponent(String(params.projectId).trim())
|
||||
return `${getGitLabApiBase(params.host)}/projects/${encodedId}/merge_requests/${params.mergeRequestIid}/diffs?per_page=100`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
'PRIVATE-TOKEN': params.accessToken,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response, params) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return {
|
||||
success: false,
|
||||
error: `GitLab API error: ${response.status} ${errorText}`,
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const changes = Array.isArray(data) ? data : []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
mergeRequestIid: params?.mergeRequestIid ?? null,
|
||||
changes,
|
||||
changesCount: changes.length,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
mergeRequestIid: {
|
||||
type: 'number',
|
||||
description: 'The merge request internal ID (IID)',
|
||||
},
|
||||
changes: {
|
||||
type: 'array',
|
||||
description: 'List of file changes (diffs)',
|
||||
},
|
||||
changesCount: {
|
||||
type: 'number',
|
||||
description: 'Number of changed files returned',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { GitLabGetPipelineParams, GitLabGetPipelineResponse } from '@/tools/gitlab/types'
|
||||
import { getGitLabApiBase } from '@/tools/gitlab/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const gitlabGetPipelineTool: ToolConfig<GitLabGetPipelineParams, GitLabGetPipelineResponse> =
|
||||
{
|
||||
id: 'gitlab_get_pipeline',
|
||||
name: 'GitLab Get Pipeline',
|
||||
description: 'Get details of a specific GitLab pipeline',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'GitLab Personal Access Token',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or URL-encoded path',
|
||||
},
|
||||
pipelineId: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Pipeline ID',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const encodedId = encodeURIComponent(String(params.projectId).trim())
|
||||
return `${getGitLabApiBase(params.host)}/projects/${encodedId}/pipelines/${params.pipelineId}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
'PRIVATE-TOKEN': params.accessToken,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return {
|
||||
success: false,
|
||||
error: `GitLab API error: ${response.status} ${errorText}`,
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const pipeline = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
pipeline,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
pipeline: {
|
||||
type: 'object',
|
||||
description: 'The GitLab pipeline details',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { GitLabGetProjectParams, GitLabGetProjectResponse } from '@/tools/gitlab/types'
|
||||
import { getGitLabApiBase } from '@/tools/gitlab/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const gitlabGetProjectTool: ToolConfig<GitLabGetProjectParams, GitLabGetProjectResponse> = {
|
||||
id: 'gitlab_get_project',
|
||||
name: 'GitLab Get Project',
|
||||
description: 'Get details of a specific GitLab project',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'GitLab Personal Access Token',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or URL-encoded path (e.g., "namespace/project")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const encodedId = encodeURIComponent(String(params.projectId).trim())
|
||||
return `${getGitLabApiBase(params.host)}/projects/${encodedId}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
'PRIVATE-TOKEN': params.accessToken,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return {
|
||||
success: false,
|
||||
error: `GitLab API error: ${response.status} ${errorText}`,
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const project = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
project,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
project: {
|
||||
type: 'object',
|
||||
description: 'The GitLab project details',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { gitlabApproveMergeRequestTool } from '@/tools/gitlab/approve_merge_request'
|
||||
import { gitlabCancelPipelineTool } from '@/tools/gitlab/cancel_pipeline'
|
||||
import { gitlabCompareBranchesTool } from '@/tools/gitlab/compare_branches'
|
||||
import { gitlabCreateBranchTool } from '@/tools/gitlab/create_branch'
|
||||
import { gitlabCreateFileTool } from '@/tools/gitlab/create_file'
|
||||
import { gitlabCreateIssueTool } from '@/tools/gitlab/create_issue'
|
||||
import { gitlabCreateIssueNoteTool } from '@/tools/gitlab/create_issue_note'
|
||||
import { gitlabCreateMergeRequestTool } from '@/tools/gitlab/create_merge_request'
|
||||
import { gitlabCreateMergeRequestNoteTool } from '@/tools/gitlab/create_merge_request_note'
|
||||
import { gitlabCreatePipelineTool } from '@/tools/gitlab/create_pipeline'
|
||||
import { gitlabCreateReleaseTool } from '@/tools/gitlab/create_release'
|
||||
import { gitlabDeleteBranchTool } from '@/tools/gitlab/delete_branch'
|
||||
import { gitlabDeleteIssueTool } from '@/tools/gitlab/delete_issue'
|
||||
import { gitlabGetFileTool } from '@/tools/gitlab/get_file'
|
||||
import { gitlabGetIssueTool } from '@/tools/gitlab/get_issue'
|
||||
import { gitlabGetJobLogTool } from '@/tools/gitlab/get_job_log'
|
||||
import { gitlabGetMergeRequestTool } from '@/tools/gitlab/get_merge_request'
|
||||
import { gitlabGetMergeRequestChangesTool } from '@/tools/gitlab/get_merge_request_changes'
|
||||
import { gitlabGetPipelineTool } from '@/tools/gitlab/get_pipeline'
|
||||
import { gitlabGetProjectTool } from '@/tools/gitlab/get_project'
|
||||
import { gitlabListBranchesTool } from '@/tools/gitlab/list_branches'
|
||||
import { gitlabListCommitsTool } from '@/tools/gitlab/list_commits'
|
||||
import { gitlabListIssuesTool } from '@/tools/gitlab/list_issues'
|
||||
import { gitlabListMergeRequestsTool } from '@/tools/gitlab/list_merge_requests'
|
||||
import { gitlabListPipelineJobsTool } from '@/tools/gitlab/list_pipeline_jobs'
|
||||
import { gitlabListPipelinesTool } from '@/tools/gitlab/list_pipelines'
|
||||
import { gitlabListProjectsTool } from '@/tools/gitlab/list_projects'
|
||||
import { gitlabListReleasesTool } from '@/tools/gitlab/list_releases'
|
||||
import { gitlabListRepositoryTreeTool } from '@/tools/gitlab/list_repository_tree'
|
||||
import { gitlabMergeMergeRequestTool } from '@/tools/gitlab/merge_merge_request'
|
||||
import { gitlabPlayJobTool } from '@/tools/gitlab/play_job'
|
||||
import { gitlabRetryPipelineTool } from '@/tools/gitlab/retry_pipeline'
|
||||
import { gitlabUpdateFileTool } from '@/tools/gitlab/update_file'
|
||||
import { gitlabUpdateIssueTool } from '@/tools/gitlab/update_issue'
|
||||
import { gitlabUpdateMergeRequestTool } from '@/tools/gitlab/update_merge_request'
|
||||
|
||||
export {
|
||||
// Projects
|
||||
gitlabListProjectsTool,
|
||||
gitlabGetProjectTool,
|
||||
// Issues
|
||||
gitlabListIssuesTool,
|
||||
gitlabGetIssueTool,
|
||||
gitlabCreateIssueTool,
|
||||
gitlabUpdateIssueTool,
|
||||
gitlabDeleteIssueTool,
|
||||
gitlabCreateIssueNoteTool,
|
||||
// Merge Requests
|
||||
gitlabListMergeRequestsTool,
|
||||
gitlabGetMergeRequestTool,
|
||||
gitlabCreateMergeRequestTool,
|
||||
gitlabUpdateMergeRequestTool,
|
||||
gitlabMergeMergeRequestTool,
|
||||
gitlabCreateMergeRequestNoteTool,
|
||||
gitlabGetMergeRequestChangesTool,
|
||||
gitlabApproveMergeRequestTool,
|
||||
// Pipelines
|
||||
gitlabListPipelinesTool,
|
||||
gitlabGetPipelineTool,
|
||||
gitlabCreatePipelineTool,
|
||||
gitlabRetryPipelineTool,
|
||||
gitlabCancelPipelineTool,
|
||||
// Jobs
|
||||
gitlabListPipelineJobsTool,
|
||||
gitlabGetJobLogTool,
|
||||
gitlabPlayJobTool,
|
||||
// Repository Files & Tree
|
||||
gitlabListRepositoryTreeTool,
|
||||
gitlabGetFileTool,
|
||||
gitlabCreateFileTool,
|
||||
gitlabUpdateFileTool,
|
||||
// Branches
|
||||
gitlabListBranchesTool,
|
||||
gitlabCreateBranchTool,
|
||||
gitlabDeleteBranchTool,
|
||||
gitlabCompareBranchesTool,
|
||||
// Commits
|
||||
gitlabListCommitsTool,
|
||||
// Releases
|
||||
gitlabListReleasesTool,
|
||||
gitlabCreateReleaseTool,
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import type { GitLabListBranchesParams, GitLabListBranchesResponse } from '@/tools/gitlab/types'
|
||||
import { getGitLabApiBase } from '@/tools/gitlab/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const gitlabListBranchesTool: ToolConfig<
|
||||
GitLabListBranchesParams,
|
||||
GitLabListBranchesResponse
|
||||
> = {
|
||||
id: 'gitlab_list_branches',
|
||||
name: 'GitLab List Branches',
|
||||
description: 'List branches in a GitLab project repository',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'GitLab Personal Access Token',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or URL-encoded path',
|
||||
},
|
||||
search: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter branches by name',
|
||||
},
|
||||
perPage: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of results per page (default 20, max 100)',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Page number for pagination',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const encodedId = encodeURIComponent(String(params.projectId).trim())
|
||||
const queryParams = new URLSearchParams()
|
||||
|
||||
if (params.search) queryParams.append('search', params.search)
|
||||
if (params.perPage) queryParams.append('per_page', String(params.perPage))
|
||||
if (params.page) queryParams.append('page', String(params.page))
|
||||
|
||||
const query = queryParams.toString()
|
||||
return `${getGitLabApiBase(params.host)}/projects/${encodedId}/repository/branches${query ? `?${query}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
'PRIVATE-TOKEN': params.accessToken,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return {
|
||||
success: false,
|
||||
error: `GitLab API error: ${response.status} ${errorText}`,
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const branches = await response.json()
|
||||
const total = response.headers.get('x-total')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
branches: branches ?? [],
|
||||
total: total ? Number.parseInt(total, 10) : (branches?.length ?? 0),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
branches: {
|
||||
type: 'array',
|
||||
description: 'List of branches',
|
||||
},
|
||||
total: {
|
||||
type: 'number',
|
||||
description: 'Total number of branches',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import type { GitLabListCommitsParams, GitLabListCommitsResponse } from '@/tools/gitlab/types'
|
||||
import { getGitLabApiBase } from '@/tools/gitlab/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const gitlabListCommitsTool: ToolConfig<GitLabListCommitsParams, GitLabListCommitsResponse> =
|
||||
{
|
||||
id: 'gitlab_list_commits',
|
||||
name: 'GitLab List Commits',
|
||||
description: 'List commits in a GitLab project repository',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'GitLab Personal Access Token',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or URL-encoded path',
|
||||
},
|
||||
refName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Branch, tag, or revision range to list commits from',
|
||||
},
|
||||
since: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Only commits after this ISO 8601 date',
|
||||
},
|
||||
until: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Only commits before this ISO 8601 date',
|
||||
},
|
||||
path: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Only commits affecting this file path',
|
||||
},
|
||||
author: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter commits by author',
|
||||
},
|
||||
perPage: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of results per page (default 20, max 100)',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Page number for pagination',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const encodedId = encodeURIComponent(String(params.projectId).trim())
|
||||
const queryParams = new URLSearchParams()
|
||||
|
||||
if (params.refName) queryParams.append('ref_name', params.refName)
|
||||
if (params.since) queryParams.append('since', params.since)
|
||||
if (params.until) queryParams.append('until', params.until)
|
||||
if (params.path) queryParams.append('path', params.path)
|
||||
if (params.author) queryParams.append('author', params.author)
|
||||
if (params.perPage) queryParams.append('per_page', String(params.perPage))
|
||||
if (params.page) queryParams.append('page', String(params.page))
|
||||
|
||||
const query = queryParams.toString()
|
||||
return `${getGitLabApiBase(params.host)}/projects/${encodedId}/repository/commits${query ? `?${query}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
'PRIVATE-TOKEN': params.accessToken,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return {
|
||||
success: false,
|
||||
error: `GitLab API error: ${response.status} ${errorText}`,
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const commits = await response.json()
|
||||
const total = response.headers.get('x-total')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
commits: commits ?? [],
|
||||
total: total ? Number.parseInt(total, 10) : (commits?.length ?? 0),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
commits: {
|
||||
type: 'array',
|
||||
description: 'List of commits',
|
||||
},
|
||||
total: {
|
||||
type: 'number',
|
||||
description: 'Total number of commits',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import type { GitLabListIssuesParams, GitLabListIssuesResponse } from '@/tools/gitlab/types'
|
||||
import { getGitLabApiBase } from '@/tools/gitlab/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const gitlabListIssuesTool: ToolConfig<GitLabListIssuesParams, GitLabListIssuesResponse> = {
|
||||
id: 'gitlab_list_issues',
|
||||
name: 'GitLab List Issues',
|
||||
description: 'List issues in a GitLab project',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'GitLab Personal Access Token',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or URL-encoded path',
|
||||
},
|
||||
state: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by state (opened, closed, all)',
|
||||
},
|
||||
labels: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated list of label names',
|
||||
},
|
||||
assigneeId: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by assignee user ID',
|
||||
},
|
||||
milestoneTitle: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by milestone title',
|
||||
},
|
||||
search: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Search issues by title and description',
|
||||
},
|
||||
orderBy: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Order by field (created_at, updated_at, priority, due_date, relative_position, label_priority, milestone_due, popularity, title, weight)',
|
||||
},
|
||||
sort: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Sort direction (asc, desc)',
|
||||
},
|
||||
perPage: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of results per page (default 20, max 100)',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Page number for pagination',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const encodedId = encodeURIComponent(String(params.projectId).trim())
|
||||
const queryParams = new URLSearchParams()
|
||||
|
||||
if (params.state) queryParams.append('state', params.state)
|
||||
if (params.labels) queryParams.append('labels', params.labels)
|
||||
if (params.assigneeId) queryParams.append('assignee_id', String(params.assigneeId))
|
||||
if (params.milestoneTitle) queryParams.append('milestone', params.milestoneTitle)
|
||||
if (params.search) queryParams.append('search', params.search)
|
||||
if (params.orderBy) queryParams.append('order_by', params.orderBy)
|
||||
if (params.sort) queryParams.append('sort', params.sort)
|
||||
if (params.perPage) queryParams.append('per_page', String(params.perPage))
|
||||
if (params.page) queryParams.append('page', String(params.page))
|
||||
|
||||
const query = queryParams.toString()
|
||||
return `${getGitLabApiBase(params.host)}/projects/${encodedId}/issues${query ? `?${query}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
'PRIVATE-TOKEN': params.accessToken,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return {
|
||||
success: false,
|
||||
error: `GitLab API error: ${response.status} ${errorText}`,
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const issues = await response.json()
|
||||
const total = response.headers.get('x-total')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
issues,
|
||||
total: total ? Number.parseInt(total, 10) : issues.length,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
issues: {
|
||||
type: 'array',
|
||||
description: 'List of GitLab issues',
|
||||
},
|
||||
total: {
|
||||
type: 'number',
|
||||
description: 'Total number of issues',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import type {
|
||||
GitLabListMergeRequestsParams,
|
||||
GitLabListMergeRequestsResponse,
|
||||
} from '@/tools/gitlab/types'
|
||||
import { getGitLabApiBase } from '@/tools/gitlab/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const gitlabListMergeRequestsTool: ToolConfig<
|
||||
GitLabListMergeRequestsParams,
|
||||
GitLabListMergeRequestsResponse
|
||||
> = {
|
||||
id: 'gitlab_list_merge_requests',
|
||||
name: 'GitLab List Merge Requests',
|
||||
description: 'List merge requests in a GitLab project',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'GitLab Personal Access Token',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or URL-encoded path',
|
||||
},
|
||||
state: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by state (opened, closed, locked, merged, all)',
|
||||
},
|
||||
labels: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated list of label names',
|
||||
},
|
||||
sourceBranch: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by source branch',
|
||||
},
|
||||
targetBranch: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by target branch',
|
||||
},
|
||||
orderBy: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Order by field (created_at, updated_at, merged_at, priority, label_priority, milestone_due, popularity, title)',
|
||||
},
|
||||
sort: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Sort direction (asc, desc)',
|
||||
},
|
||||
perPage: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of results per page (default 20, max 100)',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Page number for pagination',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const encodedId = encodeURIComponent(String(params.projectId).trim())
|
||||
const queryParams = new URLSearchParams()
|
||||
|
||||
if (params.state) queryParams.append('state', params.state)
|
||||
if (params.labels) queryParams.append('labels', params.labels)
|
||||
if (params.sourceBranch) queryParams.append('source_branch', params.sourceBranch)
|
||||
if (params.targetBranch) queryParams.append('target_branch', params.targetBranch)
|
||||
if (params.orderBy) queryParams.append('order_by', params.orderBy)
|
||||
if (params.sort) queryParams.append('sort', params.sort)
|
||||
if (params.perPage) queryParams.append('per_page', String(params.perPage))
|
||||
if (params.page) queryParams.append('page', String(params.page))
|
||||
|
||||
const query = queryParams.toString()
|
||||
return `${getGitLabApiBase(params.host)}/projects/${encodedId}/merge_requests${query ? `?${query}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
'PRIVATE-TOKEN': params.accessToken,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return {
|
||||
success: false,
|
||||
error: `GitLab API error: ${response.status} ${errorText}`,
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const mergeRequests = await response.json()
|
||||
const total = response.headers.get('x-total')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
mergeRequests,
|
||||
total: total ? Number.parseInt(total, 10) : mergeRequests.length,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
mergeRequests: {
|
||||
type: 'array',
|
||||
description: 'List of GitLab merge requests',
|
||||
},
|
||||
total: {
|
||||
type: 'number',
|
||||
description: 'Total number of merge requests',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import type {
|
||||
GitLabListPipelineJobsParams,
|
||||
GitLabListPipelineJobsResponse,
|
||||
} from '@/tools/gitlab/types'
|
||||
import { getGitLabApiBase } from '@/tools/gitlab/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const gitlabListPipelineJobsTool: ToolConfig<
|
||||
GitLabListPipelineJobsParams,
|
||||
GitLabListPipelineJobsResponse
|
||||
> = {
|
||||
id: 'gitlab_list_pipeline_jobs',
|
||||
name: 'GitLab List Pipeline Jobs',
|
||||
description: 'List jobs for a GitLab pipeline',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'GitLab Personal Access Token',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or URL-encoded path',
|
||||
},
|
||||
pipelineId: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Pipeline ID',
|
||||
},
|
||||
scope: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter jobs by scope (e.g. created, running, success, failed)',
|
||||
},
|
||||
includeRetried: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to include retried jobs',
|
||||
},
|
||||
perPage: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of results per page (default 20, max 100)',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Page number for pagination',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const encodedId = encodeURIComponent(String(params.projectId).trim())
|
||||
const queryParams = new URLSearchParams()
|
||||
|
||||
if (params.scope) queryParams.append('scope', params.scope)
|
||||
if (params.includeRetried) queryParams.append('include_retried', 'true')
|
||||
if (params.perPage) queryParams.append('per_page', String(params.perPage))
|
||||
if (params.page) queryParams.append('page', String(params.page))
|
||||
|
||||
const query = queryParams.toString()
|
||||
return `${getGitLabApiBase(params.host)}/projects/${encodedId}/pipelines/${params.pipelineId}/jobs${query ? `?${query}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
'PRIVATE-TOKEN': params.accessToken,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return {
|
||||
success: false,
|
||||
error: `GitLab API error: ${response.status} ${errorText}`,
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const jobs = await response.json()
|
||||
const total = response.headers.get('x-total')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
jobs: jobs ?? [],
|
||||
total: total ? Number.parseInt(total, 10) : (jobs?.length ?? 0),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
jobs: {
|
||||
type: 'array',
|
||||
description: 'List of pipeline jobs',
|
||||
},
|
||||
total: {
|
||||
type: 'number',
|
||||
description: 'Total number of jobs',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import type { GitLabListPipelinesParams, GitLabListPipelinesResponse } from '@/tools/gitlab/types'
|
||||
import { getGitLabApiBase } from '@/tools/gitlab/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const gitlabListPipelinesTool: ToolConfig<
|
||||
GitLabListPipelinesParams,
|
||||
GitLabListPipelinesResponse
|
||||
> = {
|
||||
id: 'gitlab_list_pipelines',
|
||||
name: 'GitLab List Pipelines',
|
||||
description: 'List pipelines in a GitLab project',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'GitLab Personal Access Token',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or URL-encoded path',
|
||||
},
|
||||
ref: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by ref (branch or tag)',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Filter by status (created, waiting_for_resource, preparing, pending, running, success, failed, canceled, skipped, manual, scheduled)',
|
||||
},
|
||||
orderBy: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Order by field (id, status, ref, updated_at, user_id)',
|
||||
},
|
||||
sort: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Sort direction (asc, desc)',
|
||||
},
|
||||
perPage: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of results per page (default 20, max 100)',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Page number for pagination',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const encodedId = encodeURIComponent(String(params.projectId).trim())
|
||||
const queryParams = new URLSearchParams()
|
||||
|
||||
if (params.ref) queryParams.append('ref', params.ref)
|
||||
if (params.status) queryParams.append('status', params.status)
|
||||
if (params.orderBy) queryParams.append('order_by', params.orderBy)
|
||||
if (params.sort) queryParams.append('sort', params.sort)
|
||||
if (params.perPage) queryParams.append('per_page', String(params.perPage))
|
||||
if (params.page) queryParams.append('page', String(params.page))
|
||||
|
||||
const query = queryParams.toString()
|
||||
return `${getGitLabApiBase(params.host)}/projects/${encodedId}/pipelines${query ? `?${query}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
'PRIVATE-TOKEN': params.accessToken,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return {
|
||||
success: false,
|
||||
error: `GitLab API error: ${response.status} ${errorText}`,
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const pipelines = await response.json()
|
||||
const total = response.headers.get('x-total')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
pipelines,
|
||||
total: total ? Number.parseInt(total, 10) : pipelines.length,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
pipelines: {
|
||||
type: 'array',
|
||||
description: 'List of GitLab pipelines',
|
||||
},
|
||||
total: {
|
||||
type: 'number',
|
||||
description: 'Total number of pipelines',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import type { GitLabListProjectsParams, GitLabListProjectsResponse } from '@/tools/gitlab/types'
|
||||
import { getGitLabApiBase } from '@/tools/gitlab/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const gitlabListProjectsTool: ToolConfig<
|
||||
GitLabListProjectsParams,
|
||||
GitLabListProjectsResponse
|
||||
> = {
|
||||
id: 'gitlab_list_projects',
|
||||
name: 'GitLab List Projects',
|
||||
description: 'List GitLab projects accessible to the authenticated user',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'GitLab Personal Access Token',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.',
|
||||
},
|
||||
owned: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Limit to projects owned by the current user',
|
||||
},
|
||||
membership: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Limit to projects the current user is a member of',
|
||||
},
|
||||
search: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Search projects by name',
|
||||
},
|
||||
visibility: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by visibility (public, internal, private)',
|
||||
},
|
||||
orderBy: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Order by field (id, name, path, created_at, updated_at, last_activity_at)',
|
||||
},
|
||||
sort: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Sort direction (asc, desc)',
|
||||
},
|
||||
perPage: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of results per page (default 20, max 100)',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Page number for pagination',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const queryParams = new URLSearchParams()
|
||||
if (params.owned) queryParams.append('owned', 'true')
|
||||
if (params.membership) queryParams.append('membership', 'true')
|
||||
if (params.search) queryParams.append('search', params.search)
|
||||
if (params.visibility) queryParams.append('visibility', params.visibility)
|
||||
if (params.orderBy) queryParams.append('order_by', params.orderBy)
|
||||
if (params.sort) queryParams.append('sort', params.sort)
|
||||
if (params.perPage) queryParams.append('per_page', String(params.perPage))
|
||||
if (params.page) queryParams.append('page', String(params.page))
|
||||
|
||||
const query = queryParams.toString()
|
||||
return `${getGitLabApiBase(params.host)}/projects${query ? `?${query}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
'PRIVATE-TOKEN': params.accessToken,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return {
|
||||
success: false,
|
||||
error: `GitLab API error: ${response.status} ${errorText}`,
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const projects = await response.json()
|
||||
const total = response.headers.get('x-total')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
projects,
|
||||
total: total ? Number.parseInt(total, 10) : projects.length,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
projects: {
|
||||
type: 'array',
|
||||
description: 'List of GitLab projects',
|
||||
},
|
||||
total: {
|
||||
type: 'number',
|
||||
description: 'Total number of projects',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import type { GitLabListReleasesParams, GitLabListReleasesResponse } from '@/tools/gitlab/types'
|
||||
import { getGitLabApiBase } from '@/tools/gitlab/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const gitlabListReleasesTool: ToolConfig<
|
||||
GitLabListReleasesParams,
|
||||
GitLabListReleasesResponse
|
||||
> = {
|
||||
id: 'gitlab_list_releases',
|
||||
name: 'GitLab List Releases',
|
||||
description: 'List releases in a GitLab project',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'GitLab Personal Access Token',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or URL-encoded path',
|
||||
},
|
||||
orderBy: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Order by field (released_at, created_at)',
|
||||
},
|
||||
sort: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Sort direction (asc, desc)',
|
||||
},
|
||||
perPage: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of results per page (default 20, max 100)',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Page number for pagination',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const encodedId = encodeURIComponent(String(params.projectId).trim())
|
||||
const queryParams = new URLSearchParams()
|
||||
|
||||
if (params.orderBy) queryParams.append('order_by', params.orderBy)
|
||||
if (params.sort) queryParams.append('sort', params.sort)
|
||||
if (params.perPage) queryParams.append('per_page', String(params.perPage))
|
||||
if (params.page) queryParams.append('page', String(params.page))
|
||||
|
||||
const query = queryParams.toString()
|
||||
return `${getGitLabApiBase(params.host)}/projects/${encodedId}/releases${query ? `?${query}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
'PRIVATE-TOKEN': params.accessToken,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return {
|
||||
success: false,
|
||||
error: `GitLab API error: ${response.status} ${errorText}`,
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const releases = await response.json()
|
||||
const total = response.headers.get('x-total')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
releases: releases ?? [],
|
||||
total: total ? Number.parseInt(total, 10) : (releases?.length ?? 0),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
releases: {
|
||||
type: 'array',
|
||||
description: 'List of GitLab releases',
|
||||
},
|
||||
total: {
|
||||
type: 'number',
|
||||
description: 'Total number of releases',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import type {
|
||||
GitLabListRepositoryTreeParams,
|
||||
GitLabListRepositoryTreeResponse,
|
||||
} from '@/tools/gitlab/types'
|
||||
import { getGitLabApiBase } from '@/tools/gitlab/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const gitlabListRepositoryTreeTool: ToolConfig<
|
||||
GitLabListRepositoryTreeParams,
|
||||
GitLabListRepositoryTreeResponse
|
||||
> = {
|
||||
id: 'gitlab_list_repository_tree',
|
||||
name: 'GitLab List Repository Tree',
|
||||
description: 'List files and directories in a GitLab project repository',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'GitLab Personal Access Token',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or URL-encoded path',
|
||||
},
|
||||
path: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Path inside the repository to list',
|
||||
},
|
||||
ref: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Branch, tag, or commit SHA to list from',
|
||||
},
|
||||
recursive: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to list files recursively',
|
||||
},
|
||||
perPage: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of results per page (default 20, max 100)',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Page number for pagination',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const encodedId = encodeURIComponent(String(params.projectId).trim())
|
||||
const queryParams = new URLSearchParams()
|
||||
|
||||
if (params.path) queryParams.append('path', params.path)
|
||||
if (params.ref) queryParams.append('ref', params.ref)
|
||||
if (params.recursive) queryParams.append('recursive', 'true')
|
||||
if (params.perPage) queryParams.append('per_page', String(params.perPage))
|
||||
if (params.page) queryParams.append('page', String(params.page))
|
||||
|
||||
const query = queryParams.toString()
|
||||
return `${getGitLabApiBase(params.host)}/projects/${encodedId}/repository/tree${query ? `?${query}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
'PRIVATE-TOKEN': params.accessToken,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return {
|
||||
success: false,
|
||||
error: `GitLab API error: ${response.status} ${errorText}`,
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const tree = await response.json()
|
||||
const total = response.headers.get('x-total')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
tree: tree ?? [],
|
||||
total: total ? Number.parseInt(total, 10) : (tree?.length ?? 0),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
tree: {
|
||||
type: 'array',
|
||||
description: 'List of repository tree entries',
|
||||
},
|
||||
total: {
|
||||
type: 'number',
|
||||
description: 'Total number of tree entries',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import type {
|
||||
GitLabMergeMergeRequestParams,
|
||||
GitLabMergeMergeRequestResponse,
|
||||
} from '@/tools/gitlab/types'
|
||||
import { getGitLabApiBase } from '@/tools/gitlab/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const gitlabMergeMergeRequestTool: ToolConfig<
|
||||
GitLabMergeMergeRequestParams,
|
||||
GitLabMergeMergeRequestResponse
|
||||
> = {
|
||||
id: 'gitlab_merge_merge_request',
|
||||
name: 'GitLab Merge Merge Request',
|
||||
description: 'Merge a merge request in a GitLab project',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'GitLab Personal Access Token',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or URL-encoded path',
|
||||
},
|
||||
mergeRequestIid: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Merge request internal ID (IID)',
|
||||
},
|
||||
mergeCommitMessage: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Custom merge commit message',
|
||||
},
|
||||
squashCommitMessage: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Custom squash commit message',
|
||||
},
|
||||
squash: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Squash commits before merging',
|
||||
},
|
||||
shouldRemoveSourceBranch: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Delete source branch after merge',
|
||||
},
|
||||
mergeWhenPipelineSucceeds: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Merge when pipeline succeeds',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const encodedId = encodeURIComponent(String(params.projectId).trim())
|
||||
return `${getGitLabApiBase(params.host)}/projects/${encodedId}/merge_requests/${params.mergeRequestIid}/merge`
|
||||
},
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
'PRIVATE-TOKEN': params.accessToken,
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, any> = {}
|
||||
|
||||
if (params.mergeCommitMessage) body.merge_commit_message = params.mergeCommitMessage
|
||||
if (params.squashCommitMessage) body.squash_commit_message = params.squashCommitMessage
|
||||
if (params.squash !== undefined) body.squash = params.squash
|
||||
if (params.shouldRemoveSourceBranch !== undefined)
|
||||
body.should_remove_source_branch = params.shouldRemoveSourceBranch
|
||||
if (params.mergeWhenPipelineSucceeds !== undefined)
|
||||
body.merge_when_pipeline_succeeds = params.mergeWhenPipelineSucceeds
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return {
|
||||
success: false,
|
||||
error: `GitLab API error: ${response.status} ${errorText}`,
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const mergeRequest = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
mergeRequest,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
mergeRequest: {
|
||||
type: 'object',
|
||||
description: 'The merged GitLab merge request',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import type { GitLabPlayJobParams, GitLabPlayJobResponse } from '@/tools/gitlab/types'
|
||||
import { getGitLabApiBase } from '@/tools/gitlab/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const gitlabPlayJobTool: ToolConfig<GitLabPlayJobParams, GitLabPlayJobResponse> = {
|
||||
id: 'gitlab_play_job',
|
||||
name: 'GitLab Play Job',
|
||||
description: 'Trigger (play) a manual GitLab job',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'GitLab Personal Access Token',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or URL-encoded path',
|
||||
},
|
||||
jobId: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Job ID',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const encodedId = encodeURIComponent(String(params.projectId).trim())
|
||||
return `${getGitLabApiBase(params.host)}/projects/${encodedId}/jobs/${params.jobId}/play`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'PRIVATE-TOKEN': params.accessToken,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return {
|
||||
success: false,
|
||||
error: `GitLab API error: ${response.status} ${errorText}`,
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id ?? null,
|
||||
name: data.name ?? null,
|
||||
status: data.status ?? null,
|
||||
webUrl: data.web_url ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: {
|
||||
type: 'number',
|
||||
description: 'The job ID',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'The job name',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
description: 'The job status',
|
||||
},
|
||||
webUrl: {
|
||||
type: 'string',
|
||||
description: 'The web URL of the job',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { GitLabRetryPipelineParams, GitLabRetryPipelineResponse } from '@/tools/gitlab/types'
|
||||
import { getGitLabApiBase } from '@/tools/gitlab/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const gitlabRetryPipelineTool: ToolConfig<
|
||||
GitLabRetryPipelineParams,
|
||||
GitLabRetryPipelineResponse
|
||||
> = {
|
||||
id: 'gitlab_retry_pipeline',
|
||||
name: 'GitLab Retry Pipeline',
|
||||
description: 'Retry a failed GitLab pipeline',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'GitLab Personal Access Token',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or URL-encoded path',
|
||||
},
|
||||
pipelineId: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Pipeline ID',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const encodedId = encodeURIComponent(String(params.projectId).trim())
|
||||
return `${getGitLabApiBase(params.host)}/projects/${encodedId}/pipelines/${params.pipelineId}/retry`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'PRIVATE-TOKEN': params.accessToken,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return {
|
||||
success: false,
|
||||
error: `GitLab API error: ${response.status} ${errorText}`,
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const pipeline = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
pipeline,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
pipeline: {
|
||||
type: 'object',
|
||||
description: 'The retried GitLab pipeline',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,791 @@
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
interface GitLabProject {
|
||||
id: number
|
||||
name: string
|
||||
path: string
|
||||
path_with_namespace: string
|
||||
description?: string
|
||||
visibility: string
|
||||
web_url: string
|
||||
default_branch?: string
|
||||
created_at: string
|
||||
last_activity_at: string
|
||||
namespace?: {
|
||||
id: number
|
||||
name: string
|
||||
path: string
|
||||
kind: string
|
||||
}
|
||||
owner?: {
|
||||
id: number
|
||||
name: string
|
||||
username: string
|
||||
}
|
||||
}
|
||||
|
||||
interface GitLabIssue {
|
||||
id: number
|
||||
iid: number
|
||||
project_id: number
|
||||
title: string
|
||||
description?: string
|
||||
state: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
closed_at?: string
|
||||
labels: string[]
|
||||
milestone?: {
|
||||
id: number
|
||||
iid: number
|
||||
title: string
|
||||
}
|
||||
assignees?: Array<{
|
||||
id: number
|
||||
name: string
|
||||
username: string
|
||||
}>
|
||||
assignee?: {
|
||||
id: number
|
||||
name: string
|
||||
username: string
|
||||
}
|
||||
author: {
|
||||
id: number
|
||||
name: string
|
||||
username: string
|
||||
}
|
||||
web_url: string
|
||||
due_date?: string
|
||||
confidential: boolean
|
||||
}
|
||||
|
||||
interface GitLabMergeRequest {
|
||||
id: number
|
||||
iid: number
|
||||
project_id: number
|
||||
title: string
|
||||
description?: string
|
||||
state: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
merged_at?: string
|
||||
closed_at?: string
|
||||
source_branch: string
|
||||
target_branch: string
|
||||
source_project_id: number
|
||||
target_project_id: number
|
||||
labels: string[]
|
||||
milestone?: {
|
||||
id: number
|
||||
iid: number
|
||||
title: string
|
||||
}
|
||||
assignee?: {
|
||||
id: number
|
||||
name: string
|
||||
username: string
|
||||
}
|
||||
assignees?: Array<{
|
||||
id: number
|
||||
name: string
|
||||
username: string
|
||||
}>
|
||||
author: {
|
||||
id: number
|
||||
name: string
|
||||
username: string
|
||||
}
|
||||
merge_status: string
|
||||
web_url: string
|
||||
draft: boolean
|
||||
work_in_progress: boolean
|
||||
has_conflicts: boolean
|
||||
merge_when_pipeline_succeeds: boolean
|
||||
}
|
||||
|
||||
interface GitLabPipeline {
|
||||
id: number
|
||||
iid: number
|
||||
project_id: number
|
||||
sha: string
|
||||
ref: string
|
||||
status: string
|
||||
source: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
web_url: string
|
||||
user?: {
|
||||
id: number
|
||||
name: string
|
||||
username: string
|
||||
}
|
||||
}
|
||||
|
||||
interface GitLabTreeEntry {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
path: string
|
||||
mode: string
|
||||
}
|
||||
|
||||
interface GitLabCommit {
|
||||
id: string
|
||||
short_id: string
|
||||
title: string
|
||||
message: string
|
||||
author_name: string
|
||||
authored_date: string
|
||||
created_at: string
|
||||
web_url: string
|
||||
}
|
||||
|
||||
interface GitLabJob {
|
||||
id: number
|
||||
name: string
|
||||
stage: string
|
||||
status: string
|
||||
started_at?: string | null
|
||||
finished_at?: string | null
|
||||
duration?: number | null
|
||||
web_url: string
|
||||
ref?: string
|
||||
}
|
||||
|
||||
interface GitLabMergeRequestChange {
|
||||
old_path: string
|
||||
new_path: string
|
||||
diff: string
|
||||
new_file: boolean
|
||||
deleted_file: boolean
|
||||
renamed_file: boolean
|
||||
}
|
||||
|
||||
interface GitLabBranch {
|
||||
name: string
|
||||
merged: boolean
|
||||
protected: boolean
|
||||
default: boolean
|
||||
developers_can_push: boolean
|
||||
developers_can_merge: boolean
|
||||
can_push: boolean
|
||||
web_url: string
|
||||
commit?: {
|
||||
id: string
|
||||
short_id: string
|
||||
title: string
|
||||
author_name: string
|
||||
authored_date: string
|
||||
}
|
||||
}
|
||||
|
||||
interface GitLabNote {
|
||||
id: number
|
||||
body: string
|
||||
author: {
|
||||
id: number
|
||||
name: string
|
||||
username: string
|
||||
}
|
||||
created_at: string
|
||||
updated_at: string
|
||||
system: boolean
|
||||
noteable_id: number
|
||||
noteable_type: string
|
||||
noteable_iid?: number
|
||||
}
|
||||
|
||||
interface GitLabRelease {
|
||||
tag_name: string
|
||||
name?: string
|
||||
description?: string
|
||||
created_at: string
|
||||
released_at?: string
|
||||
author?: {
|
||||
id: number
|
||||
name: string
|
||||
username: string
|
||||
}
|
||||
commit?: {
|
||||
id: string
|
||||
short_id: string
|
||||
title: string
|
||||
}
|
||||
milestones?: unknown[]
|
||||
assets?: {
|
||||
count?: number
|
||||
sources?: unknown[]
|
||||
links?: unknown[]
|
||||
}
|
||||
_links?: Record<string, string>
|
||||
}
|
||||
|
||||
interface GitLabBaseParams {
|
||||
accessToken: string
|
||||
/**
|
||||
* Self-managed GitLab host (e.g. `gitlab.example.com`). Optional — defaults to
|
||||
* `gitlab.com` so existing workflows keep working.
|
||||
*/
|
||||
host?: string
|
||||
}
|
||||
|
||||
export interface GitLabListProjectsParams extends GitLabBaseParams {
|
||||
owned?: boolean
|
||||
membership?: boolean
|
||||
search?: string
|
||||
visibility?: 'public' | 'internal' | 'private'
|
||||
orderBy?: 'id' | 'name' | 'path' | 'created_at' | 'updated_at' | 'last_activity_at'
|
||||
sort?: 'asc' | 'desc'
|
||||
perPage?: number
|
||||
page?: number
|
||||
}
|
||||
|
||||
export interface GitLabGetProjectParams extends GitLabBaseParams {
|
||||
projectId: string | number
|
||||
}
|
||||
|
||||
export interface GitLabListIssuesParams extends GitLabBaseParams {
|
||||
projectId: string | number
|
||||
state?: 'opened' | 'closed' | 'all'
|
||||
labels?: string
|
||||
assigneeId?: number
|
||||
milestoneTitle?: string
|
||||
search?: string
|
||||
orderBy?: 'created_at' | 'updated_at'
|
||||
sort?: 'asc' | 'desc'
|
||||
perPage?: number
|
||||
page?: number
|
||||
}
|
||||
|
||||
export interface GitLabGetIssueParams extends GitLabBaseParams {
|
||||
projectId: string | number
|
||||
issueIid: number
|
||||
}
|
||||
|
||||
export interface GitLabCreateIssueParams extends GitLabBaseParams {
|
||||
projectId: string | number
|
||||
title: string
|
||||
description?: string
|
||||
labels?: string
|
||||
assigneeIds?: number[]
|
||||
milestoneId?: number
|
||||
dueDate?: string
|
||||
confidential?: boolean
|
||||
}
|
||||
|
||||
export interface GitLabUpdateIssueParams extends GitLabBaseParams {
|
||||
projectId: string | number
|
||||
issueIid: number
|
||||
title?: string
|
||||
description?: string
|
||||
stateEvent?: 'close' | 'reopen'
|
||||
labels?: string
|
||||
assigneeIds?: number[]
|
||||
milestoneId?: number
|
||||
dueDate?: string
|
||||
confidential?: boolean
|
||||
}
|
||||
|
||||
export interface GitLabDeleteIssueParams extends GitLabBaseParams {
|
||||
projectId: string | number
|
||||
issueIid: number
|
||||
}
|
||||
|
||||
export interface GitLabListMergeRequestsParams extends GitLabBaseParams {
|
||||
projectId: string | number
|
||||
state?: 'opened' | 'closed' | 'merged' | 'all'
|
||||
labels?: string
|
||||
sourceBranch?: string
|
||||
targetBranch?: string
|
||||
orderBy?: 'created_at' | 'updated_at'
|
||||
sort?: 'asc' | 'desc'
|
||||
perPage?: number
|
||||
page?: number
|
||||
}
|
||||
|
||||
export interface GitLabGetMergeRequestParams extends GitLabBaseParams {
|
||||
projectId: string | number
|
||||
mergeRequestIid: number
|
||||
}
|
||||
|
||||
export interface GitLabCreateMergeRequestParams extends GitLabBaseParams {
|
||||
projectId: string | number
|
||||
sourceBranch: string
|
||||
targetBranch: string
|
||||
title: string
|
||||
description?: string
|
||||
labels?: string
|
||||
assigneeIds?: number[]
|
||||
milestoneId?: number
|
||||
removeSourceBranch?: boolean
|
||||
squash?: boolean
|
||||
draft?: boolean
|
||||
}
|
||||
|
||||
export interface GitLabUpdateMergeRequestParams extends GitLabBaseParams {
|
||||
projectId: string | number
|
||||
mergeRequestIid: number
|
||||
title?: string
|
||||
description?: string
|
||||
stateEvent?: 'close' | 'reopen'
|
||||
labels?: string
|
||||
assigneeIds?: number[]
|
||||
milestoneId?: number
|
||||
targetBranch?: string
|
||||
removeSourceBranch?: boolean
|
||||
squash?: boolean
|
||||
draft?: boolean
|
||||
}
|
||||
|
||||
export interface GitLabMergeMergeRequestParams extends GitLabBaseParams {
|
||||
projectId: string | number
|
||||
mergeRequestIid: number
|
||||
mergeCommitMessage?: string
|
||||
squashCommitMessage?: string
|
||||
squash?: boolean
|
||||
shouldRemoveSourceBranch?: boolean
|
||||
mergeWhenPipelineSucceeds?: boolean
|
||||
}
|
||||
|
||||
export interface GitLabListPipelinesParams extends GitLabBaseParams {
|
||||
projectId: string | number
|
||||
ref?: string
|
||||
status?:
|
||||
| 'created'
|
||||
| 'waiting_for_resource'
|
||||
| 'preparing'
|
||||
| 'pending'
|
||||
| 'running'
|
||||
| 'success'
|
||||
| 'failed'
|
||||
| 'canceled'
|
||||
| 'skipped'
|
||||
| 'manual'
|
||||
| 'scheduled'
|
||||
orderBy?: 'id' | 'status' | 'ref' | 'updated_at' | 'user_id'
|
||||
sort?: 'asc' | 'desc'
|
||||
perPage?: number
|
||||
page?: number
|
||||
}
|
||||
|
||||
export interface GitLabGetPipelineParams extends GitLabBaseParams {
|
||||
projectId: string | number
|
||||
pipelineId: number
|
||||
}
|
||||
|
||||
export interface GitLabCreatePipelineParams extends GitLabBaseParams {
|
||||
projectId: string | number
|
||||
ref: string
|
||||
variables?: Array<{ key: string; value: string; variable_type?: 'env_var' | 'file' }>
|
||||
}
|
||||
|
||||
export interface GitLabRetryPipelineParams extends GitLabBaseParams {
|
||||
projectId: string | number
|
||||
pipelineId: number
|
||||
}
|
||||
|
||||
export interface GitLabCancelPipelineParams extends GitLabBaseParams {
|
||||
projectId: string | number
|
||||
pipelineId: number
|
||||
}
|
||||
|
||||
export interface GitLabListBranchesParams extends GitLabBaseParams {
|
||||
projectId: string | number
|
||||
search?: string
|
||||
perPage?: number
|
||||
page?: number
|
||||
}
|
||||
|
||||
export interface GitLabCreateBranchParams extends GitLabBaseParams {
|
||||
projectId: string | number
|
||||
branch: string
|
||||
ref: string
|
||||
}
|
||||
|
||||
export interface GitLabDeleteBranchParams extends GitLabBaseParams {
|
||||
projectId: string | number
|
||||
branch: string
|
||||
}
|
||||
|
||||
export interface GitLabCompareBranchesParams extends GitLabBaseParams {
|
||||
projectId: string | number
|
||||
from: string
|
||||
to: string
|
||||
straight?: boolean
|
||||
}
|
||||
|
||||
export interface GitLabListRepositoryTreeParams extends GitLabBaseParams {
|
||||
projectId: string | number
|
||||
path?: string
|
||||
ref?: string
|
||||
recursive?: boolean
|
||||
perPage?: number
|
||||
page?: number
|
||||
}
|
||||
|
||||
export interface GitLabGetFileParams extends GitLabBaseParams {
|
||||
projectId: string | number
|
||||
filePath: string
|
||||
ref: string
|
||||
}
|
||||
|
||||
export interface GitLabCreateFileParams extends GitLabBaseParams {
|
||||
projectId: string | number
|
||||
filePath: string
|
||||
branch: string
|
||||
content: string
|
||||
commitMessage: string
|
||||
}
|
||||
|
||||
export interface GitLabUpdateFileParams extends GitLabBaseParams {
|
||||
projectId: string | number
|
||||
filePath: string
|
||||
branch: string
|
||||
content: string
|
||||
commitMessage: string
|
||||
lastCommitId?: string
|
||||
}
|
||||
|
||||
export interface GitLabListCommitsParams extends GitLabBaseParams {
|
||||
projectId: string | number
|
||||
refName?: string
|
||||
since?: string
|
||||
until?: string
|
||||
path?: string
|
||||
author?: string
|
||||
perPage?: number
|
||||
page?: number
|
||||
}
|
||||
|
||||
export interface GitLabGetMergeRequestChangesParams extends GitLabBaseParams {
|
||||
projectId: string | number
|
||||
mergeRequestIid: number
|
||||
}
|
||||
|
||||
export interface GitLabApproveMergeRequestParams extends GitLabBaseParams {
|
||||
projectId: string | number
|
||||
mergeRequestIid: number
|
||||
sha?: string
|
||||
}
|
||||
|
||||
export interface GitLabListPipelineJobsParams extends GitLabBaseParams {
|
||||
projectId: string | number
|
||||
pipelineId: number
|
||||
scope?: string
|
||||
includeRetried?: boolean
|
||||
perPage?: number
|
||||
page?: number
|
||||
}
|
||||
|
||||
export interface GitLabGetJobLogParams extends GitLabBaseParams {
|
||||
projectId: string | number
|
||||
jobId: number
|
||||
}
|
||||
|
||||
export interface GitLabPlayJobParams extends GitLabBaseParams {
|
||||
projectId: string | number
|
||||
jobId: number
|
||||
}
|
||||
|
||||
export interface GitLabCreateIssueNoteParams extends GitLabBaseParams {
|
||||
projectId: string | number
|
||||
issueIid: number
|
||||
body: string
|
||||
}
|
||||
|
||||
export interface GitLabCreateMergeRequestNoteParams extends GitLabBaseParams {
|
||||
projectId: string | number
|
||||
mergeRequestIid: number
|
||||
body: string
|
||||
}
|
||||
|
||||
export interface GitLabListReleasesParams extends GitLabBaseParams {
|
||||
projectId: string | number
|
||||
orderBy?: 'released_at' | 'created_at'
|
||||
sort?: 'asc' | 'desc'
|
||||
perPage?: number
|
||||
page?: number
|
||||
}
|
||||
|
||||
export interface GitLabCreateReleaseParams extends GitLabBaseParams {
|
||||
projectId: string | number
|
||||
tagName: string
|
||||
name?: string
|
||||
description?: string
|
||||
ref?: string
|
||||
releasedAt?: string
|
||||
milestones?: string[]
|
||||
}
|
||||
|
||||
export interface GitLabListProjectsResponse extends ToolResponse {
|
||||
output: {
|
||||
projects?: GitLabProject[]
|
||||
total?: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitLabGetProjectResponse extends ToolResponse {
|
||||
output: {
|
||||
project?: GitLabProject
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitLabListIssuesResponse extends ToolResponse {
|
||||
output: {
|
||||
issues?: GitLabIssue[]
|
||||
total?: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitLabGetIssueResponse extends ToolResponse {
|
||||
output: {
|
||||
issue?: GitLabIssue
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitLabCreateIssueResponse extends ToolResponse {
|
||||
output: {
|
||||
issue?: GitLabIssue
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitLabUpdateIssueResponse extends ToolResponse {
|
||||
output: {
|
||||
issue?: GitLabIssue
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitLabDeleteIssueResponse extends ToolResponse {
|
||||
output: {
|
||||
success?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitLabListMergeRequestsResponse extends ToolResponse {
|
||||
output: {
|
||||
mergeRequests?: GitLabMergeRequest[]
|
||||
total?: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitLabGetMergeRequestResponse extends ToolResponse {
|
||||
output: {
|
||||
mergeRequest?: GitLabMergeRequest
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitLabCreateMergeRequestResponse extends ToolResponse {
|
||||
output: {
|
||||
mergeRequest?: GitLabMergeRequest
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitLabUpdateMergeRequestResponse extends ToolResponse {
|
||||
output: {
|
||||
mergeRequest?: GitLabMergeRequest
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitLabMergeMergeRequestResponse extends ToolResponse {
|
||||
output: {
|
||||
mergeRequest?: GitLabMergeRequest
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitLabListPipelinesResponse extends ToolResponse {
|
||||
output: {
|
||||
pipelines?: GitLabPipeline[]
|
||||
total?: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitLabGetPipelineResponse extends ToolResponse {
|
||||
output: {
|
||||
pipeline?: GitLabPipeline
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitLabCreatePipelineResponse extends ToolResponse {
|
||||
output: {
|
||||
pipeline?: GitLabPipeline
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitLabRetryPipelineResponse extends ToolResponse {
|
||||
output: {
|
||||
pipeline?: GitLabPipeline
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitLabCancelPipelineResponse extends ToolResponse {
|
||||
output: {
|
||||
pipeline?: GitLabPipeline
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitLabListBranchesResponse extends ToolResponse {
|
||||
output: {
|
||||
branches?: GitLabBranch[]
|
||||
total?: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitLabCreateBranchResponse extends ToolResponse {
|
||||
output: {
|
||||
name?: string | null
|
||||
webUrl?: string | null
|
||||
protected?: boolean | null
|
||||
commit?: GitLabBranch['commit'] | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitLabDeleteBranchResponse extends ToolResponse {
|
||||
output: {
|
||||
success?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitLabCompareBranchesResponse extends ToolResponse {
|
||||
output: {
|
||||
commit?: unknown
|
||||
commits?: unknown[]
|
||||
diffs?: unknown[]
|
||||
compareTimeout?: boolean | null
|
||||
compareSameRef?: boolean | null
|
||||
webUrl?: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitLabCreateNoteResponse extends ToolResponse {
|
||||
output: {
|
||||
note?: GitLabNote
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitLabListReleasesResponse extends ToolResponse {
|
||||
output: {
|
||||
releases?: GitLabRelease[]
|
||||
total?: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitLabCreateReleaseResponse extends ToolResponse {
|
||||
output: {
|
||||
release?: GitLabRelease
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitLabListRepositoryTreeResponse extends ToolResponse {
|
||||
output: {
|
||||
tree?: GitLabTreeEntry[]
|
||||
total?: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitLabGetFileResponse extends ToolResponse {
|
||||
output: {
|
||||
filePath?: string | null
|
||||
fileName?: string | null
|
||||
size?: number | null
|
||||
ref?: string | null
|
||||
blobId?: string | null
|
||||
lastCommitId?: string | null
|
||||
content?: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitLabCreateFileResponse extends ToolResponse {
|
||||
output: {
|
||||
filePath?: string | null
|
||||
branch?: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitLabUpdateFileResponse extends ToolResponse {
|
||||
output: {
|
||||
filePath?: string | null
|
||||
branch?: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitLabListCommitsResponse extends ToolResponse {
|
||||
output: {
|
||||
commits?: GitLabCommit[]
|
||||
total?: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitLabGetMergeRequestChangesResponse extends ToolResponse {
|
||||
output: {
|
||||
mergeRequestIid?: number | null
|
||||
changes?: GitLabMergeRequestChange[]
|
||||
changesCount?: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitLabApproveMergeRequestResponse extends ToolResponse {
|
||||
output: {
|
||||
approvalsRequired?: number | null
|
||||
approvalsLeft?: number | null
|
||||
approvedBy?: unknown[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitLabListPipelineJobsResponse extends ToolResponse {
|
||||
output: {
|
||||
jobs?: GitLabJob[]
|
||||
total?: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitLabGetJobLogResponse extends ToolResponse {
|
||||
output: {
|
||||
log?: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitLabPlayJobResponse extends ToolResponse {
|
||||
output: {
|
||||
id?: number | null
|
||||
name?: string | null
|
||||
status?: string | null
|
||||
webUrl?: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export type GitLabResponse =
|
||||
| GitLabListProjectsResponse
|
||||
| GitLabGetProjectResponse
|
||||
| GitLabListIssuesResponse
|
||||
| GitLabGetIssueResponse
|
||||
| GitLabCreateIssueResponse
|
||||
| GitLabUpdateIssueResponse
|
||||
| GitLabDeleteIssueResponse
|
||||
| GitLabListMergeRequestsResponse
|
||||
| GitLabGetMergeRequestResponse
|
||||
| GitLabCreateMergeRequestResponse
|
||||
| GitLabUpdateMergeRequestResponse
|
||||
| GitLabMergeMergeRequestResponse
|
||||
| GitLabListPipelinesResponse
|
||||
| GitLabGetPipelineResponse
|
||||
| GitLabCreatePipelineResponse
|
||||
| GitLabRetryPipelineResponse
|
||||
| GitLabCancelPipelineResponse
|
||||
| GitLabListBranchesResponse
|
||||
| GitLabCreateBranchResponse
|
||||
| GitLabDeleteBranchResponse
|
||||
| GitLabCompareBranchesResponse
|
||||
| GitLabCreateNoteResponse
|
||||
| GitLabListReleasesResponse
|
||||
| GitLabCreateReleaseResponse
|
||||
| GitLabListRepositoryTreeResponse
|
||||
| GitLabGetFileResponse
|
||||
| GitLabCreateFileResponse
|
||||
| GitLabUpdateFileResponse
|
||||
| GitLabListCommitsResponse
|
||||
| GitLabGetMergeRequestChangesResponse
|
||||
| GitLabApproveMergeRequestResponse
|
||||
| GitLabListPipelineJobsResponse
|
||||
| GitLabGetJobLogResponse
|
||||
| GitLabPlayJobResponse
|
||||
@@ -0,0 +1,118 @@
|
||||
import type { GitLabUpdateFileParams, GitLabUpdateFileResponse } from '@/tools/gitlab/types'
|
||||
import { getGitLabApiBase } from '@/tools/gitlab/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const gitlabUpdateFileTool: ToolConfig<GitLabUpdateFileParams, GitLabUpdateFileResponse> = {
|
||||
id: 'gitlab_update_file',
|
||||
name: 'GitLab Update File',
|
||||
description: 'Update an existing file in a GitLab project repository',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'GitLab Personal Access Token',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or URL-encoded path',
|
||||
},
|
||||
filePath: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Path to the file in the repository',
|
||||
},
|
||||
branch: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Branch to commit the update to',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New file content',
|
||||
},
|
||||
commitMessage: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Commit message',
|
||||
},
|
||||
lastCommitId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Last known commit ID for the file (optimistic locking)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const encodedId = encodeURIComponent(String(params.projectId).trim())
|
||||
const encodedPath = encodeURIComponent(String(params.filePath))
|
||||
return `${getGitLabApiBase(params.host)}/projects/${encodedId}/repository/files/${encodedPath}`
|
||||
},
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
'PRIVATE-TOKEN': params.accessToken,
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {
|
||||
branch: params.branch,
|
||||
content: params.content,
|
||||
commit_message: params.commitMessage,
|
||||
encoding: 'text',
|
||||
}
|
||||
|
||||
if (params.lastCommitId) body.last_commit_id = params.lastCommitId
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return {
|
||||
success: false,
|
||||
error: `GitLab API error: ${response.status} ${errorText}`,
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
filePath: data.file_path ?? null,
|
||||
branch: data.branch ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
filePath: {
|
||||
type: 'string',
|
||||
description: 'The updated file path',
|
||||
},
|
||||
branch: {
|
||||
type: 'string',
|
||||
description: 'The branch the update was committed to',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import type { GitLabUpdateIssueParams, GitLabUpdateIssueResponse } from '@/tools/gitlab/types'
|
||||
import { getGitLabApiBase } from '@/tools/gitlab/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const gitlabUpdateIssueTool: ToolConfig<GitLabUpdateIssueParams, GitLabUpdateIssueResponse> =
|
||||
{
|
||||
id: 'gitlab_update_issue',
|
||||
name: 'GitLab Update Issue',
|
||||
description: 'Update an existing issue in a GitLab project',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'GitLab Personal Access Token',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or URL-encoded path',
|
||||
},
|
||||
issueIid: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Issue internal ID (IID)',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New issue title',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New issue description (Markdown supported)',
|
||||
},
|
||||
stateEvent: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'State event (close or reopen)',
|
||||
},
|
||||
labels: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated list of label names',
|
||||
},
|
||||
assigneeIds: {
|
||||
type: 'array',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Array of user IDs to assign',
|
||||
},
|
||||
milestoneId: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Milestone ID to assign',
|
||||
},
|
||||
dueDate: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Due date in YYYY-MM-DD format',
|
||||
},
|
||||
confidential: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether the issue is confidential',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const encodedId = encodeURIComponent(String(params.projectId).trim())
|
||||
return `${getGitLabApiBase(params.host)}/projects/${encodedId}/issues/${params.issueIid}`
|
||||
},
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
'PRIVATE-TOKEN': params.accessToken,
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, any> = {}
|
||||
|
||||
if (params.title) body.title = params.title
|
||||
if (params.description !== undefined) body.description = params.description
|
||||
if (params.stateEvent) body.state_event = params.stateEvent
|
||||
if (params.labels !== undefined) body.labels = params.labels
|
||||
if (params.assigneeIds) body.assignee_ids = params.assigneeIds
|
||||
if (params.milestoneId !== undefined) body.milestone_id = params.milestoneId
|
||||
if (params.dueDate !== undefined) body.due_date = params.dueDate
|
||||
if (params.confidential !== undefined) body.confidential = params.confidential
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return {
|
||||
success: false,
|
||||
error: `GitLab API error: ${response.status} ${errorText}`,
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const issue = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
issue,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
issue: {
|
||||
type: 'object',
|
||||
description: 'The updated GitLab issue',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import type {
|
||||
GitLabUpdateMergeRequestParams,
|
||||
GitLabUpdateMergeRequestResponse,
|
||||
} from '@/tools/gitlab/types'
|
||||
import { getGitLabApiBase } from '@/tools/gitlab/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const gitlabUpdateMergeRequestTool: ToolConfig<
|
||||
GitLabUpdateMergeRequestParams,
|
||||
GitLabUpdateMergeRequestResponse
|
||||
> = {
|
||||
id: 'gitlab_update_merge_request',
|
||||
name: 'GitLab Update Merge Request',
|
||||
description: 'Update an existing merge request in a GitLab project',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'GitLab Personal Access Token',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or URL-encoded path',
|
||||
},
|
||||
mergeRequestIid: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Merge request internal ID (IID)',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New merge request title',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New merge request description',
|
||||
},
|
||||
stateEvent: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'State event (close or reopen)',
|
||||
},
|
||||
labels: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated list of label names',
|
||||
},
|
||||
assigneeIds: {
|
||||
type: 'array',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Array of user IDs to assign',
|
||||
},
|
||||
milestoneId: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Milestone ID to assign',
|
||||
},
|
||||
targetBranch: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New target branch',
|
||||
},
|
||||
removeSourceBranch: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Delete source branch after merge',
|
||||
},
|
||||
squash: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Squash commits on merge',
|
||||
},
|
||||
draft: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Mark as draft (work in progress)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const encodedId = encodeURIComponent(String(params.projectId).trim())
|
||||
return `${getGitLabApiBase(params.host)}/projects/${encodedId}/merge_requests/${params.mergeRequestIid}`
|
||||
},
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
'PRIVATE-TOKEN': params.accessToken,
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, any> = {}
|
||||
|
||||
if (params.title) body.title = params.title
|
||||
if (params.description !== undefined) body.description = params.description
|
||||
if (params.stateEvent) body.state_event = params.stateEvent
|
||||
if (params.labels !== undefined) body.labels = params.labels
|
||||
if (params.assigneeIds !== undefined) body.assignee_ids = params.assigneeIds
|
||||
if (params.milestoneId !== undefined) body.milestone_id = params.milestoneId
|
||||
if (params.targetBranch) body.target_branch = params.targetBranch
|
||||
if (params.removeSourceBranch !== undefined)
|
||||
body.remove_source_branch = params.removeSourceBranch
|
||||
if (params.squash !== undefined) body.squash = params.squash
|
||||
if (params.draft !== undefined) body.draft = params.draft
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return {
|
||||
success: false,
|
||||
error: `GitLab API error: ${response.status} ${errorText}`,
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const mergeRequest = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
mergeRequest,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
mergeRequest: {
|
||||
type: 'object',
|
||||
description: 'The updated GitLab merge request',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getGitLabApiBase, normalizeGitLabHost, UnsafeGitLabHostError } from '@/tools/gitlab/utils'
|
||||
|
||||
describe('normalizeGitLabHost', () => {
|
||||
it('defaults to gitlab.com when the host is empty, blank, or not a string', () => {
|
||||
expect(normalizeGitLabHost(undefined)).toBe('gitlab.com')
|
||||
expect(normalizeGitLabHost(null)).toBe('gitlab.com')
|
||||
expect(normalizeGitLabHost('')).toBe('gitlab.com')
|
||||
expect(normalizeGitLabHost(' ')).toBe('gitlab.com')
|
||||
expect(normalizeGitLabHost(42)).toBe('gitlab.com')
|
||||
})
|
||||
|
||||
it('strips protocol and trailing slashes from a self-managed host', () => {
|
||||
expect(normalizeGitLabHost('gitlab.example.com')).toBe('gitlab.example.com')
|
||||
expect(normalizeGitLabHost('https://gitlab.example.com')).toBe('gitlab.example.com')
|
||||
expect(normalizeGitLabHost('http://gitlab.example.com/')).toBe('gitlab.example.com')
|
||||
expect(normalizeGitLabHost(' https://gitlab.example.com// ')).toBe('gitlab.example.com')
|
||||
})
|
||||
|
||||
it('preserves an explicit port and IDN punycode labels', () => {
|
||||
expect(normalizeGitLabHost('gitlab.example.com:8443')).toBe('gitlab.example.com:8443')
|
||||
expect(normalizeGitLabHost('xn--80ak6aa92e.com')).toBe('xn--80ak6aa92e.com')
|
||||
})
|
||||
|
||||
it('rejects hosts that could redirect the request authority (SSRF / token exfiltration)', () => {
|
||||
const unsafe = [
|
||||
'legit.com@evil.com',
|
||||
'user:pass@evil.com',
|
||||
'gitlab.com#@evil.com',
|
||||
'gitlab.com /api',
|
||||
'line\nbreak.com',
|
||||
'evil.com/path',
|
||||
'evil.com?x=1',
|
||||
'[::1]',
|
||||
'a..b.com',
|
||||
'.gitlab.com',
|
||||
'gitlab.com.',
|
||||
]
|
||||
for (const host of unsafe) {
|
||||
expect(() => normalizeGitLabHost(host), host).toThrow(UnsafeGitLabHostError)
|
||||
}
|
||||
})
|
||||
|
||||
it('accepts bare IP literals at the STRUCTURAL layer by design (private/metadata IPs are rejected later by the fetch-layer DNS guard)', () => {
|
||||
// This guard is structural only — it prevents authority confusion (userinfo,
|
||||
// path, whitespace). SSRF to private/loopback/metadata addresses is the
|
||||
// responsibility of validateUrlWithDNS / secureFetchWithValidation at fetch
|
||||
// time, the single SSRF chokepoint shared by tools, webhooks, and connectors.
|
||||
// These hosts are therefore structurally valid here, then blocked at fetch.
|
||||
expect(normalizeGitLabHost('127.0.0.1')).toBe('127.0.0.1')
|
||||
expect(normalizeGitLabHost('169.254.169.254')).toBe('169.254.169.254')
|
||||
expect(normalizeGitLabHost('localhost')).toBe('localhost')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getGitLabApiBase', () => {
|
||||
it('builds the v4 REST base for the default and self-managed hosts', () => {
|
||||
expect(getGitLabApiBase(undefined)).toBe('https://gitlab.com/api/v4')
|
||||
expect(getGitLabApiBase('gitlab.example.com')).toBe('https://gitlab.example.com/api/v4')
|
||||
expect(getGitLabApiBase('https://gitlab.example.com:8443/')).toBe(
|
||||
'https://gitlab.example.com:8443/api/v4'
|
||||
)
|
||||
})
|
||||
|
||||
it('propagates rejection of unsafe hosts', () => {
|
||||
expect(() => getGitLabApiBase('legit.com@evil.com')).toThrow(UnsafeGitLabHostError)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,68 @@
|
||||
const DEFAULT_GITLAB_HOST = 'gitlab.com'
|
||||
|
||||
/**
|
||||
* Error thrown when a user-supplied GitLab host is structurally unsafe to use
|
||||
* as the target of a server-side request that carries the user's access token.
|
||||
*/
|
||||
export class UnsafeGitLabHostError extends Error {
|
||||
constructor(rawHost: string) {
|
||||
super(`Invalid GitLab host: ${rawHost}`)
|
||||
this.name = 'UnsafeGitLabHostError'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rejects a host that is structurally unsafe to fetch with the caller's token.
|
||||
*
|
||||
* The host is later interpolated into `https://<host>/api/v4`, so anything that
|
||||
* could change the request's authority (userinfo `@`, an embedded path/query/
|
||||
* fragment, whitespace, or control characters) must be rejected to prevent the
|
||||
* `PRIVATE-TOKEN` header from being sent to an attacker-controlled origin. The
|
||||
* allowed alphabet is hostname labels plus an optional `:port`, so self-managed
|
||||
* hosts such as `gitlab.example.com` or `gitlab.example.com:8443` keep working.
|
||||
* This is a structural guard only; DNS-based private-IP/SSRF checks remain the
|
||||
* responsibility of the fetch layer.
|
||||
*/
|
||||
function assertSafeGitLabHostString(host: string, rawHost: string): void {
|
||||
const hostnameWithoutPort = host.replace(/:\d+$/, '')
|
||||
const allowedHostChars = /^[A-Za-z0-9.-]+$/
|
||||
if (!allowedHostChars.test(hostnameWithoutPort)) {
|
||||
throw new UnsafeGitLabHostError(rawHost)
|
||||
}
|
||||
if (hostnameWithoutPort.startsWith('.') || hostnameWithoutPort.endsWith('.')) {
|
||||
throw new UnsafeGitLabHostError(rawHost)
|
||||
}
|
||||
if (hostnameWithoutPort.split('.').some((label) => label.length === 0)) {
|
||||
throw new UnsafeGitLabHostError(rawHost)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a GitLab host value: trims whitespace, strips any protocol prefix
|
||||
* and trailing slashes, validates that the result is a bare host (optionally
|
||||
* with a port), and falls back to gitlab.com when empty. Mirrors the GitLab
|
||||
* connector so tools, triggers, and connectors resolve hosts identically.
|
||||
*
|
||||
* @throws {UnsafeGitLabHostError} when a non-empty host is structurally unsafe.
|
||||
*/
|
||||
export function normalizeGitLabHost(rawHost: unknown): string {
|
||||
const raw = typeof rawHost === 'string' ? rawHost.trim() : ''
|
||||
if (!raw) return DEFAULT_GITLAB_HOST
|
||||
const host = raw
|
||||
.replace(/^https?:\/\//i, '')
|
||||
.replace(/\/+$/, '')
|
||||
.trim()
|
||||
if (!host) return DEFAULT_GITLAB_HOST
|
||||
assertSafeGitLabHostString(host, String(rawHost))
|
||||
return host
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the REST API v4 base URL for the configured host. Defaults to
|
||||
* gitlab.com so existing workflows that never set a host keep working.
|
||||
*
|
||||
* @throws {UnsafeGitLabHostError} when a non-empty host is structurally unsafe.
|
||||
*/
|
||||
export function getGitLabApiBase(rawHost: unknown): string {
|
||||
return `https://${normalizeGitLabHost(rawHost)}/api/v4`
|
||||
}
|
||||
Reference in New Issue
Block a user