From 277731c8aa964b917c926094fb8a24d341b3d3a6 Mon Sep 17 00:00:00 2001 From: Duncan <283775822+duncsdownunder@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:39:33 -0700 Subject: [PATCH] feat(github-picker): include org and collaborator repos in the repo list gh repo list only returns repos the authenticated user owns, so the New Agent modal's "Pick from GitHub" mode hid organization repos entirely. List via the REST /user/repos endpoint instead, with affiliation=owner,collaborator,organization_member (paginated, sorted by updated). Falls back to the old owner-only gh repo list when gh api --slurp is unavailable (gh too old), and keeps the existing degrade-to-unauthenticated behavior on total failure. Co-Authored-By: Claude Fable 5 --- packages/server-core/src/github-repos.ts | 57 ++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/packages/server-core/src/github-repos.ts b/packages/server-core/src/github-repos.ts index 6595a20a..d9ec8471 100644 --- a/packages/server-core/src/github-repos.ts +++ b/packages/server-core/src/github-repos.ts @@ -19,6 +19,14 @@ const GHRepoListItemSchema = z.object({ export type GitHubRepoListItem = z.infer; +// REST /user/repos item shape (only the fields the picker needs). +const GHApiRepoSchema = z.object({ + full_name: z.string(), + html_url: z.string(), + description: z.string().nullable(), + updated_at: z.string(), +}); + export interface ListGitHubReposResult { repos: GitHubRepoListItem[]; authenticated: boolean; @@ -27,12 +35,53 @@ export interface ListGitHubReposResult { const REPO_LIST_LIMIT = 200; /** - * Lists the authenticated user's GitHub repos via `gh repo list`. Never - * throws — `gh` missing, not authenticated, or any other failure (network, - * malformed output) all degrade to `{ repos: [], authenticated: false }` so - * callers can render a "run `gh auth login`" hint instead of erroring. + * Lists the authenticated user's GitHub repos — personal AND org/collaborator + * repos — via the REST `/user/repos` endpoint (`gh repo list` only returns + * repos the user owns, which hides organization repos; see the affiliation + * param). `--paginate --slurp` yields a JSON array of pages. Never throws — + * `gh` missing, not authenticated, or any other failure (network, malformed + * output) all degrade to `{ repos: [], authenticated: false }` so callers can + * render a "run `gh auth login`" hint instead of erroring. */ export async function listGitHubRepos(): Promise { + try { + const { stdout } = await execWithShellEnv("gh", [ + "api", + "--paginate", + "--slurp", + "user/repos?affiliation=owner,collaborator,organization_member&sort=updated&per_page=100", + ]); + + const raw: unknown = JSON.parse(stdout); + if (!Array.isArray(raw)) { + return { repos: [], authenticated: false }; + } + + const repos: GitHubRepoListItem[] = []; + for (const page of raw) { + if (!Array.isArray(page)) continue; + for (const item of page) { + const result = GHApiRepoSchema.safeParse(item); + if (result.success) { + repos.push({ + nameWithOwner: result.data.full_name, + url: result.data.html_url, + description: result.data.description, + updatedAt: result.data.updated_at, + }); + } + } + } + + return { repos: repos.slice(0, REPO_LIST_LIMIT), authenticated: true }; + } catch { + // `gh api --slurp` needs a reasonably recent gh; fall back to the + // owner-only listing rather than losing the picker entirely. + return listOwnedGitHubRepos(); + } +} + +async function listOwnedGitHubRepos(): Promise { try { const { stdout } = await execWithShellEnv("gh", [ "repo",