Files
WeHub Mirror 6bf8bebf51
CI / Test and Build (push) Failing after 1s
CI / Migrate Dev DB (push) Has been skipped
CI / Migrate DB (push) Has been skipped
CodeQL / Analyze actions (push) Has been cancelled
CodeQL / Analyze javascript-typescript (push) Has been cancelled
CI / Detect Version (push) Has been cancelled
CI / Detect Desktop Changes (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/cron.Dockerfile, ubuntu-latest, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build AMD64 (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/cron.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/db.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/pii.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/realtime.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-8vcpu-ubuntu-2404-arm, ./docker/app.Dockerfile, linux-arm64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
Helm Chart / Lint, test, and validate chart (push) Has been cancelled
Helm Chart / Chart version bumped (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Build Dev ECR (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core) (push) Has been cancelled
CI / Promote Images (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/cron) (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 / Build Dev ECR (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Check Desktop Signing Secrets (push) Has been cancelled
CI / Desktop Release (push) Has been cancelled
CI / Create Desktop Prerelease (push) Has been cancelled
CI / Desktop Prerelease Build (push) Has been cancelled
CI / Publish Desktop Prerelease (push) Has been cancelled
CI / Prune Desktop Prereleases (push) Has been cancelled
Helm Chart / Install on kind and run helm test (push) Has been cancelled
WeHub snapshot of cb28d14c6f2c081de7a0d8729a8c816c9adef67a
2026-08-10 11:17:50 +08:00

413 lines
14 KiB
TypeScript

import {
isRecord,
nullableBoolean,
nullableString,
optionalString,
readGitHubErrorMessage,
requiredBoolean,
requiredNumber,
requiredString,
} from '@/tools/github/response-parsers'
import type {
GitHubPullRequestBranch,
GitHubPullRequestFile,
GitHubPullRequestUser,
GitHubPullRequestV2Output,
PROperationParams,
PRV2OperationParams,
PullRequestResponse,
PullRequestV2Response,
} from '@/tools/github/types'
import { PR_BRANCH_REF_OUTPUT, PR_FILE_OUTPUT_PROPERTIES, USER_OUTPUT } from '@/tools/github/types'
import type { ToolConfig } from '@/tools/types'
type GitHubPullRequest = Omit<GitHubPullRequestV2Output, 'files'>
type PullRequestFilesResult =
| { success: true; files: GitHubPullRequestFile[] }
| { success: false; error: string }
const PULL_REQUEST_FILES_PER_PAGE = 100
const MAX_PULL_REQUEST_FILES = 3_000
function parsePullRequestUser(value: unknown, context: string): GitHubPullRequestUser {
if (!isRecord(value)) throw new Error(`${context} must be an object`)
return {
login: requiredString(value, 'login', context),
id: requiredNumber(value, 'id', context),
avatar_url: requiredString(value, 'avatar_url', context),
html_url: requiredString(value, 'html_url', context),
type: requiredString(value, 'type', context),
}
}
function parseNullablePullRequestUser(
value: unknown,
context: string
): GitHubPullRequestUser | null {
if (value === null) return null
return parsePullRequestUser(value, context)
}
/**
* A branch's repository is absent on some payloads and explicitly null when the
* repository is gone (a deleted fork), so both read as "unknown" rather than as
* a malformed response.
*/
function parseBranchRepoFullName(repo: unknown, context: string): string | null {
if (repo === null || repo === undefined) return null
if (!isRecord(repo)) throw new Error(`${context} must be an object or null`)
return requiredString(repo, 'full_name', context)
}
function parsePullRequestBranch(value: unknown, context: string): GitHubPullRequestBranch {
if (!isRecord(value)) throw new Error(`${context} must be an object`)
return {
label: requiredString(value, 'label', context),
ref: requiredString(value, 'ref', context),
sha: requiredString(value, 'sha', context),
repo_full_name: parseBranchRepoFullName(value.repo, `${context}.repo`),
}
}
function parsePullRequest(value: unknown): GitHubPullRequest {
if (!isRecord(value)) throw new Error('GitHub pull request response must be an object')
return {
id: requiredNumber(value, 'id', 'pull_request'),
number: requiredNumber(value, 'number', 'pull_request'),
title: requiredString(value, 'title', 'pull_request'),
state: requiredString(value, 'state', 'pull_request'),
html_url: requiredString(value, 'html_url', 'pull_request'),
diff_url: requiredString(value, 'diff_url', 'pull_request'),
body: nullableString(value, 'body', 'pull_request'),
user: parsePullRequestUser(value.user, 'pull_request.user'),
head: parsePullRequestBranch(value.head, 'pull_request.head'),
base: parsePullRequestBranch(value.base, 'pull_request.base'),
merged: requiredBoolean(value, 'merged', 'pull_request'),
mergeable: nullableBoolean(value, 'mergeable', 'pull_request'),
merged_by: parseNullablePullRequestUser(value.merged_by, 'pull_request.merged_by'),
comments: requiredNumber(value, 'comments', 'pull_request'),
review_comments: requiredNumber(value, 'review_comments', 'pull_request'),
commits: requiredNumber(value, 'commits', 'pull_request'),
additions: requiredNumber(value, 'additions', 'pull_request'),
deletions: requiredNumber(value, 'deletions', 'pull_request'),
changed_files: requiredNumber(value, 'changed_files', 'pull_request'),
created_at: requiredString(value, 'created_at', 'pull_request'),
updated_at: requiredString(value, 'updated_at', 'pull_request'),
closed_at: nullableString(value, 'closed_at', 'pull_request'),
merged_at: nullableString(value, 'merged_at', 'pull_request'),
}
}
function parsePullRequestFile(value: unknown, index: number): GitHubPullRequestFile {
const context = `pull_request_files[${index}]`
if (!isRecord(value)) throw new Error(`${context} must be an object`)
const patch = optionalString(value, 'patch', context)
const previousFilename = optionalString(value, 'previous_filename', context)
return {
sha: requiredString(value, 'sha', context),
filename: requiredString(value, 'filename', context),
status: requiredString(value, 'status', context),
additions: requiredNumber(value, 'additions', context),
deletions: requiredNumber(value, 'deletions', context),
changes: requiredNumber(value, 'changes', context),
blob_url: requiredString(value, 'blob_url', context),
raw_url: requiredString(value, 'raw_url', context),
contents_url: requiredString(value, 'contents_url', context),
...(patch === undefined ? {} : { patch }),
...(previousFilename === undefined ? {} : { previous_filename: previousFilename }),
}
}
function parsePullRequestFiles(value: unknown): GitHubPullRequestFile[] {
if (!Array.isArray(value)) throw new Error('GitHub pull request files response must be an array')
return value.map(parsePullRequestFile)
}
async function parsePullRequestResponse(response: Response): Promise<GitHubPullRequest> {
if (!response.ok) {
throw new Error(
(await readGitHubErrorMessage(response)) ??
`Failed to fetch pull request (HTTP ${response.status})`
)
}
const value: unknown = await response.json()
return parsePullRequest(value)
}
async function fetchPullRequestFiles(
params: PROperationParams,
pullNumber: number
): Promise<PullRequestFilesResult> {
const files: GitHubPullRequestFile[] = []
const maxPages = MAX_PULL_REQUEST_FILES / PULL_REQUEST_FILES_PER_PAGE
for (let page = 1; page <= maxPages; page += 1) {
const response = await fetch(
`https://api.github.com/repos/${params.owner}/${params.repo}/pulls/${pullNumber}/files?per_page=${PULL_REQUEST_FILES_PER_PAGE}&page=${page}`,
{
headers: {
Accept: 'application/vnd.github+json',
Authorization: `Bearer ${params.apiKey}`,
'X-GitHub-Api-Version': '2022-11-28',
},
}
)
if (!response.ok) {
return {
success: false,
error:
(await readGitHubErrorMessage(response)) ??
`Failed to fetch PR files (HTTP ${response.status})`,
}
}
const value: unknown = await response.json()
const pageFiles = parsePullRequestFiles(value)
if (pageFiles.length > PULL_REQUEST_FILES_PER_PAGE) {
throw new Error(
`GitHub returned more than ${PULL_REQUEST_FILES_PER_PAGE} pull request files in one page`
)
}
files.push(...pageFiles)
if (pageFiles.length < PULL_REQUEST_FILES_PER_PAGE) break
}
return { success: true, files }
}
function requireParams<T extends PROperationParams>(params: T | undefined): T {
if (!params) throw new Error('GitHub PR reader parameters are required')
return params
}
const PR_PARAMS = {
owner: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Repository owner',
},
repo: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Repository name',
},
pullNumber: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Pull request number',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'GitHub API token',
},
} satisfies ToolConfig<PROperationParams, PullRequestResponse>['params']
export const prTool: ToolConfig<PROperationParams, PullRequestResponse> = {
id: 'github_pr',
name: 'GitHub PR Reader',
description: 'Fetch PR details including diff and files changed',
version: '1.0.0',
params: PR_PARAMS,
request: {
url: (params) =>
`https://api.github.com/repos/${params.owner}/${params.repo}/pulls/${params.pullNumber}`,
method: 'GET',
headers: (params) => ({
Accept: 'application/vnd.github.v3+json',
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response, params) => {
const requestParams = requireParams(params)
const pr = await parsePullRequestResponse(response)
const filesResult = await fetchPullRequestFiles(requestParams, pr.number)
if (!filesResult.success) {
return {
success: false,
error: filesResult.error,
output: {
content: '',
metadata: {
number: pr.number,
title: pr.title,
state: pr.state,
html_url: pr.html_url,
diff_url: pr.diff_url,
created_at: pr.created_at,
updated_at: pr.updated_at,
files: [],
},
},
}
}
const content = `PR #${pr.number}: "${pr.title}" (${pr.state}) - Created: ${pr.created_at}, Updated: ${pr.updated_at}
Description: ${pr.body || 'No description'}
Files changed: ${filesResult.files.length}
URL: ${pr.html_url}`
return {
success: true,
output: {
content,
metadata: {
number: pr.number,
title: pr.title,
state: pr.state,
html_url: pr.html_url,
diff_url: pr.diff_url,
created_at: pr.created_at,
updated_at: pr.updated_at,
files: filesResult.files.map((file) => ({
filename: file.filename,
additions: file.additions,
deletions: file.deletions,
changes: file.changes,
patch: file.patch,
blob_url: file.blob_url,
raw_url: file.raw_url,
status: file.status,
})),
},
},
}
},
outputs: {
content: { type: 'string', description: 'Human-readable PR summary' },
metadata: {
type: 'object',
description: 'Detailed PR metadata including file changes',
properties: {
number: { type: 'number', description: 'Pull request number' },
title: { type: 'string', description: 'PR title' },
state: { type: 'string', description: 'PR state (open/closed/merged)' },
html_url: { type: 'string', description: 'GitHub web URL' },
diff_url: { type: 'string', description: 'Raw diff URL' },
created_at: { type: 'string', description: 'Creation timestamp' },
updated_at: { type: 'string', description: 'Last update timestamp' },
files: {
type: 'array',
description: 'Files changed in the PR',
items: {
type: 'object',
properties: {
filename: { type: 'string', description: 'File path' },
additions: { type: 'number', description: 'Lines added' },
deletions: { type: 'number', description: 'Lines deleted' },
changes: { type: 'number', description: 'Total changes' },
patch: { type: 'string', description: 'File diff patch', optional: true },
blob_url: { type: 'string', description: 'GitHub blob URL' },
raw_url: { type: 'string', description: 'Raw file URL' },
status: { type: 'string', description: 'Change type (added/modified/deleted)' },
},
},
},
},
},
},
}
export const prV2Tool: ToolConfig<PRV2OperationParams, PullRequestV2Response> = {
id: 'github_pr_v2',
name: prTool.name,
description: prTool.description,
version: '2.0.0',
params: {
...PR_PARAMS,
includeFiles: {
type: 'boolean',
required: false,
default: true,
visibility: 'user-or-llm',
description: 'Whether to fetch changed-file details from the separate files endpoint',
},
},
request: prTool.request,
transformResponse: async (response: Response, params) => {
const requestParams = requireParams(params)
const pr = await parsePullRequestResponse(response)
if (requestParams.includeFiles !== false) {
const filesResult = await fetchPullRequestFiles(requestParams, pr.number)
if (!filesResult.success) {
return {
success: false,
error: filesResult.error,
output: {
...pr,
},
}
}
return {
success: true,
output: {
...pr,
files: filesResult.files,
},
}
}
return {
success: true,
output: {
...pr,
},
}
},
outputs: {
id: { type: 'number', description: 'Pull request ID' },
number: { type: 'number', description: 'Pull request number' },
title: { type: 'string', description: 'PR title' },
state: { type: 'string', description: 'PR state (open/closed)' },
html_url: { type: 'string', description: 'GitHub web URL' },
diff_url: { type: 'string', description: 'Raw diff URL' },
body: { type: 'string', description: 'PR description', nullable: true },
user: USER_OUTPUT,
head: PR_BRANCH_REF_OUTPUT,
base: PR_BRANCH_REF_OUTPUT,
merged: { type: 'boolean', description: 'Whether PR is merged' },
mergeable: { type: 'boolean', description: 'Whether PR is mergeable', nullable: true },
merged_by: { ...USER_OUTPUT, nullable: true },
comments: { type: 'number', description: 'Number of comments' },
review_comments: { type: 'number', description: 'Number of review comments' },
commits: { type: 'number', description: 'Number of commits' },
additions: { type: 'number', description: 'Lines added' },
deletions: { type: 'number', description: 'Lines deleted' },
changed_files: { type: 'number', description: 'Number of changed files' },
created_at: { type: 'string', description: 'Creation timestamp' },
updated_at: { type: 'string', description: 'Last update timestamp' },
closed_at: { type: 'string', description: 'Close timestamp', nullable: true },
merged_at: { type: 'string', description: 'Merge timestamp', nullable: true },
files: {
type: 'array',
description: 'Array of changed file objects',
optional: true,
items: {
type: 'object',
properties: PR_FILE_OUTPUT_PROPERTIES,
},
},
},
}