Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/paged-list-has-more.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@github-tools/sdk': minor
---

REST list tools now return `{ items, hasMore, page, perPage, nextPage }` instead of a bare array. Object-shaped lists (`listCheckRuns`, `listWorkflowRuns`, `listWorkflows`, `listWorkflowJobs`, reactions) add the same paging fields next to their existing keys. When `hasMore` is true, call again with `nextPage` (or raise `maxPages`) β€” do not repeat the same page. `page` is restored on `listCommits`, `listIssues`, `listPullRequests`, `listCheckRuns`, `listReleases`, and `listBranches`. Filter `listCommits` with `path` / `author` / `since` / `until`. `getRepositoryTree` accepts a `path` prefix; tree and large diffs are capped in the model-facing output.
2 changes: 1 addition & 1 deletion .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ pnpm --filter @github-tools/sdk typecheck # Type-check the SDK

Every tool splits into a **core** function (pure logic) and a **tool factory** (the `ai` SDK wrapper). See `getGistCore`/`getGist` (`packages/github-tools/src/core/gists.ts` / `src/tools/gists.ts`) for a read tool, `createIssue` (`src/tools/issues.ts`) for a write tool.

1. **Core logic** β€” add `{name}InputSchema` (zod, `.describe()` on every field), `{name}Description`, and `{name}Core({ token, ...args })` to `packages/github-tools/src/core/{domain}.ts`. Shape the return β€” never return the raw Octokit response.
1. **Core logic** β€” add `{name}InputSchema` (zod, `.describe()` on every field), `{name}Description`, and `{name}Core({ token, ...args })` to `packages/github-tools/src/core/{domain}.ts`. Shape the return β€” never return the raw Octokit response. REST list tools return `pagedList(...)` (`{ items, hasMore, page, perPage, nextPage? }`); object-shaped lists add the same paging fields next to existing keys (`checkRuns`, `runs`, …).
2. **Tool factory** β€” add the `"use step"` wrapper and the exported factory to `packages/github-tools/src/tools/{domain}.ts`. Read tools take `(token)`; write tools also take `({ needsApproval = true }: ToolOptions = {})`.
3. **Register** (new domain? add a re-export in `packages/github-tools/src/core/index.ts` too):
- `packages/github-tools/src/core/catalog.ts` β€” add one `GITHUB_TOOL_CATALOG` entry (JSDoc, `description`, `inputSchema`, `get core()`, `write: true` for write tools, `connectScopes`). `GITHUB_TOOL_NAMES`, `GITHUB_WRITE_TOOLS`, `TOOL_CONNECT_SCOPES`, and the eve registry are all derived from it β€” no separate registration
Expand Down
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ export const myTool = (token: GithubTokenInput, { needsApproval = true }: ToolOp
})
```

**REST list tools** return `{ items, hasMore, page, perPage, nextPage? }` via `pagedList()` (`src/core/pagination.ts`). Object-shaped lists (`listCheckRuns`, `listWorkflowRuns`, …) add the same paging fields next to their existing keys. `hasMore` must live on the execute result β€” eve `toModelOutput` cannot see input. Restore `page` on every REST list schema.

**Adding a new tool?** Follow the checklist in [`.github/CONTRIBUTING.md`](.github/CONTRIBUTING.md#adding-a-new-tool) β€” registration files, chat metadata, docs, changeset.

### Key source files
Expand Down
9 changes: 5 additions & 4 deletions apps/docs/content/docs/4.guide/6.working-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ Prefer one composite call over chaining several reads:

| Tool | Returns |
|---|---|
| `getPullRequestContext` | PR details + files + reviews (+ optional CI checks) |
| `getIssueContext` | Issue + `labelNames` + recent comments |
| `getPullRequestContext` | PR details + files + reviews (+ optional CI checks). `filesHasMore` / `reviewsHasMore` when those lists continue |
| `getIssueContext` | Issue + `labelNames` + recent comments. `commentsHasMore` when more comments exist |
| `getReleaseContext` | Release + previous release + tag comparison |
| `getCiFailureContext` | Combined status, failing checks, failed workflow jobs/steps |

Expand All @@ -66,12 +66,13 @@ Call independent follow-up reads **in the same step** when you already know the
| `getIssueContext` | Defaults to `detail: 'full'` (one-shot) and returns `labelNames` (strings), not full label objects | `detail: 'summary'`; use `listLabels` for descriptions; use `listIssueComments` to paginate beyond the embedded comments |
| `includePatch: false` | Omits diff patches on `listPullRequestFiles`, `getCommit`, `compareCommits` | `includePatch: true`; optionally `filenames` on `listPullRequestFiles` |
| File ranges | Prefer `startLine` / `endLine` / `maxLines` on `getFileContent` | Omit ranges only for small files |
| `maxPages` | List tools fetch one page by default | Set `maxPages` to combine sequential pages in one call |
| Paged lists | REST list tools return `{ items, hasMore, page, nextPage }` (or add those fields next to `checkRuns` / `runs` / …). Default is one page. | When `hasMore`, call with `nextPage` or set `maxPages` to combine pages. Do not repeat the same page. Filter `listCommits` with `path` / `author` / `since` / `until` |
| `getRepositoryTree` | Optional `path` prefix; model output caps at 200 entries | Prefer `path` over `recursive: true`; if `truncated`, narrow `path` |
| Text-match fragments | `searchCode` truncates each snippet to ~300 chars | None β€” fetch the file with `getFileContent` for full context |
| `listDiscussions` | Returns 20 discussions per call, cursor-paginated | Raise `perPage`, or pass the returned `endCursor` as `after` |
| `getWorkflowJobLogs` | Returns the last 200 log lines with per-line timestamps stripped | Raise `maxLines` (up to 2000) when the error is higher up |
| `listPullRequestReviewThreads` | Returns unresolved threads only, comment bodies truncated (~500 chars), cursor-paginated | `status: 'all'` for resolved threads; `detail: 'full'` for complete bodies; pass `endCursor` as `after` |
| `listNotifications` | Returns 20 unread threads per call (max 50) | `all: true` to include read threads; raise `perPage` |
| `listNotifications` | Returns 20 unread threads per call (max 50) | `all: true` to include read threads; raise `perPage`; when `hasMore`, pass `nextPage` |

## Example: code review bootstrap

Expand Down
8 changes: 4 additions & 4 deletions apps/docs/content/docs/5.api/1.tools-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Available in all presets. These tools manage repositories, branches, and file co
| `getRepository` | read repository metadata (name, description, stars, language) | No |
| `listBranches` | list branches and their HEAD commits | No |
| `getFileContent` | read a file at a path/ref; prefer `startLine`/`endLine` or `maxLines` for large files | No |
| `getRepositoryTree` | list the file and directory structure of a repository at a given ref | No |
| `getRepositoryTree` | list the file and directory structure at a given ref (prefer a `path` prefix over `recursive: true`) | No |
| `createBranch` | create a new branch from an existing branch or commit SHA | Yes |
| `deleteBranch` | permanently delete a branch | Yes |
| `forkRepository` | fork a repository to your account or an organization | Yes |
Expand Down Expand Up @@ -202,17 +202,17 @@ Available in all presets:

| Tool | Capability | Write |
|---|---|---|
| `listCommits` | list commit history for a branch | No |
| `listCommits` | list commit history (filter with `path` / `author` / `since` / `until`; when `hasMore`, pass `nextPage`) | No |
| `getCommit` | read a single commit with file stats (patches omitted by default; set `includePatch` for diffs) | No |
| `getBlame` | line-level git blame for a file (GraphQL) | No |
| `compareCommits` | compare two branches, tags, or commits: ahead/behind counts, commits in between, and files that differ (patches omitted by default) | No |
| `searchCode` | search code across repository files, with matching text snippets when GitHub returns them | No |
| `searchRepositories` | search repositories by query | No |
| `searchIssues` | search issues and pull requests using qualifiers like `is:open` or `type:pr` | No |

## Fetch beyond one page
## Paged list results

List tools (`listCommits`, `listPullRequests`, `listIssues`, `listWorkflowRuns`, `listCheckRuns`, `listReleases`) accept an optional `maxPages` alongside `perPage`. Omit it to fetch a single page as before; set it to sequentially fetch and combine up to that many pages, stopping early once a page comes back short. This lets the model pull a full history in one call instead of paging manually across several tool calls.
REST list tools return `{ items, hasMore, page, perPage, nextPage? }` β€” or add those paging fields next to existing keys (`checkRuns`, `runs`, `workflows`, `jobs`). When `hasMore` is true, call again with `nextPage` (not the same `page`), or set `maxPages` to combine sequential pages in one call. Filter `listCommits` with `path` / `author` / `since` / `until` instead of walking the full history. `getRepositoryTree` accepts a `path` prefix; prefer that over `recursive: true`.

## Identify write operations

Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/5.api/2.reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ Core properties (`execute`, `inputSchema`, `outputSchema`) cannot be overridden.

### Rate-limit metadata

Object-shaped tool results include a `rateLimit` field from the last GitHub response (`x-ratelimit-remaining`, `x-ratelimit-limit`, `x-ratelimit-reset`, `x-ratelimit-resource`, and `retry-after` when present). Array-shaped results (`listIssues`, `listPullRequests`, …) are unchanged. The field is stripped before the model sees the output (`toModelOutput`); hooks, channels, and the chat UI still receive it.
Object-shaped tool results include a `rateLimit` field from the last GitHub response (`x-ratelimit-remaining`, `x-ratelimit-limit`, `x-ratelimit-reset`, `x-ratelimit-resource`, and `retry-after` when present). REST list tools now return objects (`{ items, hasMore, … }` or keyed collections), so they carry `rateLimit` too. The field is stripped before the model sees the output (`toModelOutput`); hooks, channels, and the chat UI still receive it.

```ts [rate-limit.ts]
import type { GithubRateLimit } from '@github-tools/sdk'
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/skills/github-tools-agents/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ Array presets merge: `preset: ['code-review', 'issue-triage']`. Start with the s

## Working context

Pass `context: { owner, repo, pullNumber?, issueNumber?, ref? }` to `createGithubTools` / `createGithubAgent` / `createDurableGithubAgent` to default those fields on tool inputs and inject them into the agent system prompt. Prefer composite tools (`getPullRequestContext`, `getIssueContext`, `getReleaseContext`, `getCiFailureContext`) for multi-part reads β€” call follow-up reads in the same step when possible. Diff patches are omitted by default β€” set `includePatch: true` (optionally with `filenames`) when you need specific diffs. Bodies are truncated by default (`detail: 'summary'`). `getIssueContext` returns `labelNames` (strings) rather than full label objects. Prefer `getFileContent` with `startLine`/`endLine` or `maxLines` for large files. `getWorkflowJobLogs` returns the last 200 log lines with timestamps stripped β€” raise `maxLines` (up to 2000) only when needed. `listPullRequestReviewThreads` returns unresolved threads only by default with truncated comment bodies. Object-shaped execute results include `rateLimit` (`remaining` / `limit` / `reset` / `resource`); it is stripped from the model-facing output. Array-shaped list tools do not carry it. On 403/429 the error text includes remaining/reset.
Pass `context: { owner, repo, pullNumber?, issueNumber?, ref? }` to `createGithubTools` / `createGithubAgent` / `createDurableGithubAgent` to default those fields on tool inputs and inject them into the agent system prompt. Prefer composite tools (`getPullRequestContext`, `getIssueContext`, `getReleaseContext`, `getCiFailureContext`) for multi-part reads β€” call follow-up reads in the same step when possible. Diff patches are omitted by default β€” set `includePatch: true` (optionally with `filenames`) when you need specific diffs. Bodies are truncated by default (`detail: 'summary'`). `getIssueContext` returns `labelNames` (strings) rather than full label objects. Prefer `getFileContent` with `startLine`/`endLine` or `maxLines` for large files. `getWorkflowJobLogs` returns the last 200 log lines with timestamps stripped β€” raise `maxLines` (up to 2000) only when needed. `listPullRequestReviewThreads` returns unresolved threads only by default with truncated comment bodies. REST list tools return `{ items, hasMore, page, nextPage }` (or add those fields next to `checkRuns` / `runs`); when `hasMore`, call with `nextPage` or raise `maxPages` β€” never the same page. Filter `listCommits` with `path` / `author` / `since` / `until`. Prefer a `path` prefix on `getRepositoryTree` over `recursive: true`. Object-shaped execute results include `rateLimit` (`remaining` / `limit` / `reset` / `resource`); it is stripped from the model-facing output. On 403/429 the error text includes remaining/reset.

## Write safety

Expand Down
10 changes: 5 additions & 5 deletions packages/github-tools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ Core properties (`execute`, `inputSchema`, `outputSchema`) cannot be overridden.

## Rate-limit metadata

Object-shaped tool results include a `rateLimit` field from the last GitHub response. Array-shaped results are unchanged. The field is stripped before the model sees the output; hooks, channels, and UIs still receive it.
Object-shaped tool results include a `rateLimit` field from the last GitHub response. REST list tools now return objects, so they carry it too. The field is stripped before the model sees the output; hooks, channels, and UIs still receive it.

```ts
import type { GithubRateLimit } from '@github-tools/sdk'
Expand Down Expand Up @@ -471,16 +471,16 @@ New agents should use the [eve extension](#eve-extension) above; see [`examples/

## Available Tools

List tools (`listCommits`, `listPullRequests`, `listIssues`, `listWorkflowRuns`, `listCheckRuns`, `listReleases`) accept an optional `maxPages` alongside `perPage`. Set it to sequentially fetch and combine up to that many pages in one call, stopping early once a page comes back short.
List tools return `{ items, hasMore, page, perPage, nextPage? }` (or add those fields next to `checkRuns` / `runs` / …). When `hasMore`, call with `nextPage` or set `maxPages` to combine sequential pages in one call β€” do not repeat the same page. Filter `listCommits` with `path` / `author` / `since` / `until`. `getRepositoryTree` accepts a `path` prefix.

### Repository

| Tool | Description |
|---|---|
| `getRepository` | Get repository metadata (stars, language, default branch, …) |
| `listBranches` | List branches |
| `listBranches` | List branches (`hasMore` / `nextPage` when there are more) |
| `getFileContent` | Read a file or directory listing (prefer `startLine`/`endLine` or `maxLines` for large files) |
| `getRepositoryTree` | List the file and directory structure at a given ref |
| `getRepositoryTree` | List the file and directory structure at a given ref (prefer a `path` prefix over `recursive: true`) |
| `createBranch` | Create a new branch from an existing branch or commit SHA |
| `deleteBranch` | Permanently delete a branch |
| `forkRepository` | Fork a repository to a user or organization |
Expand Down Expand Up @@ -606,7 +606,7 @@ Pull request conversations share the issue numbering, so the issue-level tools w

| Tool | Description |
|---|---|
| `listCommits` | List commits, optionally filtered by file path, author, or date range |
| `listCommits` | List commits, optionally filtered by file path, author, or date range. When `hasMore`, pass `nextPage` |
| `getCommit` | Get a commit's full details including changed files and diffs |
| `getBlame` | Line-level git blame for a file (GitHub GraphQL) |
| `compareCommits` | Compare two branches, tags, or commits: ahead/behind counts, commits in between, and files that differ |
Expand Down
3 changes: 2 additions & 1 deletion packages/github-tools/src/agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import { formatContextInstructions, type GithubToolsContext } from './core/conte

const SHARED_RULES = `When a tool execution is denied by the user, do not retry it. Briefly acknowledge the decision and move on.
Call independent read tools in the same step when you already know the arguments β€” never serialize reads that could run in parallel.
Bodies default to detail summary; patches default to includePatch false; prefer getFileContent with startLine/endLine or maxLines for large files.`
Bodies default to detail summary; patches default to includePatch false; prefer getFileContent with startLine/endLine or maxLines for large files.
Paged lists return { items, hasMore, page, nextPage }. When hasMore, call with nextPage or raise maxPages β€” never the same page. Prefer path/author/since/until on listCommits instead of walking history. Prefer a path prefix on getRepositoryTree over recursive true.`

const DEFAULT_INSTRUCTIONS = `You are a helpful GitHub assistant. You can read and explore repositories, issues, pull requests, discussions, commits, code, gists, and workflows. You can also create issues, pull requests, comments, gists, reactions, trigger workflows, and update files when asked.

Expand Down
16 changes: 8 additions & 8 deletions packages/github-tools/src/core/bundles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const getPullRequestContextInputSchema = z.object({
detail: detailSchema,
})

export const getPullRequestContextDescription = 'Fetch pull request details plus files, reviews, and optional CI checks in one call β€” prefer this over separate getPullRequest / listPullRequestFiles / listPullRequestReviews calls'
export const getPullRequestContextDescription = 'Fetch pull request details plus files, reviews, and optional CI checks in one call β€” prefer this over separate getPullRequest / listPullRequestFiles / listPullRequestReviews calls. filesHasMore / reviewsHasMore mean more pages exist on those lists.'

export async function getPullRequestContextCore({
token,
Expand Down Expand Up @@ -80,8 +80,8 @@ export async function getPullRequestContextCore({

return withComposedRateLimit({
pullRequest,
...files !== undefined ? { files } : {},
...reviews !== undefined ? { reviews } : {},
...files !== undefined ? { files: files.items, filesHasMore: files.hasMore } : {},
...reviews !== undefined ? { reviews: reviews.items, reviewsHasMore: reviews.hasMore } : {},
...checks !== undefined ? { checks } : {},
})
}
Expand All @@ -101,7 +101,7 @@ export const getIssueContextInputSchema = z.object({
.describe('full returns the complete body (default for this one-shot tool); summary truncates to ~500 chars'),
})

export const getIssueContextDescription = 'Fetch an issue plus available label names and recent comments in one call β€” prefer this over separate getIssue / listLabels / comment calls when triaging. Call once; do not re-fetch the same issue.'
export const getIssueContextDescription = 'Fetch an issue plus available label names and recent comments in one call β€” prefer this over separate getIssue / listLabels / comment calls when triaging. Call once; do not re-fetch the same issue. commentsHasMore means more comments exist β€” use listIssueComments with nextPage.'

export async function getIssueContextCore({
token,
Expand Down Expand Up @@ -143,8 +143,8 @@ export async function getIssueContextCore({
return withComposedRateLimit({
issue,
// Names only β€” full label objects (color/description) dominate triage payloads on large repos
...labels !== undefined ? { labelNames: labels.map(label => label.name) } : {},
...comments !== undefined ? { comments } : {},
...labels !== undefined ? { labelNames: labels.items.map(label => label.name) } : {},
...comments !== undefined ? { comments: comments.items, commentsHasMore: comments.hasMore } : {},
})
}

Expand Down Expand Up @@ -180,7 +180,7 @@ export async function getReleaseContextCore({
? await getReleaseCore({ token, owner, repo, releaseId, detail })
: await getLatestReleaseCore({ token, owner, repo, detail })

let previous: Awaited<ReturnType<typeof listReleasesCore>>[number] | undefined
let previous: Awaited<ReturnType<typeof listReleasesCore>>['items'][number] | undefined
if (includePrevious || includeCompare) {
const releases = await listReleasesCore({
token,
Expand All @@ -190,7 +190,7 @@ export async function getReleaseContextCore({
maxPages: 1,
detail,
})
previous = releases.find(r => r.id !== release.id && !r.draft)
previous = releases.items.find(r => r.id !== release.id && !r.draft)
}

let comparison: Awaited<ReturnType<typeof compareCommitsCore>> | undefined
Expand Down
Loading
Loading