From 4c59baf25325bfa5e6a407391a583d71b0957fb8 Mon Sep 17 00:00:00 2001 From: Hugo Richard Date: Thu, 26 Feb 2026 17:40:14 +0100 Subject: [PATCH 1/2] feat: add repository management tools --- apps/chat/shared/utils/tools/github.ts | 3 + .../docs/1.getting-started/1.introduction.md | 4 +- apps/docs/content/docs/2.guide/2.presets.md | 2 +- .../docs/2.guide/3.approval-control.md | 6 + .../docs/2.guide/5.token-and-permissions.md | 12 +- .../content/docs/3.api/1.tools-catalog.md | 5 +- packages/github-tools/src/index.ts | 12 +- packages/github-tools/src/tools/repository.ts | 105 ++++++++++++++++++ 8 files changed, 136 insertions(+), 13 deletions(-) diff --git a/apps/chat/shared/utils/tools/github.ts b/apps/chat/shared/utils/tools/github.ts index 3ad8ddb..0c9c066 100644 --- a/apps/chat/shared/utils/tools/github.ts +++ b/apps/chat/shared/utils/tools/github.ts @@ -14,6 +14,9 @@ export const GITHUB_TOOL_META: Record = { getRepository: { title: 'Get Repository', label: 'Repository fetched', labelActive: 'Fetching repository', icon: 'i-simple-icons-github' }, listBranches: { title: 'List Branches', label: 'Branches listed', labelActive: 'Listing branches', icon: 'i-lucide-git-branch' }, getFileContent: { title: 'Get File Content', label: 'File read', labelActive: 'Reading file', icon: 'i-lucide-file-code' }, + createBranch: { title: 'Create Branch', label: 'Branch created', labelActive: 'Creating branch', icon: 'i-lucide-git-branch-plus' }, + forkRepository: { title: 'Fork Repository', label: 'Repository forked', labelActive: 'Forking repository', icon: 'i-lucide-git-fork' }, + createRepository: { title: 'Create Repository', label: 'Repository created', labelActive: 'Creating repository', icon: 'i-lucide-plus' }, createOrUpdateFile: { title: 'Create / Update File', label: 'File updated', labelActive: 'Updating file', icon: 'i-lucide-file-pen' }, listPullRequests: { title: 'List Pull Requests', label: 'Pull requests listed', labelActive: 'Listing pull requests', icon: 'i-lucide-git-pull-request' }, getPullRequest: { title: 'Get Pull Request', label: 'Pull request fetched', labelActive: 'Fetching pull request', icon: 'i-lucide-git-pull-request' }, diff --git a/apps/docs/content/docs/1.getting-started/1.introduction.md b/apps/docs/content/docs/1.getting-started/1.introduction.md index 940488e..5a85278 100644 --- a/apps/docs/content/docs/1.getting-started/1.introduction.md +++ b/apps/docs/content/docs/1.getting-started/1.introduction.md @@ -21,7 +21,7 @@ Pass them to `generateText`, `streamText`, or any agent loop — the model decid ## Explore the tools -The SDK ships **18 tools** covering repositories, pull requests, issues, commits, and code search. Each tool maps to a single GitHub REST endpoint and is fully typed with [Zod](https://zod.dev) schemas. +The SDK ships **21 tools** covering repositories, branches, pull requests, issues, commits, and code search. Each tool maps to a single GitHub REST endpoint and is fully typed with [Zod](https://zod.dev) schemas. Browse the full list in the [Tools Catalog](/api/tools-catalog). @@ -63,7 +63,7 @@ See [Examples](/guide/examples) for more agent patterns. title: Tools to: /api/tools-catalog --- - 18 individual GitHub operations you wire into any AI SDK call. + 21 individual GitHub operations you wire into any AI SDK call. ::: :::card diff --git a/apps/docs/content/docs/2.guide/2.presets.md b/apps/docs/content/docs/2.guide/2.presets.md index 9a92c1b..efc174f 100644 --- a/apps/docs/content/docs/2.guide/2.presets.md +++ b/apps/docs/content/docs/2.guide/2.presets.md @@ -48,7 +48,7 @@ const tools = createGithubTools({ | `repo-explorer` | repository metadata, branches, file content, code search | knowledge retrieval, repo Q&A | | `code-review` | pull requests, commits, file diffs, review comments | PR copilots, change summaries | | `issue-triage` | issues, labels, comments, close/create | support triage, backlog bots | -| `maintainer` | all tool families | operator workflows with strict approvals | +| `maintainer` | all tool families including branch creation, forking, and repo creation | operator workflows with strict approvals | ## Pair presets with token scopes diff --git a/apps/docs/content/docs/2.guide/3.approval-control.md b/apps/docs/content/docs/2.guide/3.approval-control.md index 518cdc1..b5a3ed9 100644 --- a/apps/docs/content/docs/2.guide/3.approval-control.md +++ b/apps/docs/content/docs/2.guide/3.approval-control.md @@ -50,6 +50,9 @@ import { createGithubTools } from '@github-tools/sdk' const tools = createGithubTools({ requireApproval: { + createBranch: false, + forkRepository: true, + createRepository: true, mergePullRequest: true, createOrUpdateFile: true, closeIssue: true, @@ -65,10 +68,13 @@ const tools = createGithubTools({ | Operation | Risk | Suggested policy | |---|---|---| +| `createRepository` | High | Always require approval | +| `forkRepository` | High | Always require approval | | `createOrUpdateFile` | High | Always require approval | | `mergePullRequest` | High | Always require approval | | `closeIssue` | Medium | Require in production repos | | `createPullRequest` | Medium | Optional in trusted CI | +| `createBranch` | Low | Usually skip | | `addPullRequestComment` | Low | Usually skip | | `addIssueComment` | Low | Usually skip | diff --git a/apps/docs/content/docs/2.guide/5.token-and-permissions.md b/apps/docs/content/docs/2.guide/5.token-and-permissions.md index 3701f45..b1dac60 100644 --- a/apps/docs/content/docs/2.guide/5.token-and-permissions.md +++ b/apps/docs/content/docs/2.guide/5.token-and-permissions.md @@ -27,12 +27,12 @@ Fine-grained personal access tokens let you restrict access per repository and p ## Map permissions to presets -| Preset | Repository access | Contents | Pull requests | Issues | -|---|---|---|---|---| -| `repo-explorer` | selected repos | `read` | — | — | -| `code-review` | selected repos | `read` | `read` (or `write` for comments) | — | -| `issue-triage` | selected repos | `read` | — | `write` | -| `maintainer` | selected repos | `write` | `write` | `write` | +| Preset | Repository access | Contents | Pull requests | Issues | Administration | +|---|---|---|---|---|---| +| `repo-explorer` | selected repos | `read` | — | — | — | +| `code-review` | selected repos | `read` | `read` (or `write` for comments) | — | — | +| `issue-triage` | selected repos | `read` | — | `write` | — | +| `maintainer` | selected repos | `write` | `write` | `write` | `write` (for repo creation and forking) | ## Apply least-privilege step by step diff --git a/apps/docs/content/docs/3.api/1.tools-catalog.md b/apps/docs/content/docs/3.api/1.tools-catalog.md index 8520bb8..80a4a8f 100644 --- a/apps/docs/content/docs/3.api/1.tools-catalog.md +++ b/apps/docs/content/docs/3.api/1.tools-catalog.md @@ -17,13 +17,16 @@ links: ## Repository tools -Available in all presets. These tools read repository metadata and file content: +Available in all presets. These tools manage repositories, branches, and file content: | Tool | Capability | Write | |---|---|---| | `getRepository` | read repository metadata (name, description, stars, language) | — | | `listBranches` | list branches and their HEAD commits | — | | `getFileContent` | read a file at a specific path and ref | — | +| `createBranch` | create a new branch from an existing branch or commit SHA | Yes | +| `forkRepository` | fork a repository to your account or an organization | Yes | +| `createRepository` | create a new repository for the authenticated user or an organization | Yes | | `createOrUpdateFile` | create or update a file in the repository | Yes | ## Pull request tools diff --git a/packages/github-tools/src/index.ts b/packages/github-tools/src/index.ts index d42841c..280f664 100644 --- a/packages/github-tools/src/index.ts +++ b/packages/github-tools/src/index.ts @@ -1,11 +1,14 @@ import { createOctokit } from './client' -import { getRepository, listBranches, getFileContent, createOrUpdateFile } from './tools/repository' +import { getRepository, listBranches, getFileContent, createBranch, forkRepository, createRepository, createOrUpdateFile } from './tools/repository' import { listPullRequests, getPullRequest, createPullRequest, mergePullRequest, addPullRequestComment } from './tools/pull-requests' import { listIssues, getIssue, createIssue, addIssueComment, closeIssue } from './tools/issues' import { searchCode, searchRepositories } from './tools/search' import { listCommits, getCommit } from './tools/commits' export type GithubWriteToolName = + | 'createBranch' + | 'forkRepository' + | 'createRepository' | 'createOrUpdateFile' | 'createPullRequest' | 'mergePullRequest' @@ -60,7 +63,7 @@ const PRESET_TOOLS: Record = { 'searchCode', 'searchRepositories' ], 'maintainer': [ - 'getRepository', 'listBranches', 'getFileContent', 'createOrUpdateFile', + 'getRepository', 'listBranches', 'getFileContent', 'createBranch', 'forkRepository', 'createRepository', 'createOrUpdateFile', 'listPullRequests', 'getPullRequest', 'createPullRequest', 'mergePullRequest', 'addPullRequestComment', 'listIssues', 'getIssue', 'createIssue', 'addIssueComment', 'closeIssue', 'listCommits', 'getCommit', @@ -157,6 +160,9 @@ export function createGithubTools({ token, requireApproval = true, preset }: Git searchRepositories: searchRepositories(octokit), listCommits: listCommits(octokit), getCommit: getCommit(octokit), + createBranch: createBranch(octokit, approval('createBranch')), + forkRepository: forkRepository(octokit, approval('forkRepository')), + createRepository: createRepository(octokit, approval('createRepository')), createOrUpdateFile: createOrUpdateFile(octokit, approval('createOrUpdateFile')), createPullRequest: createPullRequest(octokit, approval('createPullRequest')), mergePullRequest: mergePullRequest(octokit, approval('mergePullRequest')), @@ -177,7 +183,7 @@ export type GithubTools = ReturnType // Re-export individual tool factories for cherry-picking export { createOctokit } from './client' -export { getRepository, listBranches, getFileContent, createOrUpdateFile } from './tools/repository' +export { getRepository, listBranches, getFileContent, createBranch, forkRepository, createRepository, createOrUpdateFile } from './tools/repository' export { listPullRequests, getPullRequest, createPullRequest, mergePullRequest, addPullRequestComment } from './tools/pull-requests' export { listIssues, getIssue, createIssue, addIssueComment, closeIssue } from './tools/issues' export { searchCode, searchRepositories } from './tools/search' diff --git a/packages/github-tools/src/tools/repository.ts b/packages/github-tools/src/tools/repository.ts index dba3df6..51fe281 100644 --- a/packages/github-tools/src/tools/repository.ts +++ b/packages/github-tools/src/tools/repository.ts @@ -74,6 +74,111 @@ export const getFileContent = (octokit: Octokit) => }, }) +export const createBranch = (octokit: Octokit, { needsApproval = true }: ToolOptions = {}) => + tool({ + description: 'Create a new branch in a GitHub repository from an existing branch or commit SHA', + needsApproval, + inputSchema: z.object({ + owner: z.string().describe('Repository owner'), + repo: z.string().describe('Repository name'), + branch: z.string().describe('Name for the new branch'), + from: z.string().optional().describe('Source branch name or commit SHA to branch from (defaults to the default branch)'), + }), + execute: async ({ owner, repo, branch, from }) => { + let sha = from + if (!sha || !sha.match(/^[0-9a-f]{40}$/i)) { + const { data: ref } = await octokit.rest.git.getRef({ + owner, + repo, + ref: `heads/${from || (await octokit.rest.repos.get({ owner, repo })).data.default_branch}`, + }) + sha = ref.object.sha + } + const { data } = await octokit.rest.git.createRef({ + owner, + repo, + ref: `refs/heads/${branch}`, + sha, + }) + return { + ref: data.ref, + sha: data.object.sha, + url: data.url, + } + }, + }) + +export const forkRepository = (octokit: Octokit, { needsApproval = true }: ToolOptions = {}) => + tool({ + description: 'Fork a GitHub repository to the authenticated user account or a specified organization', + needsApproval, + inputSchema: z.object({ + owner: z.string().describe('Repository owner to fork from'), + repo: z.string().describe('Repository name to fork'), + organization: z.string().optional().describe('Organization to fork into (omit to fork to your personal account)'), + name: z.string().optional().describe('Name for the forked repository (defaults to the original name)'), + }), + execute: async ({ owner, repo, organization, name }) => { + const { data } = await octokit.rest.repos.createFork({ + owner, + repo, + organization, + name, + }) + return { + name: data.name, + fullName: data.full_name, + url: data.html_url, + cloneUrl: data.clone_url, + sshUrl: data.ssh_url, + defaultBranch: data.default_branch, + private: data.private, + parent: data.parent ? { fullName: data.parent.full_name, url: data.parent.html_url } : null, + } + }, + }) + +export const createRepository = (octokit: Octokit, { needsApproval = true }: ToolOptions = {}) => + tool({ + description: 'Create a new GitHub repository for the authenticated user or a specified organization', + needsApproval, + inputSchema: z.object({ + name: z.string().describe('Repository name'), + description: z.string().optional().describe('A short description of the repository'), + isPrivate: z.boolean().optional().default(false).describe('Whether the repository is private'), + autoInit: z.boolean().optional().default(false).describe('Create an initial commit with a README'), + gitignoreTemplate: z.string().optional().describe('Gitignore template to use (e.g. "Node", "Python")'), + licenseTemplate: z.string().optional().describe('License keyword (e.g. "mit", "apache-2.0")'), + org: z.string().optional().describe('Organization to create the repository in (omit for personal repo)'), + }), + execute: async ({ name, description, isPrivate, autoInit, gitignoreTemplate, licenseTemplate, org }) => { + const params = { + name, + description, + private: isPrivate, + auto_init: autoInit, + gitignore_template: gitignoreTemplate, + license_template: licenseTemplate, + } + + const { data } = org + ? await octokit.rest.repos.createInOrg({ org, ...params }) + : await octokit.rest.repos.createForAuthenticatedUser(params) + + return { + name: data.name, + fullName: data.full_name, + description: data.description, + url: data.html_url, + cloneUrl: data.clone_url, + sshUrl: data.ssh_url, + defaultBranch: data.default_branch, + private: data.private, + createdAt: data.created_at, + } + }, + }) + export const createOrUpdateFile = (octokit: Octokit, { needsApproval = true }: ToolOptions = {}) => tool({ description: 'Create or update a file in a GitHub repository. Provide the SHA when updating an existing file.', From 911e7d4c461200d681e97b2b1bdccc6a81623cfc Mon Sep 17 00:00:00 2001 From: Hugo Richard Date: Thu, 26 Feb 2026 17:41:22 +0100 Subject: [PATCH 2/2] add changeset --- .changeset/brave-houses-dress.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/brave-houses-dress.md diff --git a/.changeset/brave-houses-dress.md b/.changeset/brave-houses-dress.md new file mode 100644 index 0000000..1e152b6 --- /dev/null +++ b/.changeset/brave-houses-dress.md @@ -0,0 +1,5 @@ +--- +"@github-tools/sdk": minor +--- + +feat: add repository management tools