From e677b859e4c07019fa96b9963173e6aca2ab6eb4 Mon Sep 17 00:00:00 2001 From: owenkephart Date: Sat, 1 Aug 2026 01:54:07 +0000 Subject: [PATCH 1/9] feat(eve): add guided GitHub setup Signed-off-by: owenkephart --- .changeset/guided-github-setup.md | 5 + apps/docs/lib/integrations/data.ts | 13 +-- apps/docs/registry.json | 24 ++--- apps/docs/registry/channels/github.ts | 7 +- .../docs/scripts/validate-channel-registry.ts | 2 + docs/channels/github.mdx | 17 ++- packages/eve-catalog/src/index.ts | 3 +- .../setup/integrations/github/connect.test.ts | 92 ++++++++++++++++ .../src/setup/integrations/github/connect.ts | 102 ++++++++++++++++++ .../setup/integrations/github/setup.test.ts | 59 ++++++++++ .../src/setup/integrations/github/setup.ts | 96 +++++++++++++++++ .../src/setup/integrations/registry.test.ts | 4 + .../eve/src/setup/integrations/registry.ts | 2 + 13 files changed, 389 insertions(+), 37 deletions(-) create mode 100644 .changeset/guided-github-setup.md create mode 100644 packages/eve/src/setup/integrations/github/connect.test.ts create mode 100644 packages/eve/src/setup/integrations/github/connect.ts create mode 100644 packages/eve/src/setup/integrations/github/setup.test.ts create mode 100644 packages/eve/src/setup/integrations/github/setup.ts diff --git a/.changeset/guided-github-setup.md b/.changeset/guided-github-setup.md new file mode 100644 index 000000000..b8c8ad491 --- /dev/null +++ b/.changeset/guided-github-setup.md @@ -0,0 +1,5 @@ +--- +"eve": patch +--- + +Add guided GitHub channel setup through `eve add channel/github`. The flow provisions a Vercel Connect GitHub App, routes verified webhooks, scaffolds the channel, and explains how to install and use the app. diff --git a/apps/docs/lib/integrations/data.ts b/apps/docs/lib/integrations/data.ts index 060d4663c..92d205ce3 100644 --- a/apps/docs/lib/integrations/data.ts +++ b/apps/docs/lib/integrations/data.ts @@ -271,26 +271,23 @@ TWILIO_AUTH_TOKEN=... # required for inbound signature verification logo: "github", docsHref: "/docs/channels/github", keywords: ["issues", "pull requests", "app", "webhook", "code"], - install: `Add this channel from eve's registry. This writes \`agent/channels/github.ts\`: + install: `Add this channel from eve's registry to create a Vercel Connect GitHub App, route verified webhooks, and write \`agent/channels/github.ts\`: \`\`\`bash eve add channel/github \`\`\``, - quickStart: `Create \`agent/channels/github.ts\`: + quickStart: `The guided setup writes \`agent/channels/github.ts\`: \`\`\`ts // agent/channels/github.ts +import { connectGitHubCredentials } from "@vercel/connect/eve"; import { githubChannel } from "eve/channels/github"; export default githubChannel({ - credentials: { - appId: () => process.env.GITHUB_APP_ID!, - privateKey: () => process.env.GITHUB_APP_PRIVATE_KEY!, - webhookSecret: () => process.env.GITHUB_WEBHOOK_SECRET!, - }, + credentials: connectGitHubCredentials("github/my-agent"), }); \`\`\``, - configure: `Create a GitHub App, subscribe to issue and pull-request events, and set the webhook URL to eve's route (\`/eve/v1/github\`). Provide the app ID, private key, and webhook secret through environment variables. See the [GitHub channel docs](/docs/channels/github) for required permissions.`, + configure: `Sign in to Vercel, then let the guided flow create or link a project, provision the GitHub App, and attach its verified webhook trigger to \`/eve/v1/github\`. Deploy, install the app from Vercel Connect, then mention it in an issue, pull request, or review comment. See the [GitHub channel docs](/docs/channels/github) for permissions and events.`, }, "linear-agent": { logo: "linear", diff --git a/apps/docs/registry.json b/apps/docs/registry.json index 621c1285c..1fee1e238 100644 --- a/apps/docs/registry.json +++ b/apps/docs/registry.json @@ -1209,19 +1209,19 @@ "name": "channel/github", "type": "registry:item", "title": "GitHub", - "description": "Drive your agent from issues, pull requests, and comments.", - "envVars": { - "GITHUB_APP_ID": "", - "GITHUB_APP_PRIVATE_KEY": "", - "GITHUB_WEBHOOK_SECRET": "" - }, - "files": [ - { - "path": "registry/channels/github.ts", - "type": "registry:file", - "target": "agent/channels/github.ts" + "description": "Drive your agent from issues, pull requests, and comments, with guided Connect setup.", + "meta": { + "eve": { + "setup": { + "command": "eve", + "package": "eve", + "bin": "eve", + "args": ["integration", "setup", "github"] + }, + "requires": ">=0.30.7" } - ] + }, + "dependencies": ["@vercel/connect"] }, { "name": "channel/linear-agent", diff --git a/apps/docs/registry/channels/github.ts b/apps/docs/registry/channels/github.ts index 736d53f92..5b7b7d5d2 100644 --- a/apps/docs/registry/channels/github.ts +++ b/apps/docs/registry/channels/github.ts @@ -1,9 +1,6 @@ +import { connectGitHubCredentials } from "@vercel/connect/eve"; import { githubChannel } from "eve/channels/github"; export default githubChannel({ - credentials: { - appId: () => process.env.GITHUB_APP_ID!, - privateKey: () => process.env.GITHUB_APP_PRIVATE_KEY!, - webhookSecret: () => process.env.GITHUB_WEBHOOK_SECRET!, - }, + credentials: connectGitHubCredentials("github/my-agent"), }); diff --git a/apps/docs/scripts/validate-channel-registry.ts b/apps/docs/scripts/validate-channel-registry.ts index b7570950e..5d16f9ecc 100644 --- a/apps/docs/scripts/validate-channel-registry.ts +++ b/apps/docs/scripts/validate-channel-registry.ts @@ -35,6 +35,7 @@ const registrySlugsByCatalogSlug: Readonly> = { const setupKindsByCatalogSlug: Readonly> = { discord: "discord", + github: "github", "linear-agent": "linear", eve: "web", photon: "photon", @@ -115,6 +116,7 @@ for (const [index, item] of items.entries()) { if ( entry.slug === "slack" || entry.slug === "discord" || + entry.slug === "github" || entry.slug === "linear-agent" || entry.slug === "eve" || entry.slug === "photon" diff --git a/docs/channels/github.mdx b/docs/channels/github.mdx index d82b641e3..5fd6e5efa 100644 --- a/docs/channels/github.mdx +++ b/docs/channels/github.mdx @@ -6,24 +6,19 @@ type: integration The GitHub channel lets the agent work directly on a repository. Someone `@mentions` it in an issue, PR, or review comment, and the agent answers right there in the thread, with the PR diff already in context and the repo checked out into the sandbox. It takes GitHub App webhooks at `/eve/v1/github`, checks the signature, derives auth from whoever triggered the event, and replies on the native surface. Credentials can run through [Vercel Connect](../guides/auth-and-route-protection), which manages the GitHub App, the installation token, and inbound webhook verification, so there's no app private key or webhook secret for you to hold. See [Channels](./overview) for the contract this builds on. -## Set up Connect +## Guided Connect setup -Create a GitHub Connect client and copy its UID (e.g. `github/my-agent`), then attach this project as the trigger destination at eve's GitHub route: +Run the registry setup from the agent directory: ```bash -npm install -g vercel@latest -vercel connect create github --triggers -vercel connect detach --yes -vercel connect attach --triggers --trigger-path /eve/v1/github --yes +eve add channel/github ``` -The `create` step provisions the GitHub App and a trigger destination at the default Connect path. `detach` then `attach --trigger-path /eve/v1/github` re-points the trigger at the eve GitHub route, since eve does not serve the default Connect path. `--triggers` makes Connect receive the App's webhooks, verify them, and forward them to your deployment. During registration, subscribe to `issue_comment` and `pull_request_review_comment` for mention-driven turns — the managed App defaults to `pull_request` only — and add `issues`, `pull_request`, `check_suite`, `check_run`, or `workflow_run` if you wire up their opt-in hooks. You can also create the client from the [Connect dashboard](https://vercel.com/d?to=/%5Bteam%5D/~/connect&title=Go+to+Connect). +The flow checks that the Vercel CLI is authenticated, creates or links a Vercel project when needed, provisions an app-scoped GitHub Connect client, and replaces its default trigger with `/eve/v1/github`. It then installs `@vercel/connect` and writes `agent/channels/github.ts` with the connector UID. -## Add the channel +Vercel Connect creates the GitHub App, receives and verifies its webhooks, and forwards them to the deployed agent. After deploying, open the GitHub App in the Connect dashboard and install it in the organization or account where you want to use it. Mention the app in an issue, pull request, or review comment to start a conversation. -```bash -npm install @vercel/connect -``` +The generated channel uses Connect-managed credentials: ```ts title="agent/channels/github.ts" import { connectGitHubCredentials } from "@vercel/connect/eve"; diff --git a/packages/eve-catalog/src/index.ts b/packages/eve-catalog/src/index.ts index 2def2d4f7..19f0c38ed 100644 --- a/packages/eve-catalog/src/index.ts +++ b/packages/eve-catalog/src/index.ts @@ -122,7 +122,8 @@ export const INTEGRATIONS: readonly IntegrationEntry[] = [ slug: "github", name: "GitHub", kind: "channel", - tagline: "Drive your agent from issues, pull requests, and comments.", + tagline: + "Drive your agent from issues, pull requests, and comments, with guided Connect setup.", surfaces: { scaffoldable: false, gallery: true }, }, { diff --git a/packages/eve/src/setup/integrations/github/connect.test.ts b/packages/eve/src/setup/integrations/github/connect.test.ts new file mode 100644 index 000000000..4ec70ce18 --- /dev/null +++ b/packages/eve/src/setup/integrations/github/connect.test.ts @@ -0,0 +1,92 @@ +import { describe, expect, it, vi } from "vitest"; + +import type { ChannelSetupLog } from "#setup/cli/index.js"; +import { parseCreatedGitHubConnector, provisionGitHubConnector } from "./connect.js"; + +function log(): ChannelSetupLog { + return { + message: vi.fn(), + info: vi.fn(), + success: vi.fn(), + warning: vi.fn(), + error: vi.fn(), + commandOutput: vi.fn(), + }; +} + +describe("GitHub Connect provisioning", () => { + it("parses an app-scoped GitHub connector", () => { + expect( + parseCreatedGitHubConnector( + JSON.stringify({ + id: "scl_github", + uid: "github/agent", + supportedSubjectTypes: ["app"], + }), + ), + ).toEqual({ id: "scl_github", uid: "github/agent" }); + }); + + it("creates the connector and replaces its trigger", async () => { + const runVercelCaptureStdout = vi.fn(async () => ({ + ok: true as const, + stdout: JSON.stringify({ + id: "scl_github", + uid: "github/agent", + supportedSubjectTypes: ["app"], + }), + stderr: "", + })); + const runVercel = vi.fn(async () => true); + + await expect( + provisionGitHubConnector({ + log: log(), + project: { orgId: "team_123", projectId: "prj_123" }, + projectRoot: "/project", + slug: "agent", + deps: { runVercel, runVercelCaptureStdout }, + }), + ).resolves.toEqual({ id: "scl_github", uid: "github/agent" }); + + expect(runVercelCaptureStdout).toHaveBeenCalledWith( + [ + "connect", + "create", + "github", + "--name", + "agent", + "--triggers", + "-F", + "json", + "--scope", + "team_123", + ], + expect.objectContaining({ cwd: "/project", nonInteractive: true }), + ); + expect(runVercel).toHaveBeenNthCalledWith( + 1, + ["connect", "detach", "github/agent", "--project", "prj_123", "--yes", "--scope", "team_123"], + expect.objectContaining({ cwd: "/project", nonInteractive: true }), + ); + expect(runVercel).toHaveBeenNthCalledWith( + 2, + [ + "connect", + "attach", + "github/agent", + "--project", + "prj_123", + "--environment", + "production", + "--triggers", + "--trigger-path", + "/eve/v1/github", + "--yes", + "--scope", + "team_123", + ], + expect.objectContaining({ cwd: "/project", nonInteractive: true }), + ); + }); +}); diff --git a/packages/eve/src/setup/integrations/github/connect.ts b/packages/eve/src/setup/integrations/github/connect.ts new file mode 100644 index 000000000..f6aae404a --- /dev/null +++ b/packages/eve/src/setup/integrations/github/connect.ts @@ -0,0 +1,102 @@ +import { createPromptCommandOutput, withPhase, type ChannelSetupLog } from "#setup/cli/index.js"; +import { replaceConnectTrigger } from "#setup/connect-provisioning.js"; +import type { VercelProjectReference } from "#setup/project-resolution.js"; +import { runVercel, runVercelCaptureStdout } from "#setup/primitives/run-vercel.js"; +import { z } from "zod"; + +export const GITHUB_TRIGGER_PATH = "/eve/v1/github"; + +/** Identity of the GitHub connector provisioned for an agent channel. */ +export interface GitHubConnectorRef { + id: string; + uid: string; +} + +/** Effects used to provision a GitHub Connect connector. */ +export interface ProvisionGitHubConnectorDeps { + runVercel: typeof runVercel; + runVercelCaptureStdout: typeof runVercelCaptureStdout; +} + +const GitHubConnectorRefSchema = z.object({ + id: z.string().min(1), + uid: z.string().min(1), + supportedSubjectTypes: z.array(z.string()).refine((types) => types.includes("app")), +}); + +/** Parses `vercel connect create -F json` output for an app-scoped GitHub connector. */ +export function parseCreatedGitHubConnector(stdout: string): GitHubConnectorRef | undefined { + try { + const parsed = GitHubConnectorRefSchema.safeParse(JSON.parse(stdout)); + return parsed.success ? { id: parsed.data.id, uid: parsed.data.uid } : undefined; + } catch { + return undefined; + } +} + +/** Creates a GitHub connector and routes its verified webhooks to eve. */ +export async function provisionGitHubConnector(input: { + log: ChannelSetupLog; + project: VercelProjectReference; + projectRoot: string; + slug: string; + signal?: AbortSignal; + deps?: ProvisionGitHubConnectorDeps; +}): Promise { + const deps = input.deps ?? { runVercel, runVercelCaptureStdout }; + const onOutput = createPromptCommandOutput(input.log); + const result = await withPhase(input.log, "Creating GitHub connector...", () => + deps.runVercelCaptureStdout( + [ + "connect", + "create", + "github", + "--name", + input.slug, + "--triggers", + "-F", + "json", + "--scope", + input.project.orgId, + ], + { + cwd: input.projectRoot, + nonInteractive: true, + onOutput, + signal: input.signal, + }, + ), + ); + input.signal?.throwIfAborted(); + if (!result.ok) { + const detail = [result.stderr, result.stdout].find( + (value): value is string => value !== undefined && value.trim().length > 0, + ); + throw new Error( + detail ? `GitHub connector creation failed:\n${detail}` : "GitHub connector creation failed.", + ); + } + const connector = parseCreatedGitHubConnector(result.stdout); + if (connector === undefined) throw new Error("Vercel returned an invalid GitHub connector."); + + const attachment = await withPhase(input.log, "Connecting GitHub webhooks...", () => + replaceConnectTrigger({ + connectorUid: connector.uid, + projectRoot: input.projectRoot, + projectId: input.project.projectId, + orgId: input.project.orgId, + environment: "production", + triggerPath: GITHUB_TRIGGER_PATH, + onOutput, + signal: input.signal, + deps, + }), + ); + input.signal?.throwIfAborted(); + if (attachment.state !== "attached") { + throw new Error( + `GitHub connector was created, but its trigger could not be attached. Run \`vercel connect attach ${connector.uid} --project ${input.project.projectId} --environment production --triggers --trigger-path ${GITHUB_TRIGGER_PATH} --yes --scope ${input.project.orgId}\`.`, + ); + } + return connector; +} diff --git a/packages/eve/src/setup/integrations/github/setup.test.ts b/packages/eve/src/setup/integrations/github/setup.test.ts new file mode 100644 index 000000000..f501073b5 --- /dev/null +++ b/packages/eve/src/setup/integrations/github/setup.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it, vi } from "vitest"; + +import { createFakePrompter } from "#internal/testing/fake-prompter.js"; + +import { integrationSetupEnvironment } from "../shared/environment.js"; +import { createIntegrationSetupUi } from "../shared/ui.js"; +import { setupGitHub, type GitHubSetupDeps } from "./setup.js"; + +function deps(): GitHubSetupDeps { + return { + deriveConnectorSlug: vi.fn(async () => "agent" as never), + ensureVercelProject: vi.fn(async () => ({ orgId: "team-id", projectId: "project-id" })), + openUrl: vi.fn(), + provisionConnector: vi.fn(async () => ({ id: "scl_github", uid: "github/agent" })), + writeTextFile: vi.fn(async () => {}), + }; +} + +describe("GitHub setup", () => { + it("provisions Connect, routes webhooks, and scaffolds the channel", async () => { + const fake = createFakePrompter(); + const effects = deps(); + + await expect( + setupGitHub( + { + appRoot: "/project", + environment: integrationSetupEnvironment("authenticated", { kind: "unresolved" }), + ui: createIntegrationSetupUi({ + asker: { ask: vi.fn(), askMany: vi.fn() }, + prompter: fake.prompter, + }), + }, + effects, + ), + ).resolves.toMatchObject({ kind: "done" }); + + expect(effects.writeTextFile).toHaveBeenCalledWith( + "/project/agent/channels/github.ts", + expect.stringContaining('connectGitHubCredentials("github/agent")'), + { force: undefined }, + ); + expect(effects.openUrl).toHaveBeenCalledOnce(); + }); + + it("requires an authenticated Vercel CLI", async () => { + const fake = createFakePrompter(); + await expect( + setupGitHub({ + appRoot: "/project", + environment: integrationSetupEnvironment("logged-out", { kind: "unresolved" }), + ui: createIntegrationSetupUi({ + asker: { ask: vi.fn(), askMany: vi.fn() }, + prompter: fake.prompter, + }), + }), + ).rejects.toThrow("vercel login"); + }); +}); diff --git a/packages/eve/src/setup/integrations/github/setup.ts b/packages/eve/src/setup/integrations/github/setup.ts new file mode 100644 index 000000000..0af699b03 --- /dev/null +++ b/packages/eve/src/setup/integrations/github/setup.ts @@ -0,0 +1,96 @@ +import { join } from "node:path"; + +import { ensureVercelProject } from "#setup/flows/ensure-vercel-project.js"; +import { openUrl } from "#setup/primitives/open-url.js"; +import { deriveSlackConnectorSlug } from "#setup/scaffold/index.js"; +import { writeTextFile } from "#setup/scaffold/files.js"; +import { WizardCancelledError } from "#setup/step.js"; + +import type { + IntegrationSetupContext, + IntegrationSetupResult, + SetupIntegration, +} from "../types.js"; +import { provisionGitHubConnector } from "./connect.js"; + +export interface GitHubSetupDeps { + deriveConnectorSlug: typeof deriveSlackConnectorSlug; + ensureVercelProject: typeof ensureVercelProject; + openUrl: typeof openUrl; + provisionConnector: typeof provisionGitHubConnector; + writeTextFile: typeof writeTextFile; +} + +const defaultDeps: GitHubSetupDeps = { + deriveConnectorSlug: deriveSlackConnectorSlug, + ensureVercelProject, + openUrl, + provisionConnector: provisionGitHubConnector, + writeTextFile, +}; + +function connectTemplate(uid: string): string { + return `import { connectGitHubCredentials } from "@vercel/connect/eve"; +import { githubChannel } from "eve/channels/github"; + +export default githubChannel({ + credentials: connectGitHubCredentials(${JSON.stringify(uid)}), +}); +`; +} + +/** Runs guided GitHub App connector and channel setup. */ +export async function setupGitHub( + context: IntegrationSetupContext, + deps: GitHubSetupDeps = defaultDeps, +): Promise { + if (context.environment.vercel.kind === "unavailable") { + throw new Error( + "GitHub setup requires an authenticated Vercel CLI. Run `vercel login`, then retry.", + ); + } + try { + context.ui.prompter.note( + "Vercel Connect creates a GitHub App and routes verified webhooks to your deployed agent.", + "GitHub App", + ); + const project = await deps.ensureVercelProject({ + appRoot: context.appRoot, + prompter: context.ui.prompter, + signal: context.signal, + }); + const connector = await deps.provisionConnector({ + log: context.ui.prompter.log, + project, + projectRoot: context.appRoot, + slug: await deps.deriveConnectorSlug(context.appRoot), + signal: context.signal, + }); + await deps.writeTextFile( + join(context.appRoot, "agent/channels/github.ts"), + connectTemplate(connector.uid), + { force: context.force }, + ); + const dashboardUrl = "https://vercel.com/d?to=/%5Bteam%5D/~/connect&title=Open+Vercel+Connect"; + context.ui.nextSteps([ + "Deploy the agent, then open the GitHub App in Vercel Connect and install it in the organization or account where you want to use it.", + "Mention the app in an issue, pull request, or review comment to start a conversation.", + ]); + deps.openUrl(dashboardUrl); + return { + kind: "done", + facts: [{ label: "Vercel Connect", value: dashboardUrl, kind: "url" }], + }; + } catch (error) { + if (error instanceof WizardCancelledError) return { kind: "cancelled" }; + throw error; + } +} + +/** GitHub App setup registration. */ +export const GITHUB_SETUP: SetupIntegration = { + kind: "github", + label: "GitHub", + hint: "Respond to issues, pull requests, and comments", + setup: setupGitHub, +}; diff --git a/packages/eve/src/setup/integrations/registry.test.ts b/packages/eve/src/setup/integrations/registry.test.ts index f2a42ffa1..07b0d4644 100644 --- a/packages/eve/src/setup/integrations/registry.test.ts +++ b/packages/eve/src/setup/integrations/registry.test.ts @@ -38,6 +38,10 @@ describe("setup integrations", () => { }); }); + it("registers guided GitHub setup", () => { + expect(setupIntegration("github")).toMatchObject({ kind: "github", label: "GitHub" }); + }); + it("rejects an unknown integration", () => { expect(() => setupIntegration("unknown")).toThrow( 'Integration setup "unknown" is not available', diff --git a/packages/eve/src/setup/integrations/registry.ts b/packages/eve/src/setup/integrations/registry.ts index 32d6bfd0f..eb3a46821 100644 --- a/packages/eve/src/setup/integrations/registry.ts +++ b/packages/eve/src/setup/integrations/registry.ts @@ -1,4 +1,5 @@ import { DISCORD_SETUP } from "./discord/setup.js"; +import { GITHUB_SETUP } from "./github/setup.js"; import { LINEAR_SETUP } from "./linear/setup.js"; import { PHOTON_SETUP } from "./photon/setup.js"; import { SLACK_SETUP } from "./slack/setup.js"; @@ -10,6 +11,7 @@ export const SETUP_INTEGRATIONS: readonly SetupIntegration[] = [ WEB_SETUP, SLACK_SETUP, DISCORD_SETUP, + GITHUB_SETUP, LINEAR_SETUP, PHOTON_SETUP, ]; From dbc845d6a3b3201460abd18424ec919ce5a9d3a6 Mon Sep 17 00:00:00 2001 From: "vercel[bot]" <35613825+vercel[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 01:44:36 +0000 Subject: [PATCH 2/9] feat(eve): configure GitHub webhook events during setup Signed-off-by: owenkephart Co-Authored-By: owenkephart --- packages/eve/src/cli/dev/tui/setup-flow.ts | 2 +- packages/eve/src/cli/dev/tui/setup-panel.ts | 6 +- .../eve/src/cli/dev/tui/terminal-renderer.ts | 2 +- packages/eve/src/cli/dev/tui/tui-prompter.ts | 7 +- .../setup/integrations/github/connect.test.ts | 5 + .../src/setup/integrations/github/connect.ts | 2 + .../setup/integrations/github/setup.test.ts | 49 +++++++- .../src/setup/integrations/github/setup.ts | 107 +++++++++++++++++- packages/eve/src/setup/prompter.ts | 11 +- .../eve/src/setup/registry-setup-protocol.ts | 2 +- 10 files changed, 172 insertions(+), 21 deletions(-) diff --git a/packages/eve/src/cli/dev/tui/setup-flow.ts b/packages/eve/src/cli/dev/tui/setup-flow.ts index 1221a791b..7edb87b4e 100644 --- a/packages/eve/src/cli/dev/tui/setup-flow.ts +++ b/packages/eve/src/cli/dev/tui/setup-flow.ts @@ -102,7 +102,7 @@ export interface SetupFlowRenderer { */ readChoice(options: ChannelSetupChoiceOptions): ChannelSetupChoice; setStatus(status: SetupFlowStatus | undefined): void; - renderLine(text: string, tone: "info" | "success" | "warning" | "error"): void; + renderLine(text: string, tone: "info" | "success" | "warning" | "error" | "neutral"): void; renderOutput(text: string): void; /** Temporarily restores the terminal while a child process inherits stdio. */ withInheritedStdio(task: () => Promise): Promise; diff --git a/packages/eve/src/cli/dev/tui/setup-panel.ts b/packages/eve/src/cli/dev/tui/setup-panel.ts index 55f165b32..5083f8a12 100644 --- a/packages/eve/src/cli/dev/tui/setup-panel.ts +++ b/packages/eve/src/cli/dev/tui/setup-panel.ts @@ -151,7 +151,7 @@ export interface SetupAcknowledgePanelState { /** One progress line shown inside the flow panel while it runs. */ export interface FlowPanelLine { text: string; - tone: "info" | "success" | "warning" | "error"; + tone: "info" | "success" | "warning" | "error" | "neutral"; /** * Subprocess output a warning/error settle pulled in as its evidence. * Renders like any info line in the panel, but survives the panel close @@ -263,6 +263,8 @@ function toneGlyph(tone: FlowPanelLine["tone"], theme: Theme): string { return c.yellow(theme.glyph.warning); case "error": return c.red(theme.glyph.error); + case "neutral": + return theme.glyph.dot; case "info": return c.dim(theme.glyph.dot); } @@ -309,7 +311,7 @@ export function renderFlowPanel(state: FlowPanelState, theme: Theme, width: numb for (const line of recent) { const text = line.text.split("\n"); for (const [index, part] of text.entries()) { - const body = line.tone === "info" ? c.dim(part) : part; + const body = line.tone === "info" ? c.dim(part) : line.tone === "neutral" ? c.bold(part) : part; const prefix = index === 0 ? `${toneGlyph(line.tone, theme)} ` : " "; rows.push(` ${prefix}${body}`); } diff --git a/packages/eve/src/cli/dev/tui/terminal-renderer.ts b/packages/eve/src/cli/dev/tui/terminal-renderer.ts index 9f26316cb..5fd1d6a12 100644 --- a/packages/eve/src/cli/dev/tui/terminal-renderer.ts +++ b/packages/eve/src/cli/dev/tui/terminal-renderer.ts @@ -2558,7 +2558,7 @@ export class TerminalRenderer implements AgentTUIRenderer { * Commits one persistent flow line to the transcript (progress the user * must keep, like the Slack Connect URL), toned info/success/warning/error. */ - #renderFlowLine(text: string, tone: "info" | "success" | "warning" | "error"): void { + #renderFlowLine(text: string, tone: "info" | "success" | "warning" | "error" | "neutral"): void { const content = stripTerminalControls(text); if (content.trim().length === 0) return; const flow = this.#setupFlow; diff --git a/packages/eve/src/cli/dev/tui/tui-prompter.ts b/packages/eve/src/cli/dev/tui/tui-prompter.ts index aa4e69152..db501675d 100644 --- a/packages/eve/src/cli/dev/tui/tui-prompter.ts +++ b/packages/eve/src/cli/dev/tui/tui-prompter.ts @@ -192,7 +192,12 @@ export function createTuiPrompter(renderer: TuiPrompterRenderer): Prompter { }, note(message, title, options) { - const tone = options?.tone === "success" ? "success" : "warning"; + const tone = + options?.tone === "success" + ? "success" + : options?.tone === "neutral" + ? "neutral" + : "warning"; if (title) renderer.renderLine(title, tone); renderer.renderLine(message, tone); }, diff --git a/packages/eve/src/setup/integrations/github/connect.test.ts b/packages/eve/src/setup/integrations/github/connect.test.ts index 4ec70ce18..f8c68b8f0 100644 --- a/packages/eve/src/setup/integrations/github/connect.test.ts +++ b/packages/eve/src/setup/integrations/github/connect.test.ts @@ -41,6 +41,7 @@ describe("GitHub Connect provisioning", () => { await expect( provisionGitHubConnector({ + events: ["issue_comment", "pull_request_review_comment"], log: log(), project: { orgId: "team_123", projectId: "prj_123" }, projectRoot: "/project", @@ -57,6 +58,10 @@ describe("GitHub Connect provisioning", () => { "--name", "agent", "--triggers", + "--trigger-event", + "issue_comment", + "--trigger-event", + "pull_request_review_comment", "-F", "json", "--scope", diff --git a/packages/eve/src/setup/integrations/github/connect.ts b/packages/eve/src/setup/integrations/github/connect.ts index f6aae404a..ea5e7b1ed 100644 --- a/packages/eve/src/setup/integrations/github/connect.ts +++ b/packages/eve/src/setup/integrations/github/connect.ts @@ -36,6 +36,7 @@ export function parseCreatedGitHubConnector(stdout: string): GitHubConnectorRef /** Creates a GitHub connector and routes its verified webhooks to eve. */ export async function provisionGitHubConnector(input: { + events: readonly string[]; log: ChannelSetupLog; project: VercelProjectReference; projectRoot: string; @@ -54,6 +55,7 @@ export async function provisionGitHubConnector(input: { "--name", input.slug, "--triggers", + ...input.events.flatMap((event) => ["--trigger-event", event]), "-F", "json", "--scope", diff --git a/packages/eve/src/setup/integrations/github/setup.test.ts b/packages/eve/src/setup/integrations/github/setup.test.ts index f501073b5..c51926b38 100644 --- a/packages/eve/src/setup/integrations/github/setup.test.ts +++ b/packages/eve/src/setup/integrations/github/setup.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi } from "vitest"; import { createFakePrompter } from "#internal/testing/fake-prompter.js"; +import type { Asker } from "#setup/ask.js"; import { integrationSetupEnvironment } from "../shared/environment.js"; import { createIntegrationSetupUi } from "../shared/ui.js"; @@ -17,32 +18,68 @@ function deps(): GitHubSetupDeps { } describe("GitHub setup", () => { - it("provisions Connect, routes webhooks, and scaffolds the channel", async () => { + function asker(events = ["issue_comment", "pull_request_review_comment"]): Asker { + return { + ask: vi.fn(), + askMany: vi.fn(async () => events) as Asker["askMany"], + }; + } + + it("provisions Connect, routes the selected webhooks, and scaffolds matching handlers", async () => { const fake = createFakePrompter(); const effects = deps(); + const selectedEvents = ["issue_comment", "issues", "workflow_run"]; await expect( setupGitHub( { appRoot: "/project", environment: integrationSetupEnvironment("authenticated", { kind: "unresolved" }), - ui: createIntegrationSetupUi({ - asker: { ask: vi.fn(), askMany: vi.fn() }, - prompter: fake.prompter, - }), + ui: createIntegrationSetupUi({ asker: asker(selectedEvents), prompter: fake.prompter }), }, effects, ), ).resolves.toMatchObject({ kind: "done" }); + expect(effects.provisionConnector).toHaveBeenCalledWith( + expect.objectContaining({ events: selectedEvents }), + ); expect(effects.writeTextFile).toHaveBeenCalledWith( "/project/agent/channels/github.ts", expect.stringContaining('connectGitHubCredentials("github/agent")'), { force: undefined }, ); + const scaffold = vi.mocked(effects.writeTextFile).mock.calls[0]?.[1] ?? ""; + expect(scaffold).toContain("onIssue(ctx, issue)"); + expect(scaffold).toContain("onWorkflowRun(ctx, workflowRun)"); + expect(scaffold).not.toContain("onPullRequest(ctx, pullRequest)"); expect(effects.openUrl).toHaveBeenCalledOnce(); }); + it("recommends comment events that the default scaffold handles", async () => { + const fake = createFakePrompter(); + const askMany = vi.fn(async () => [ + "issue_comment", + "pull_request_review_comment", + ]) as Asker["askMany"]; + + await setupGitHub( + { + appRoot: "/project", + environment: integrationSetupEnvironment("authenticated", { kind: "unresolved" }), + ui: createIntegrationSetupUi({ asker: { ask: vi.fn(), askMany }, prompter: fake.prompter }), + }, + deps(), + ); + + expect(askMany).toHaveBeenCalledWith( + expect.objectContaining({ + key: "github-events", + recommended: ["issue_comment", "pull_request_review_comment"], + }), + ); + }); + it("requires an authenticated Vercel CLI", async () => { const fake = createFakePrompter(); await expect( @@ -50,7 +87,7 @@ describe("GitHub setup", () => { appRoot: "/project", environment: integrationSetupEnvironment("logged-out", { kind: "unresolved" }), ui: createIntegrationSetupUi({ - asker: { ask: vi.fn(), askMany: vi.fn() }, + asker: asker(), prompter: fake.prompter, }), }), diff --git a/packages/eve/src/setup/integrations/github/setup.ts b/packages/eve/src/setup/integrations/github/setup.ts index 0af699b03..71903cceb 100644 --- a/packages/eve/src/setup/integrations/github/setup.ts +++ b/packages/eve/src/setup/integrations/github/setup.ts @@ -1,5 +1,6 @@ import { join } from "node:path"; +import type { MultiSelectQuestion } from "#setup/ask.js"; import { ensureVercelProject } from "#setup/flows/ensure-vercel-project.js"; import { openUrl } from "#setup/primitives/open-url.js"; import { deriveSlackConnectorSlug } from "#setup/scaffold/index.js"; @@ -29,12 +30,107 @@ const defaultDeps: GitHubSetupDeps = { writeTextFile, }; -function connectTemplate(uid: string): string { +const GITHUB_EVENT_OPTIONS = [ + { + id: "issue_comment", + label: "Issue and pull request comments", + value: "issue_comment", + hint: "Respond when someone mentions the app in an issue or pull request comment.", + }, + { + id: "pull_request_review_comment", + label: "Pull request review comments", + value: "pull_request_review_comment", + hint: "Respond when someone mentions the app in an inline review comment.", + }, + { + id: "issues", + label: "Issues", + value: "issues", + hint: "Start a turn when an issue changes.", + }, + { + id: "pull_request", + label: "Pull requests", + value: "pull_request", + hint: "Start a turn when a pull request changes.", + }, + { + id: "check_suite", + label: "Check suites", + value: "check_suite", + hint: "Start a turn when a check suite changes.", + }, + { + id: "check_run", + label: "Check runs", + value: "check_run", + hint: "Start a turn when a check run changes.", + }, + { + id: "workflow_run", + label: "Workflow runs", + value: "workflow_run", + hint: "Start a turn when a workflow run changes.", + }, +] as const; + +type GitHubWebhookEvent = (typeof GITHUB_EVENT_OPTIONS)[number]["value"]; + +const DEFAULT_GITHUB_EVENTS: readonly GitHubWebhookEvent[] = [ + "issue_comment", + "pull_request_review_comment", +]; + +const githubEventsQuestion: MultiSelectQuestion = { + key: "github-events", + message: "Which GitHub events should this app receive?", + options: GITHUB_EVENT_OPTIONS, + recommended: DEFAULT_GITHUB_EVENTS, + requireSelection: true, +}; + +function connectTemplate(uid: string, events: readonly GitHubWebhookEvent[]): string { + const handlers = [ + events.includes("issues") + ? ` onIssue(ctx, issue) { + if (issue.action !== "opened") return null; + return { auth: defaultGitHubAuth(ctx) }; + },` + : undefined, + events.includes("pull_request") + ? ` onPullRequest(ctx, pullRequest) { + if (pullRequest.action !== "opened") return null; + return { auth: defaultGitHubAuth(ctx) }; + },` + : undefined, + events.includes("check_suite") + ? ` onCheckSuite(ctx, checkSuite) { + if (checkSuite.action !== "completed") return null; + return { auth: defaultGitHubAuth(ctx) }; + },` + : undefined, + events.includes("check_run") + ? ` onCheckRun(ctx, checkRun) { + if (checkRun.action !== "completed") return null; + return { auth: defaultGitHubAuth(ctx) }; + },` + : undefined, + events.includes("workflow_run") + ? ` onWorkflowRun(ctx, workflowRun) { + if (workflowRun.action !== "completed") return null; + return { auth: defaultGitHubAuth(ctx) }; + },` + : undefined, + ].filter((handler): handler is string => handler !== undefined); + const defaultAuthImport = handlers.length > 0 ? ", defaultGitHubAuth" : ""; + const handlerBlock = handlers.length > 0 ? `\n${handlers.join("\n")}` : ""; + return `import { connectGitHubCredentials } from "@vercel/connect/eve"; -import { githubChannel } from "eve/channels/github"; +import { githubChannel${defaultAuthImport} } from "eve/channels/github"; export default githubChannel({ - credentials: connectGitHubCredentials(${JSON.stringify(uid)}), + credentials: connectGitHubCredentials(${JSON.stringify(uid)}),${handlerBlock} }); `; } @@ -53,7 +149,9 @@ export async function setupGitHub( context.ui.prompter.note( "Vercel Connect creates a GitHub App and routes verified webhooks to your deployed agent.", "GitHub App", + { tone: "neutral" }, ); + const events = await context.ui.asker.askMany(githubEventsQuestion); const project = await deps.ensureVercelProject({ appRoot: context.appRoot, prompter: context.ui.prompter, @@ -61,6 +159,7 @@ export async function setupGitHub( }); const connector = await deps.provisionConnector({ log: context.ui.prompter.log, + events, project, projectRoot: context.appRoot, slug: await deps.deriveConnectorSlug(context.appRoot), @@ -68,7 +167,7 @@ export async function setupGitHub( }); await deps.writeTextFile( join(context.appRoot, "agent/channels/github.ts"), - connectTemplate(connector.uid), + connectTemplate(connector.uid, events), { force: context.force }, ); const dashboardUrl = "https://vercel.com/d?to=/%5Bteam%5D/~/connect&title=Open+Vercel+Connect"; diff --git a/packages/eve/src/setup/prompter.ts b/packages/eve/src/setup/prompter.ts index 3b49d4aaa..f69bcfdee 100644 --- a/packages/eve/src/setup/prompter.ts +++ b/packages/eve/src/setup/prompter.ts @@ -173,8 +173,8 @@ export interface EditableSelectOptions extends SingleSe }; } -/** Color intent for {@link Prompter.note}: red warning (default) or green success. */ -export type NoteTone = "warning" | "success"; +/** Color intent for {@link Prompter.note}: red warning (default), green success, or bold neutral. */ +export type NoteTone = "warning" | "success" | "neutral"; /** Input for {@link Prompter.acknowledge}: a heading plus optional body lines. */ export interface AcknowledgeOptions { @@ -240,7 +240,7 @@ export interface Prompter { /** * Rail-attached notice, no bullet — reads as a follow-up to the previous * step. Red by default (warnings, collisions); pass `tone: "success"` for a - * green closing note like the one-shot next steps. + * green closing note or `tone: "neutral"` for bold default terminal text. */ note(message: string, title?: string, options?: { tone?: NoteTone }): void; @@ -502,8 +502,9 @@ export function createPrompter(): Prompter { note(message, title, options) { log.settle(); - const paint = options?.tone === "success" ? pc.green : pc.red; - if (title) process.stdout.write(formatRailLine(paint(pc.bold(title)), pc, process.stdout)); + const paint = + options?.tone === "success" ? pc.green : options?.tone === "neutral" ? pc.bold : pc.red; + if (title) process.stdout.write(formatRailLine(paint(title), pc, process.stdout)); process.stdout.write(formatRailLine(paint(message), pc, process.stdout)); }, diff --git a/packages/eve/src/setup/registry-setup-protocol.ts b/packages/eve/src/setup/registry-setup-protocol.ts index 8f18150da..341849f03 100644 --- a/packages/eve/src/setup/registry-setup-protocol.ts +++ b/packages/eve/src/setup/registry-setup-protocol.ts @@ -64,7 +64,7 @@ export type RegistrySetupChildMessage = level: "message" | "info" | "success" | "warning" | "error" | "commandOutput"; text: string; } - | { type: "note"; message: string; title?: string; tone?: "warning" | "success" } + | { type: "note"; message: string; title?: string; tone?: "warning" | "success" | "neutral" } | { type: "intro" | "outro"; text: string; subtitle?: string } | { type: "result"; outcome: RegistrySetupOutcome } | { From b078fd153d03250dfae828eaa1934def77ce4d53 Mon Sep 17 00:00:00 2001 From: "vercel[bot]" <35613825+vercel[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 02:03:54 +0000 Subject: [PATCH 3/9] fix(eve): use GitHub events create flag Signed-off-by: owenkephart Co-Authored-By: owenkephart --- packages/eve/src/setup/integrations/github/connect.test.ts | 6 ++---- packages/eve/src/setup/integrations/github/connect.ts | 3 ++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/eve/src/setup/integrations/github/connect.test.ts b/packages/eve/src/setup/integrations/github/connect.test.ts index f8c68b8f0..003c09e90 100644 --- a/packages/eve/src/setup/integrations/github/connect.test.ts +++ b/packages/eve/src/setup/integrations/github/connect.test.ts @@ -58,10 +58,8 @@ describe("GitHub Connect provisioning", () => { "--name", "agent", "--triggers", - "--trigger-event", - "issue_comment", - "--trigger-event", - "pull_request_review_comment", + "--events", + "issue_comment,pull_request_review_comment", "-F", "json", "--scope", diff --git a/packages/eve/src/setup/integrations/github/connect.ts b/packages/eve/src/setup/integrations/github/connect.ts index ea5e7b1ed..02649738d 100644 --- a/packages/eve/src/setup/integrations/github/connect.ts +++ b/packages/eve/src/setup/integrations/github/connect.ts @@ -55,7 +55,8 @@ export async function provisionGitHubConnector(input: { "--name", input.slug, "--triggers", - ...input.events.flatMap((event) => ["--trigger-event", event]), + "--events", + input.events.join(","), "-F", "json", "--scope", From 1eba7fa520f1a83be82a55a1e5e034ccb4c920af Mon Sep 17 00:00:00 2001 From: "vercel[bot]" <35613825+vercel[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 02:25:05 +0000 Subject: [PATCH 4/9] fix(eve): use repeatable trigger event flags Signed-off-by: owenkephart Co-Authored-By: owenkephart --- packages/eve/src/setup/integrations/github/connect.test.ts | 6 ++++-- packages/eve/src/setup/integrations/github/connect.ts | 3 +-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/eve/src/setup/integrations/github/connect.test.ts b/packages/eve/src/setup/integrations/github/connect.test.ts index 003c09e90..f8c68b8f0 100644 --- a/packages/eve/src/setup/integrations/github/connect.test.ts +++ b/packages/eve/src/setup/integrations/github/connect.test.ts @@ -58,8 +58,10 @@ describe("GitHub Connect provisioning", () => { "--name", "agent", "--triggers", - "--events", - "issue_comment,pull_request_review_comment", + "--trigger-event", + "issue_comment", + "--trigger-event", + "pull_request_review_comment", "-F", "json", "--scope", diff --git a/packages/eve/src/setup/integrations/github/connect.ts b/packages/eve/src/setup/integrations/github/connect.ts index 02649738d..ea5e7b1ed 100644 --- a/packages/eve/src/setup/integrations/github/connect.ts +++ b/packages/eve/src/setup/integrations/github/connect.ts @@ -55,8 +55,7 @@ export async function provisionGitHubConnector(input: { "--name", input.slug, "--triggers", - "--events", - input.events.join(","), + ...input.events.flatMap((event) => ["--trigger-event", event]), "-F", "json", "--scope", From dfe660609fee0a789eb66c8bd01e3baa08c2ad46 Mon Sep 17 00:00:00 2001 From: owenkephart Date: Mon, 3 Aug 2026 15:58:11 +0000 Subject: [PATCH 5/9] refactor(eve): simplify guided setup presentation Signed-off-by: owenkephart --- apps/docs/registry.json | 8 ++++---- packages/eve/src/cli/dev/tui/setup-flow.ts | 2 +- packages/eve/src/cli/dev/tui/setup-panel.ts | 6 ++---- packages/eve/src/cli/dev/tui/terminal-renderer.ts | 2 +- packages/eve/src/cli/dev/tui/tui-prompter.ts | 6 +----- packages/eve/src/setup/integrations/github/setup.ts | 2 +- packages/eve/src/setup/prompter.ts | 8 ++++---- packages/eve/src/setup/registry-setup-protocol.ts | 2 +- 8 files changed, 15 insertions(+), 21 deletions(-) diff --git a/apps/docs/registry.json b/apps/docs/registry.json index 1fee1e238..5277aa3e0 100644 --- a/apps/docs/registry.json +++ b/apps/docs/registry.json @@ -1135,14 +1135,14 @@ "requires": ">=0.27.8" } }, - "dependencies": ["@vercel/connect@0.4.2"] + "dependencies": ["@vercel/connect@>=0.4.2"] }, { "name": "channel/discord", "type": "registry:item", "title": "Discord", "description": "Connect an eve agent to Discord with guided connector and slash-command setup.", - "dependencies": ["@vercel/connect@0.6.0"], + "dependencies": ["@vercel/connect@>=0.6.0"], "meta": { "eve": { "setup": { @@ -1221,7 +1221,7 @@ "requires": ">=0.30.7" } }, - "dependencies": ["@vercel/connect"] + "dependencies": ["@vercel/connect@>=0.6.0"] }, { "name": "channel/linear-agent", @@ -1681,7 +1681,7 @@ "type": "registry:item", "title": "Photon iMessage", "description": "Connect an eve agent to iMessage through Photon with guided project and phone setup.", - "dependencies": ["@vercel/connect@0.5.0"], + "dependencies": ["@vercel/connect@>=0.5.0"], "meta": { "eve": { "setup": { diff --git a/packages/eve/src/cli/dev/tui/setup-flow.ts b/packages/eve/src/cli/dev/tui/setup-flow.ts index 7edb87b4e..1221a791b 100644 --- a/packages/eve/src/cli/dev/tui/setup-flow.ts +++ b/packages/eve/src/cli/dev/tui/setup-flow.ts @@ -102,7 +102,7 @@ export interface SetupFlowRenderer { */ readChoice(options: ChannelSetupChoiceOptions): ChannelSetupChoice; setStatus(status: SetupFlowStatus | undefined): void; - renderLine(text: string, tone: "info" | "success" | "warning" | "error" | "neutral"): void; + renderLine(text: string, tone: "info" | "success" | "warning" | "error"): void; renderOutput(text: string): void; /** Temporarily restores the terminal while a child process inherits stdio. */ withInheritedStdio(task: () => Promise): Promise; diff --git a/packages/eve/src/cli/dev/tui/setup-panel.ts b/packages/eve/src/cli/dev/tui/setup-panel.ts index 5083f8a12..55f165b32 100644 --- a/packages/eve/src/cli/dev/tui/setup-panel.ts +++ b/packages/eve/src/cli/dev/tui/setup-panel.ts @@ -151,7 +151,7 @@ export interface SetupAcknowledgePanelState { /** One progress line shown inside the flow panel while it runs. */ export interface FlowPanelLine { text: string; - tone: "info" | "success" | "warning" | "error" | "neutral"; + tone: "info" | "success" | "warning" | "error"; /** * Subprocess output a warning/error settle pulled in as its evidence. * Renders like any info line in the panel, but survives the panel close @@ -263,8 +263,6 @@ function toneGlyph(tone: FlowPanelLine["tone"], theme: Theme): string { return c.yellow(theme.glyph.warning); case "error": return c.red(theme.glyph.error); - case "neutral": - return theme.glyph.dot; case "info": return c.dim(theme.glyph.dot); } @@ -311,7 +309,7 @@ export function renderFlowPanel(state: FlowPanelState, theme: Theme, width: numb for (const line of recent) { const text = line.text.split("\n"); for (const [index, part] of text.entries()) { - const body = line.tone === "info" ? c.dim(part) : line.tone === "neutral" ? c.bold(part) : part; + const body = line.tone === "info" ? c.dim(part) : part; const prefix = index === 0 ? `${toneGlyph(line.tone, theme)} ` : " "; rows.push(` ${prefix}${body}`); } diff --git a/packages/eve/src/cli/dev/tui/terminal-renderer.ts b/packages/eve/src/cli/dev/tui/terminal-renderer.ts index 5fd1d6a12..9f26316cb 100644 --- a/packages/eve/src/cli/dev/tui/terminal-renderer.ts +++ b/packages/eve/src/cli/dev/tui/terminal-renderer.ts @@ -2558,7 +2558,7 @@ export class TerminalRenderer implements AgentTUIRenderer { * Commits one persistent flow line to the transcript (progress the user * must keep, like the Slack Connect URL), toned info/success/warning/error. */ - #renderFlowLine(text: string, tone: "info" | "success" | "warning" | "error" | "neutral"): void { + #renderFlowLine(text: string, tone: "info" | "success" | "warning" | "error"): void { const content = stripTerminalControls(text); if (content.trim().length === 0) return; const flow = this.#setupFlow; diff --git a/packages/eve/src/cli/dev/tui/tui-prompter.ts b/packages/eve/src/cli/dev/tui/tui-prompter.ts index db501675d..40b9535a1 100644 --- a/packages/eve/src/cli/dev/tui/tui-prompter.ts +++ b/packages/eve/src/cli/dev/tui/tui-prompter.ts @@ -193,11 +193,7 @@ export function createTuiPrompter(renderer: TuiPrompterRenderer): Prompter { note(message, title, options) { const tone = - options?.tone === "success" - ? "success" - : options?.tone === "neutral" - ? "neutral" - : "warning"; + options?.tone === "success" ? "success" : options?.tone === "info" ? "info" : "warning"; if (title) renderer.renderLine(title, tone); renderer.renderLine(message, tone); }, diff --git a/packages/eve/src/setup/integrations/github/setup.ts b/packages/eve/src/setup/integrations/github/setup.ts index 71903cceb..817000f80 100644 --- a/packages/eve/src/setup/integrations/github/setup.ts +++ b/packages/eve/src/setup/integrations/github/setup.ts @@ -149,7 +149,7 @@ export async function setupGitHub( context.ui.prompter.note( "Vercel Connect creates a GitHub App and routes verified webhooks to your deployed agent.", "GitHub App", - { tone: "neutral" }, + { tone: "info" }, ); const events = await context.ui.asker.askMany(githubEventsQuestion); const project = await deps.ensureVercelProject({ diff --git a/packages/eve/src/setup/prompter.ts b/packages/eve/src/setup/prompter.ts index f69bcfdee..a6fbd80e2 100644 --- a/packages/eve/src/setup/prompter.ts +++ b/packages/eve/src/setup/prompter.ts @@ -173,8 +173,8 @@ export interface EditableSelectOptions extends SingleSe }; } -/** Color intent for {@link Prompter.note}: red warning (default), green success, or bold neutral. */ -export type NoteTone = "warning" | "success" | "neutral"; +/** Color intent for {@link Prompter.note}: red warning (default), green success, or dim info. */ +export type NoteTone = "warning" | "success" | "info"; /** Input for {@link Prompter.acknowledge}: a heading plus optional body lines. */ export interface AcknowledgeOptions { @@ -240,7 +240,7 @@ export interface Prompter { /** * Rail-attached notice, no bullet — reads as a follow-up to the previous * step. Red by default (warnings, collisions); pass `tone: "success"` for a - * green closing note or `tone: "neutral"` for bold default terminal text. + * green closing note or `tone: "info"` for a subdued informational note. */ note(message: string, title?: string, options?: { tone?: NoteTone }): void; @@ -503,7 +503,7 @@ export function createPrompter(): Prompter { note(message, title, options) { log.settle(); const paint = - options?.tone === "success" ? pc.green : options?.tone === "neutral" ? pc.bold : pc.red; + options?.tone === "success" ? pc.green : options?.tone === "info" ? pc.dim : pc.red; if (title) process.stdout.write(formatRailLine(paint(title), pc, process.stdout)); process.stdout.write(formatRailLine(paint(message), pc, process.stdout)); }, diff --git a/packages/eve/src/setup/registry-setup-protocol.ts b/packages/eve/src/setup/registry-setup-protocol.ts index 341849f03..ecadcbb6e 100644 --- a/packages/eve/src/setup/registry-setup-protocol.ts +++ b/packages/eve/src/setup/registry-setup-protocol.ts @@ -64,7 +64,7 @@ export type RegistrySetupChildMessage = level: "message" | "info" | "success" | "warning" | "error" | "commandOutput"; text: string; } - | { type: "note"; message: string; title?: string; tone?: "warning" | "success" | "neutral" } + | { type: "note"; message: string; title?: string; tone?: "warning" | "success" | "info" } | { type: "intro" | "outro"; text: string; subtitle?: string } | { type: "result"; outcome: RegistrySetupOutcome } | { From 186e8adbe8c2a07d33020f533908d9ecb7802ba2 Mon Sep 17 00:00:00 2001 From: owenkephart Date: Tue, 4 Aug 2026 14:19:13 -0500 Subject: [PATCH 6/9] fix(eve): polish GitHub setup flow Signed-off-by: owenkephart --- .../src/cli/dev/tui/terminal-renderer.test.ts | 18 ++++++++++++++++++ .../eve/src/cli/dev/tui/terminal-renderer.ts | 6 ++++++ packages/eve/src/cli/dev/tui/tui-prompter.ts | 3 +-- .../setup/integrations/github/setup.test.ts | 2 -- .../eve/src/setup/integrations/github/setup.ts | 9 ++------- packages/eve/src/setup/prompter.ts | 11 +++++------ .../eve/src/setup/registry-setup-protocol.ts | 2 +- 7 files changed, 33 insertions(+), 18 deletions(-) diff --git a/packages/eve/src/cli/dev/tui/terminal-renderer.test.ts b/packages/eve/src/cli/dev/tui/terminal-renderer.test.ts index 4175d7f9a..97d57005a 100644 --- a/packages/eve/src/cli/dev/tui/terminal-renderer.test.ts +++ b/packages/eve/src/cli/dev/tui/terminal-renderer.test.ts @@ -1490,6 +1490,24 @@ describe("TerminalRenderer (inline scrollback)", () => { expect(screen.snapshot()).toContain("⎿ ✓ Registry items added: connection/linear."); }); + it("keeps a setup command result tight after its flow closes", async () => { + const { screen, input, renderer } = makeRenderer(); + + const prompt = renderer.readPrompt(); + input.type("/add"); + input.enter(); + await prompt; + renderer.setupFlow.begin("/add"); + renderer.setupFlow.end({ preserveDiagnostics: false }); + renderer.renderCommandResult("Registry items added: channel/github.", "success"); + renderer.shutdown(); + + const lines = screen.snapshot().split("\n"); + const command = lines.findIndex((line) => line.includes("│ /add")); + const result = lines.findIndex((line) => line.includes("⎿ ✓ Registry items added")); + expect(result).toBe(command + 1); + }); + it("marks a failed automatic command and keeps its multiline outcome in one result block", () => { const { screen, renderer } = makeRenderer(); renderer.renderCommandInvocation("/vc:login", "failed"); diff --git a/packages/eve/src/cli/dev/tui/terminal-renderer.ts b/packages/eve/src/cli/dev/tui/terminal-renderer.ts index 9f26316cb..11b279204 100644 --- a/packages/eve/src/cli/dev/tui/terminal-renderer.ts +++ b/packages/eve/src/cli/dev/tui/terminal-renderer.ts @@ -1648,6 +1648,8 @@ export class TerminalRenderer implements AgentTUIRenderer { const content = stripTerminalControls(text); if (content.trim().length === 0) return; this.#start(); + const latest = this.#blocks.at(-1); + if (latest?.kind === "command") latest.live = false; this.#pushBlock( tone === "success" ? { kind: "result", body: content, live: false, status: "done" } @@ -1695,6 +1697,10 @@ export class TerminalRenderer implements AgentTUIRenderer { if (flow === undefined) return; this.#setupFlow = undefined; this.#stopTicker(); + // The result belongs to the slash command that opened this flow. Its + // elbow should stay adjacent even though closing the panel repaints first. + const latest = this.#blocks.at(-1); + if (latest?.kind === "command") latest.live = true; if (preserveDiagnostics) { let evidence: string[] = []; diff --git a/packages/eve/src/cli/dev/tui/tui-prompter.ts b/packages/eve/src/cli/dev/tui/tui-prompter.ts index 40b9535a1..aa4e69152 100644 --- a/packages/eve/src/cli/dev/tui/tui-prompter.ts +++ b/packages/eve/src/cli/dev/tui/tui-prompter.ts @@ -192,8 +192,7 @@ export function createTuiPrompter(renderer: TuiPrompterRenderer): Prompter { }, note(message, title, options) { - const tone = - options?.tone === "success" ? "success" : options?.tone === "info" ? "info" : "warning"; + const tone = options?.tone === "success" ? "success" : "warning"; if (title) renderer.renderLine(title, tone); renderer.renderLine(message, tone); }, diff --git a/packages/eve/src/setup/integrations/github/setup.test.ts b/packages/eve/src/setup/integrations/github/setup.test.ts index c51926b38..f00779bc3 100644 --- a/packages/eve/src/setup/integrations/github/setup.test.ts +++ b/packages/eve/src/setup/integrations/github/setup.test.ts @@ -11,7 +11,6 @@ function deps(): GitHubSetupDeps { return { deriveConnectorSlug: vi.fn(async () => "agent" as never), ensureVercelProject: vi.fn(async () => ({ orgId: "team-id", projectId: "project-id" })), - openUrl: vi.fn(), provisionConnector: vi.fn(async () => ({ id: "scl_github", uid: "github/agent" })), writeTextFile: vi.fn(async () => {}), }; @@ -53,7 +52,6 @@ describe("GitHub setup", () => { expect(scaffold).toContain("onIssue(ctx, issue)"); expect(scaffold).toContain("onWorkflowRun(ctx, workflowRun)"); expect(scaffold).not.toContain("onPullRequest(ctx, pullRequest)"); - expect(effects.openUrl).toHaveBeenCalledOnce(); }); it("recommends comment events that the default scaffold handles", async () => { diff --git a/packages/eve/src/setup/integrations/github/setup.ts b/packages/eve/src/setup/integrations/github/setup.ts index 817000f80..e6b0572a8 100644 --- a/packages/eve/src/setup/integrations/github/setup.ts +++ b/packages/eve/src/setup/integrations/github/setup.ts @@ -2,7 +2,6 @@ import { join } from "node:path"; import type { MultiSelectQuestion } from "#setup/ask.js"; import { ensureVercelProject } from "#setup/flows/ensure-vercel-project.js"; -import { openUrl } from "#setup/primitives/open-url.js"; import { deriveSlackConnectorSlug } from "#setup/scaffold/index.js"; import { writeTextFile } from "#setup/scaffold/files.js"; import { WizardCancelledError } from "#setup/step.js"; @@ -17,7 +16,6 @@ import { provisionGitHubConnector } from "./connect.js"; export interface GitHubSetupDeps { deriveConnectorSlug: typeof deriveSlackConnectorSlug; ensureVercelProject: typeof ensureVercelProject; - openUrl: typeof openUrl; provisionConnector: typeof provisionGitHubConnector; writeTextFile: typeof writeTextFile; } @@ -25,7 +23,6 @@ export interface GitHubSetupDeps { const defaultDeps: GitHubSetupDeps = { deriveConnectorSlug: deriveSlackConnectorSlug, ensureVercelProject, - openUrl, provisionConnector: provisionGitHubConnector, writeTextFile, }; @@ -146,10 +143,9 @@ export async function setupGitHub( ); } try { - context.ui.prompter.note( + context.ui.prompter.log.info("GitHub App"); + context.ui.prompter.log.info( "Vercel Connect creates a GitHub App and routes verified webhooks to your deployed agent.", - "GitHub App", - { tone: "info" }, ); const events = await context.ui.asker.askMany(githubEventsQuestion); const project = await deps.ensureVercelProject({ @@ -175,7 +171,6 @@ export async function setupGitHub( "Deploy the agent, then open the GitHub App in Vercel Connect and install it in the organization or account where you want to use it.", "Mention the app in an issue, pull request, or review comment to start a conversation.", ]); - deps.openUrl(dashboardUrl); return { kind: "done", facts: [{ label: "Vercel Connect", value: dashboardUrl, kind: "url" }], diff --git a/packages/eve/src/setup/prompter.ts b/packages/eve/src/setup/prompter.ts index a6fbd80e2..3b49d4aaa 100644 --- a/packages/eve/src/setup/prompter.ts +++ b/packages/eve/src/setup/prompter.ts @@ -173,8 +173,8 @@ export interface EditableSelectOptions extends SingleSe }; } -/** Color intent for {@link Prompter.note}: red warning (default), green success, or dim info. */ -export type NoteTone = "warning" | "success" | "info"; +/** Color intent for {@link Prompter.note}: red warning (default) or green success. */ +export type NoteTone = "warning" | "success"; /** Input for {@link Prompter.acknowledge}: a heading plus optional body lines. */ export interface AcknowledgeOptions { @@ -240,7 +240,7 @@ export interface Prompter { /** * Rail-attached notice, no bullet — reads as a follow-up to the previous * step. Red by default (warnings, collisions); pass `tone: "success"` for a - * green closing note or `tone: "info"` for a subdued informational note. + * green closing note like the one-shot next steps. */ note(message: string, title?: string, options?: { tone?: NoteTone }): void; @@ -502,9 +502,8 @@ export function createPrompter(): Prompter { note(message, title, options) { log.settle(); - const paint = - options?.tone === "success" ? pc.green : options?.tone === "info" ? pc.dim : pc.red; - if (title) process.stdout.write(formatRailLine(paint(title), pc, process.stdout)); + const paint = options?.tone === "success" ? pc.green : pc.red; + if (title) process.stdout.write(formatRailLine(paint(pc.bold(title)), pc, process.stdout)); process.stdout.write(formatRailLine(paint(message), pc, process.stdout)); }, diff --git a/packages/eve/src/setup/registry-setup-protocol.ts b/packages/eve/src/setup/registry-setup-protocol.ts index ecadcbb6e..8f18150da 100644 --- a/packages/eve/src/setup/registry-setup-protocol.ts +++ b/packages/eve/src/setup/registry-setup-protocol.ts @@ -64,7 +64,7 @@ export type RegistrySetupChildMessage = level: "message" | "info" | "success" | "warning" | "error" | "commandOutput"; text: string; } - | { type: "note"; message: string; title?: string; tone?: "warning" | "success" | "info" } + | { type: "note"; message: string; title?: string; tone?: "warning" | "success" } | { type: "intro" | "outro"; text: string; subtitle?: string } | { type: "result"; outcome: RegistrySetupOutcome } | { From 7e3b550211631b6e0b7943f528a1b66f21ddc4bd Mon Sep 17 00:00:00 2001 From: owenkephart Date: Tue, 4 Aug 2026 15:50:24 -0500 Subject: [PATCH 7/9] fix(eve): configure GitHub App mentions Signed-off-by: owenkephart --- docs/channels/github.mdx | 4 +- .../setup/integrations/github/connect.test.ts | 37 ++++++++---- .../src/setup/integrations/github/connect.ts | 57 ++++++++++++++++++- .../setup/integrations/github/setup.test.ts | 7 ++- .../src/setup/integrations/github/setup.ts | 42 ++++++++------ 5 files changed, 113 insertions(+), 34 deletions(-) diff --git a/docs/channels/github.mdx b/docs/channels/github.mdx index 5fd6e5efa..d6f7086f9 100644 --- a/docs/channels/github.mdx +++ b/docs/channels/github.mdx @@ -16,7 +16,7 @@ eve add channel/github The flow checks that the Vercel CLI is authenticated, creates or links a Vercel project when needed, provisions an app-scoped GitHub Connect client, and replaces its default trigger with `/eve/v1/github`. It then installs `@vercel/connect` and writes `agent/channels/github.ts` with the connector UID. -Vercel Connect creates the GitHub App, receives and verifies its webhooks, and forwards them to the deployed agent. After deploying, open the GitHub App in the Connect dashboard and install it in the organization or account where you want to use it. Mention the app in an issue, pull request, or review comment to start a conversation. +Vercel Connect creates the GitHub App, receives and verifies its webhooks, and forwards them to the deployed agent. After deploying, open the GitHub App in the Connect dashboard and install it in the organization or account where you want to use it. Mention the generated app handle (for example, `@my-agent`) in an issue, pull request, or review comment to start a conversation. The generated channel uses Connect-managed credentials: @@ -60,7 +60,7 @@ GITHUB_APP_SLUG=... # supplies botName when it is not set in config `appId`/`privateKey`/`webhookSecret` also take a lazy resolver function if you'd rather fetch them on demand. -Point the GitHub App webhook URL at `https:///eve/v1/github`. For mention-driven turns, subscribe to `issue_comment` and `pull_request_review_comment`; add `issues`, `pull_request`, `check_suite`, `check_run`, or `workflow_run` if you wire up their opt-in hooks. A comment that `@mention`s `botName` starts a turn. +Point the GitHub App webhook URL at `https:///eve/v1/github`. For mention-driven turns, subscribe to `issue_comment` and `pull_request_review_comment`; add `issues`, `pull_request`, `check_suite`, `check_run`, or `workflow_run` if you wire up their opt-in hooks. After installing the App for the repository, a new comment that includes `@botName` starts a turn. GitHub may display the App as `botName[bot]`, but write the mention without the `[bot]` suffix. ## How the channel handles messages diff --git a/packages/eve/src/setup/integrations/github/connect.test.ts b/packages/eve/src/setup/integrations/github/connect.test.ts index f8c68b8f0..4f046e334 100644 --- a/packages/eve/src/setup/integrations/github/connect.test.ts +++ b/packages/eve/src/setup/integrations/github/connect.test.ts @@ -28,15 +28,27 @@ describe("GitHub Connect provisioning", () => { }); it("creates the connector and replaces its trigger", async () => { - const runVercelCaptureStdout = vi.fn(async () => ({ - ok: true as const, - stdout: JSON.stringify({ - id: "scl_github", - uid: "github/agent", - supportedSubjectTypes: ["app"], - }), - stderr: "", - })); + const runVercelCaptureStdout = vi + .fn() + .mockResolvedValueOnce({ + ok: true as const, + stdout: JSON.stringify({ + id: "scl_github", + uid: "github/agent", + supportedSubjectTypes: ["app"], + }), + stderr: "", + }) + .mockResolvedValueOnce({ + ok: true as const, + stdout: JSON.stringify({ + data: { appSlug: "agent" }, + id: "scl_github", + type: "github", + uid: "github/agent", + }), + stderr: "", + }); const runVercel = vi.fn(async () => true); await expect( @@ -48,7 +60,7 @@ describe("GitHub Connect provisioning", () => { slug: "agent", deps: { runVercel, runVercelCaptureStdout }, }), - ).resolves.toEqual({ id: "scl_github", uid: "github/agent" }); + ).resolves.toEqual({ appSlug: "agent", id: "scl_github", uid: "github/agent" }); expect(runVercelCaptureStdout).toHaveBeenCalledWith( [ @@ -69,6 +81,11 @@ describe("GitHub Connect provisioning", () => { ], expect.objectContaining({ cwd: "/project", nonInteractive: true }), ); + expect(runVercelCaptureStdout).toHaveBeenNthCalledWith( + 2, + ["api", "/v1/connect/connectors/scl_github", "--scope", "team_123", "--raw"], + expect.objectContaining({ cwd: "/project", nonInteractive: true }), + ); expect(runVercel).toHaveBeenNthCalledWith( 1, ["connect", "detach", "github/agent", "--project", "prj_123", "--yes", "--scope", "team_123"], diff --git a/packages/eve/src/setup/integrations/github/connect.ts b/packages/eve/src/setup/integrations/github/connect.ts index ea5e7b1ed..03be247e0 100644 --- a/packages/eve/src/setup/integrations/github/connect.ts +++ b/packages/eve/src/setup/integrations/github/connect.ts @@ -8,6 +8,8 @@ export const GITHUB_TRIGGER_PATH = "/eve/v1/github"; /** Identity of the GitHub connector provisioned for an agent channel. */ export interface GitHubConnectorRef { + /** GitHub's actual @mention handle, without the `[bot]` suffix. */ + appSlug: string; id: string; uid: string; } @@ -24,8 +26,19 @@ const GitHubConnectorRefSchema = z.object({ supportedSubjectTypes: z.array(z.string()).refine((types) => types.includes("app")), }); +const GitHubConnectorDetailsSchema = z.object({ + data: z.object({ + appSlug: z.string().min(1), + }), + id: z.string().min(1), + type: z.literal("github"), + uid: z.string().min(1), +}); + /** Parses `vercel connect create -F json` output for an app-scoped GitHub connector. */ -export function parseCreatedGitHubConnector(stdout: string): GitHubConnectorRef | undefined { +export function parseCreatedGitHubConnector( + stdout: string, +): Omit | undefined { try { const parsed = GitHubConnectorRefSchema.safeParse(JSON.parse(stdout)); return parsed.success ? { id: parsed.data.id, uid: parsed.data.uid } : undefined; @@ -34,6 +47,21 @@ export function parseCreatedGitHubConnector(stdout: string): GitHubConnectorRef } } +function parseGitHubConnectorDetails( + stdout: string, + connector: Omit, +): GitHubConnectorRef | undefined { + try { + const parsed = GitHubConnectorDetailsSchema.safeParse(JSON.parse(stdout)); + if (!parsed.success || parsed.data.id !== connector.id || parsed.data.uid !== connector.uid) { + return undefined; + } + return { ...connector, appSlug: parsed.data.data.appSlug }; + } catch { + return undefined; + } +} + /** Creates a GitHub connector and routes its verified webhooks to eve. */ export async function provisionGitHubConnector(input: { events: readonly string[]; @@ -78,8 +106,31 @@ export async function provisionGitHubConnector(input: { detail ? `GitHub connector creation failed:\n${detail}` : "GitHub connector creation failed.", ); } - const connector = parseCreatedGitHubConnector(result.stdout); - if (connector === undefined) throw new Error("Vercel returned an invalid GitHub connector."); + const created = parseCreatedGitHubConnector(result.stdout); + if (created === undefined) throw new Error("Vercel returned an invalid GitHub connector."); + + const details = await withPhase(input.log, "Reading GitHub App details...", () => + deps.runVercelCaptureStdout( + [ + "api", + `/v1/connect/connectors/${encodeURIComponent(created.id)}`, + "--scope", + input.project.orgId, + "--raw", + ], + { + cwd: input.projectRoot, + nonInteractive: true, + onOutput, + signal: input.signal, + }, + ), + ); + input.signal?.throwIfAborted(); + if (!details.ok) throw new Error("Could not read the created GitHub connector."); + const connector = parseGitHubConnectorDetails(details.stdout, created); + if (connector === undefined) + throw new Error("Vercel returned invalid details for the GitHub connector."); const attachment = await withPhase(input.log, "Connecting GitHub webhooks...", () => replaceConnectTrigger({ diff --git a/packages/eve/src/setup/integrations/github/setup.test.ts b/packages/eve/src/setup/integrations/github/setup.test.ts index f00779bc3..8b4d2c679 100644 --- a/packages/eve/src/setup/integrations/github/setup.test.ts +++ b/packages/eve/src/setup/integrations/github/setup.test.ts @@ -11,7 +11,11 @@ function deps(): GitHubSetupDeps { return { deriveConnectorSlug: vi.fn(async () => "agent" as never), ensureVercelProject: vi.fn(async () => ({ orgId: "team-id", projectId: "project-id" })), - provisionConnector: vi.fn(async () => ({ id: "scl_github", uid: "github/agent" })), + provisionConnector: vi.fn(async () => ({ + appSlug: "agent", + id: "scl_github", + uid: "github/agent", + })), writeTextFile: vi.fn(async () => {}), }; } @@ -49,6 +53,7 @@ describe("GitHub setup", () => { { force: undefined }, ); const scaffold = vi.mocked(effects.writeTextFile).mock.calls[0]?.[1] ?? ""; + expect(scaffold).toContain('botName: "agent"'); expect(scaffold).toContain("onIssue(ctx, issue)"); expect(scaffold).toContain("onWorkflowRun(ctx, workflowRun)"); expect(scaffold).not.toContain("onPullRequest(ctx, pullRequest)"); diff --git a/packages/eve/src/setup/integrations/github/setup.ts b/packages/eve/src/setup/integrations/github/setup.ts index e6b0572a8..b2afedb4c 100644 --- a/packages/eve/src/setup/integrations/github/setup.ts +++ b/packages/eve/src/setup/integrations/github/setup.ts @@ -30,45 +30,45 @@ const defaultDeps: GitHubSetupDeps = { const GITHUB_EVENT_OPTIONS = [ { id: "issue_comment", - label: "Issue and pull request comments", + label: "New issue and pull request comments", value: "issue_comment", - hint: "Respond when someone mentions the app in an issue or pull request comment.", + hint: "Start a turn when a new timeline comment @mentions the app.", }, { id: "pull_request_review_comment", - label: "Pull request review comments", + label: "New inline pull request review comments", value: "pull_request_review_comment", - hint: "Respond when someone mentions the app in an inline review comment.", + hint: "Start a turn when a new inline review comment @mentions the app.", }, { id: "issues", - label: "Issues", + label: "New issues", value: "issues", - hint: "Start a turn when an issue changes.", + hint: "Start a turn when an issue is opened. Other issue changes are ignored.", }, { id: "pull_request", - label: "Pull requests", + label: "New pull requests", value: "pull_request", - hint: "Start a turn when a pull request changes.", + hint: "Start a turn when a pull request is opened. Other pull request changes are ignored.", }, { id: "check_suite", - label: "Check suites", + label: "Completed check suites", value: "check_suite", - hint: "Start a turn when a check suite changes.", + hint: "Start a turn when a check suite completes for a pull request, including successful and failed suites.", }, { id: "check_run", - label: "Check runs", + label: "Completed check runs", value: "check_run", - hint: "Start a turn when a check run changes.", + hint: "Start a turn when a check run completes for a pull request, including successful and failed runs.", }, { id: "workflow_run", - label: "Workflow runs", + label: "Completed GitHub Actions workflow runs", value: "workflow_run", - hint: "Start a turn when a workflow run changes.", + hint: "Start a turn when a GitHub Actions workflow run completes for a pull request, including successful and failed runs.", }, ] as const; @@ -81,13 +81,18 @@ const DEFAULT_GITHUB_EVENTS: readonly GitHubWebhookEvent[] = [ const githubEventsQuestion: MultiSelectQuestion = { key: "github-events", - message: "Which GitHub events should this app receive?", + message: + "Which GitHub webhook events should this app subscribe to? The generated channel starts turns only for the conditions described below.", options: GITHUB_EVENT_OPTIONS, recommended: DEFAULT_GITHUB_EVENTS, requireSelection: true, }; -function connectTemplate(uid: string, events: readonly GitHubWebhookEvent[]): string { +function connectTemplate( + uid: string, + appSlug: string, + events: readonly GitHubWebhookEvent[], +): string { const handlers = [ events.includes("issues") ? ` onIssue(ctx, issue) { @@ -127,6 +132,7 @@ function connectTemplate(uid: string, events: readonly GitHubWebhookEvent[]): st import { githubChannel${defaultAuthImport} } from "eve/channels/github"; export default githubChannel({ + botName: ${JSON.stringify(appSlug)}, credentials: connectGitHubCredentials(${JSON.stringify(uid)}),${handlerBlock} }); `; @@ -163,13 +169,13 @@ export async function setupGitHub( }); await deps.writeTextFile( join(context.appRoot, "agent/channels/github.ts"), - connectTemplate(connector.uid, events), + connectTemplate(connector.uid, connector.appSlug, events), { force: context.force }, ); const dashboardUrl = "https://vercel.com/d?to=/%5Bteam%5D/~/connect&title=Open+Vercel+Connect"; context.ui.nextSteps([ "Deploy the agent, then open the GitHub App in Vercel Connect and install it in the organization or account where you want to use it.", - "Mention the app in an issue, pull request, or review comment to start a conversation.", + `Mention @${connector.appSlug} in an issue, pull request, or review comment to start a conversation.`, ]); return { kind: "done", From 8ea7127e55ea8a83fc5f79dba76048c86ec9ba90 Mon Sep 17 00:00:00 2001 From: owenkephart Date: Wed, 5 Aug 2026 10:21:04 -0500 Subject: [PATCH 8/9] docs(eve): describe GitHub comment invocation Signed-off-by: owenkephart --- apps/docs/lib/integrations/data.ts | 2 +- docs/channels/github.mdx | 12 ++++++------ packages/eve/src/setup/integrations/github/setup.ts | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/docs/lib/integrations/data.ts b/apps/docs/lib/integrations/data.ts index 92d205ce3..582a353af 100644 --- a/apps/docs/lib/integrations/data.ts +++ b/apps/docs/lib/integrations/data.ts @@ -287,7 +287,7 @@ export default githubChannel({ credentials: connectGitHubCredentials("github/my-agent"), }); \`\`\``, - configure: `Sign in to Vercel, then let the guided flow create or link a project, provision the GitHub App, and attach its verified webhook trigger to \`/eve/v1/github\`. Deploy, install the app from Vercel Connect, then mention it in an issue, pull request, or review comment. See the [GitHub channel docs](/docs/channels/github) for permissions and events.`, + configure: `Sign in to Vercel, then let the guided flow create or link a project, provision the GitHub App, and attach its verified webhook trigger to \`/eve/v1/github\`. Deploy, install the app from Vercel Connect, then add its \`@handle\` invocation token to a new issue, pull request, or review comment. GitHub may not autocomplete or render the token as a linked mention. See the [GitHub channel docs](/docs/channels/github) for permissions and events.`, }, "linear-agent": { logo: "linear", diff --git a/docs/channels/github.mdx b/docs/channels/github.mdx index d6f7086f9..de0400160 100644 --- a/docs/channels/github.mdx +++ b/docs/channels/github.mdx @@ -1,10 +1,10 @@ --- title: "GitHub" -description: "Reach your agent from GitHub App webhooks, with @mention dispatch, PR diff context, sandbox checkout, and Vercel Connect credentials." +description: "Reach your agent from GitHub App webhooks, with comment invocation, PR diff context, sandbox checkout, and Vercel Connect credentials." type: integration --- -The GitHub channel lets the agent work directly on a repository. Someone `@mentions` it in an issue, PR, or review comment, and the agent answers right there in the thread, with the PR diff already in context and the repo checked out into the sandbox. It takes GitHub App webhooks at `/eve/v1/github`, checks the signature, derives auth from whoever triggered the event, and replies on the native surface. Credentials can run through [Vercel Connect](../guides/auth-and-route-protection), which manages the GitHub App, the installation token, and inbound webhook verification, so there's no app private key or webhook secret for you to hold. See [Channels](./overview) for the contract this builds on. +The GitHub channel lets the agent work directly on a repository. Add its invocation token, such as `@my-agent`, to a new issue, PR, or review comment and the agent answers in that thread, with the PR diff already in context and the repo checked out into the sandbox. The token is an eve convention: GitHub may not autocomplete it or render it as a linked mention. The channel takes GitHub App webhooks at `/eve/v1/github`, checks the signature, derives auth from whoever triggered the event, and replies on the native surface. Credentials can run through [Vercel Connect](../guides/auth-and-route-protection), which manages the GitHub App, the installation token, and inbound webhook verification, so there's no app private key or webhook secret for you to hold. See [Channels](./overview) for the contract this builds on. ## Guided Connect setup @@ -16,7 +16,7 @@ eve add channel/github The flow checks that the Vercel CLI is authenticated, creates or links a Vercel project when needed, provisions an app-scoped GitHub Connect client, and replaces its default trigger with `/eve/v1/github`. It then installs `@vercel/connect` and writes `agent/channels/github.ts` with the connector UID. -Vercel Connect creates the GitHub App, receives and verifies its webhooks, and forwards them to the deployed agent. After deploying, open the GitHub App in the Connect dashboard and install it in the organization or account where you want to use it. Mention the generated app handle (for example, `@my-agent`) in an issue, pull request, or review comment to start a conversation. +Vercel Connect creates the GitHub App, receives and verifies its webhooks, and forwards them to the deployed agent. After deploying, open the GitHub App in the Connect dashboard and install it in the organization or account where you want to use it. Add the generated invocation token (for example, `@my-agent`) to a new issue, pull request, or review comment to start a conversation. GitHub may not autocomplete the token or render it as a linked mention. The generated channel uses Connect-managed credentials: @@ -60,7 +60,7 @@ GITHUB_APP_SLUG=... # supplies botName when it is not set in config `appId`/`privateKey`/`webhookSecret` also take a lazy resolver function if you'd rather fetch them on demand. -Point the GitHub App webhook URL at `https:///eve/v1/github`. For mention-driven turns, subscribe to `issue_comment` and `pull_request_review_comment`; add `issues`, `pull_request`, `check_suite`, `check_run`, or `workflow_run` if you wire up their opt-in hooks. After installing the App for the repository, a new comment that includes `@botName` starts a turn. GitHub may display the App as `botName[bot]`, but write the mention without the `[bot]` suffix. +Point the GitHub App webhook URL at `https:///eve/v1/github`. For comment-invoked turns, subscribe to `issue_comment` and `pull_request_review_comment`; add `issues`, `pull_request`, `check_suite`, `check_run`, or `workflow_run` if you wire up their opt-in hooks. After installing the App for the repository, a new comment that includes `@botName` starts a turn. This is a text invocation token, not a GitHub-native mention: GitHub may display the App as `botName[bot]`, but it may not autocomplete or link `@botName`. ## How the channel handles messages @@ -73,7 +73,7 @@ import { defaultGitHubAuth, githubChannel } from "eve/channels/github"; export default githubChannel({ botName: "my-agent", - // Replaces the @mention gate. ctx.conversation.kind is "issue", "pull_request", or "review_thread". + // Replaces the default invocation-token gate. ctx.conversation.kind is "issue", "pull_request", or "review_thread". onComment: (ctx, comment) => ({ auth: defaultGitHubAuth(ctx) }), // Opt in; no default dispatch on these events. onIssue: (ctx, issue) => (issue.action === "opened" ? { auth: defaultGitHubAuth(ctx) } : null), @@ -103,7 +103,7 @@ GitHub comments have no interactive button or card affordance. A human-in-the-lo ### Proactive sessions -Start a session without an inbound mention through `receive(github, { message, target, auth })` from a schedule `run` handler, or `args.receive(github, ...)` from another channel. The target requires `owner`, `repo`, and exactly one of `issueNumber` or `pullRequestNumber`. +Start a session without an inbound comment invocation through `receive(github, { message, target, auth })` from a schedule `run` handler, or `args.receive(github, ...)` from another channel. The target requires `owner`, `repo`, and exactly one of `issueNumber` or `pullRequestNumber`. ### Attachments diff --git a/packages/eve/src/setup/integrations/github/setup.ts b/packages/eve/src/setup/integrations/github/setup.ts index b2afedb4c..abf92979e 100644 --- a/packages/eve/src/setup/integrations/github/setup.ts +++ b/packages/eve/src/setup/integrations/github/setup.ts @@ -32,13 +32,13 @@ const GITHUB_EVENT_OPTIONS = [ id: "issue_comment", label: "New issue and pull request comments", value: "issue_comment", - hint: "Start a turn when a new timeline comment @mentions the app.", + hint: "Start a turn when a new timeline comment includes the app's invocation token.", }, { id: "pull_request_review_comment", label: "New inline pull request review comments", value: "pull_request_review_comment", - hint: "Start a turn when a new inline review comment @mentions the app.", + hint: "Start a turn when a new inline review comment includes the app's invocation token.", }, { id: "issues", @@ -175,7 +175,7 @@ export async function setupGitHub( const dashboardUrl = "https://vercel.com/d?to=/%5Bteam%5D/~/connect&title=Open+Vercel+Connect"; context.ui.nextSteps([ "Deploy the agent, then open the GitHub App in Vercel Connect and install it in the organization or account where you want to use it.", - `Mention @${connector.appSlug} in an issue, pull request, or review comment to start a conversation.`, + `Add @${connector.appSlug} to a new issue, pull request, or review comment to invoke the agent. GitHub may not autocomplete or render the token as a linked mention.`, ]); return { kind: "done", From 4ca355cdf80e1686586757252b8b9c68be4837aa Mon Sep 17 00:00:00 2001 From: owenkephart Date: Wed, 5 Aug 2026 11:15:23 -0500 Subject: [PATCH 9/9] update Signed-off-by: owenkephart --- .../setup/integrations/github/setup.test.ts | 5 +- .../src/setup/integrations/github/setup.ts | 53 +++---------------- 2 files changed, 10 insertions(+), 48 deletions(-) diff --git a/packages/eve/src/setup/integrations/github/setup.test.ts b/packages/eve/src/setup/integrations/github/setup.test.ts index 8b4d2c679..c36b1e9b3 100644 --- a/packages/eve/src/setup/integrations/github/setup.test.ts +++ b/packages/eve/src/setup/integrations/github/setup.test.ts @@ -31,7 +31,7 @@ describe("GitHub setup", () => { it("provisions Connect, routes the selected webhooks, and scaffolds matching handlers", async () => { const fake = createFakePrompter(); const effects = deps(); - const selectedEvents = ["issue_comment", "issues", "workflow_run"]; + const selectedEvents = ["issue_comment", "issues", "pull_request"]; await expect( setupGitHub( @@ -55,8 +55,7 @@ describe("GitHub setup", () => { const scaffold = vi.mocked(effects.writeTextFile).mock.calls[0]?.[1] ?? ""; expect(scaffold).toContain('botName: "agent"'); expect(scaffold).toContain("onIssue(ctx, issue)"); - expect(scaffold).toContain("onWorkflowRun(ctx, workflowRun)"); - expect(scaffold).not.toContain("onPullRequest(ctx, pullRequest)"); + expect(scaffold).toContain("onPullRequest(ctx, pullRequest)"); }); it("recommends comment events that the default scaffold handles", async () => { diff --git a/packages/eve/src/setup/integrations/github/setup.ts b/packages/eve/src/setup/integrations/github/setup.ts index abf92979e..6badc0d0a 100644 --- a/packages/eve/src/setup/integrations/github/setup.ts +++ b/packages/eve/src/setup/integrations/github/setup.ts @@ -30,45 +30,27 @@ const defaultDeps: GitHubSetupDeps = { const GITHUB_EVENT_OPTIONS = [ { id: "issue_comment", - label: "New issue and pull request comments", + label: "New issue and PR comments", value: "issue_comment", - hint: "Start a turn when a new timeline comment includes the app's invocation token.", + hint: "Reply when a new timeline comment includes `@`.", }, { id: "pull_request_review_comment", - label: "New inline pull request review comments", + label: "New inline PR review comments", value: "pull_request_review_comment", - hint: "Start a turn when a new inline review comment includes the app's invocation token.", + hint: "Reply when a new inline review comment includes `@`.", }, { id: "issues", label: "New issues", value: "issues", - hint: "Start a turn when an issue is opened. Other issue changes are ignored.", + hint: "Add comments to new issues.", }, { id: "pull_request", - label: "New pull requests", + label: "New PRs", value: "pull_request", - hint: "Start a turn when a pull request is opened. Other pull request changes are ignored.", - }, - { - id: "check_suite", - label: "Completed check suites", - value: "check_suite", - hint: "Start a turn when a check suite completes for a pull request, including successful and failed suites.", - }, - { - id: "check_run", - label: "Completed check runs", - value: "check_run", - hint: "Start a turn when a check run completes for a pull request, including successful and failed runs.", - }, - { - id: "workflow_run", - label: "Completed GitHub Actions workflow runs", - value: "workflow_run", - hint: "Start a turn when a GitHub Actions workflow run completes for a pull request, including successful and failed runs.", + hint: "Add comments to new pull requests.", }, ] as const; @@ -81,8 +63,7 @@ const DEFAULT_GITHUB_EVENTS: readonly GitHubWebhookEvent[] = [ const githubEventsQuestion: MultiSelectQuestion = { key: "github-events", - message: - "Which GitHub webhook events should this app subscribe to? The generated channel starts turns only for the conditions described below.", + message: "What should this GitHub App respond to?", options: GITHUB_EVENT_OPTIONS, recommended: DEFAULT_GITHUB_EVENTS, requireSelection: true, @@ -104,24 +85,6 @@ function connectTemplate( ? ` onPullRequest(ctx, pullRequest) { if (pullRequest.action !== "opened") return null; return { auth: defaultGitHubAuth(ctx) }; - },` - : undefined, - events.includes("check_suite") - ? ` onCheckSuite(ctx, checkSuite) { - if (checkSuite.action !== "completed") return null; - return { auth: defaultGitHubAuth(ctx) }; - },` - : undefined, - events.includes("check_run") - ? ` onCheckRun(ctx, checkRun) { - if (checkRun.action !== "completed") return null; - return { auth: defaultGitHubAuth(ctx) }; - },` - : undefined, - events.includes("workflow_run") - ? ` onWorkflowRun(ctx, workflowRun) { - if (workflowRun.action !== "completed") return null; - return { auth: defaultGitHubAuth(ctx) }; },` : undefined, ].filter((handler): handler is string => handler !== undefined);