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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -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',
},
},
}
+78
View File
@@ -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',
},
},
}
+123
View File
@@ -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',
},
},
}
+102
View File
@@ -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',
},
},
}
+106
View File
@@ -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',
},
},
}
+127
View File
@@ -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',
},
},
}
+97
View File
@@ -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',
},
},
}
+122
View File
@@ -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',
},
},
}
+77
View File
@@ -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',
},
},
}
+74
View File
@@ -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',
},
},
}
+114
View File
@@ -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',
},
},
}
+75
View File
@@ -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',
},
},
}
+75
View File
@@ -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',
},
},
}
+76
View File
@@ -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',
},
},
}
+69
View File
@@ -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',
},
},
}
+82
View File
@@ -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,
}
+103
View File
@@ -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',
},
},
}
+129
View File
@@ -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',
},
},
}
+143
View File
@@ -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',
},
},
}
+119
View File
@@ -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',
},
},
}
+125
View File
@@ -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',
},
},
}
+130
View File
@@ -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',
},
},
}
+110
View File
@@ -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',
},
},
}
+90
View File
@@ -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',
},
},
}
+78
View File
@@ -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',
},
},
}
+791
View File
@@ -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
+118
View File
@@ -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',
},
},
}
+139
View File
@@ -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',
},
},
}
+71
View File
@@ -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)
})
})
+68
View File
@@ -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`
}