diff --git a/README.md b/README.md index 84c7f186..81496ad8 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ A powerful, extensible AI coding agent CLI with multi-provider support, built-in - **Interactive TUI**: Rich terminal interface powered by Bubble Tea with streaming, syntax highlighting, and custom rendering - **Session Management**: Tree-based conversation history with branching support - **Non-Interactive Mode**: Script-friendly positional args with JSON output +- **GitHub Integration**: Scaffold a GitHub Actions workflow with `kit github install` to run Kit as a collaborator/reviewer on `/kit` comments - **ACP Server**: Run Kit as an [Agent Client Protocol](https://agentclientprotocol.com) agent over stdio - **Go SDK**: Embed Kit in your own applications with full agent lifecycle events (30+ event types) and behavior-modifying hooks @@ -260,6 +261,12 @@ kit install --uninstall # Remove an installed package # Skills kit skill # Install the Kit extensions skill via skills.sh +# GitHub integration +kit github install # Scaffold .github/workflows/kit.yml (run Kit on '/kit' comments) +kit github install --model anthropic/claude-sonnet-4-5-20250929 +kit github install --force # Overwrite an existing workflow file +kit github install --no-secret # Skip the offer to set the provider secret via the gh CLI + # ACP server kit acp # Start as ACP agent (stdio JSON-RPC) kit acp --debug # With debug logging to stderr @@ -478,6 +485,41 @@ Placeholders inside fenced code blocks (```) and inline code spans are ignored. Disable templates with `--no-prompt-templates` or load a specific template with `--prompt-template `. +## GitHub Integration + +Kit can run as an automated collaborator/reviewer inside GitHub Actions. The +`kit github install` command scaffolds a workflow that triggers when someone +comments `/kit ...` on an issue or pull request review, runs the agent +non-interactively in the runner, and lets it respond. + +```bash +kit github install +``` + +This writes `.github/workflows/kit.yml`. By default the command prompts for the +model (pre-filled with a sensible default); pass `--model` to skip the prompt. +If the [`gh` CLI](https://cli.github.com/) is detected on your `PATH` and the +provider API key is present in your environment, you'll be offered the option to +store it as a repository secret automatically. + +The generated workflow: + +- Triggers only on `issue_comment` and `pull_request_review_comment` (`types: [created]`). +- Runs only when the comment begins with the `/kit` command token. +- Restricts triggers to repository owners, members, and collaborators (via `author_association`). +- Uses least-privilege `permissions` and `persist-credentials: false`. +- Authenticates git/PR operations with the built-in `secrets.GITHUB_TOKEN` and + the provider via a repository secret (e.g. `ANTHROPIC_API_KEY`). + +After committing the workflow and setting the provider secret, comment +`/kit ` on any issue or pull request to trigger Kit. + +| Flag | Description | +| --- | --- | +| `--model` | Provider/model to write into the workflow | +| `--force` | Overwrite an existing workflow file | +| `--no-secret` | Skip the offer to set the provider secret via the `gh` CLI | + ## Session Management Kit uses a tree-based session model that supports branching and forking conversations. diff --git a/cmd/github.go b/cmd/github.go new file mode 100644 index 00000000..fb506cf5 --- /dev/null +++ b/cmd/github.go @@ -0,0 +1,255 @@ +package cmd + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "charm.land/huh/v2" + "github.com/charmbracelet/log" + kit "github.com/mark3labs/kit/pkg/kit" + "github.com/spf13/cobra" +) + +// defaultGitHubModel is the model written into the generated workflow when the +// user does not specify one and runs non-interactively. +const defaultGitHubModel = "anthropic/claude-sonnet-4-5-20250929" + +// githubWorkflowPath is the repository-relative location of the generated +// GitHub Actions workflow that wires Kit into a repository as a collaborator. +const githubWorkflowPath = ".github/workflows/kit.yml" + +var ( + githubInstallModel string + githubInstallForce bool + githubInstallNoSecret bool +) + +// githubCmd is the parent command for GitHub integration subcommands. It groups +// the turnkey setup tooling that wires Kit into a repository as an automated +// collaborator/reviewer driven by GitHub Actions. +var githubCmd = &cobra.Command{ + Use: "github", + Short: "Set up Kit as a GitHub collaborator/reviewer", + Long: `Set up Kit as an automated collaborator/reviewer in a GitHub repository. + +Kit runs inside a GitHub Actions runner, reads the relevant context (an issue +thread or pull request), runs the agent non-interactively, and responds by +posting comments and opening pull requests. + +Use 'kit github install' to scaffold the GitHub Actions workflow.`, +} + +// githubInstallCmd scaffolds the GitHub Actions workflow that runs Kit on +// '/kit' comment triggers. It writes .github/workflows/kit.yml and, when the +// 'gh' CLI is available, offers to set the provider API key as a repository +// secret. +var githubInstallCmd = &cobra.Command{ + Use: "install", + Short: "Scaffold the GitHub Actions workflow that runs Kit", + Long: `Scaffold the GitHub Actions workflow that runs Kit as a collaborator. + +This writes .github/workflows/kit.yml configured to trigger when someone +comments '/kit ...' on an issue or pull request review. The workflow runs Kit +inside an ephemeral Actions runner with least-privilege permissions and +'persist-credentials: false', mirroring established security practice. + +If the GitHub CLI ('gh') is detected on your PATH, you will be offered the +option to store your provider API key as a repository secret automatically. + +Flags: + --model Provider/model to write into the workflow (e.g. anthropic/claude-sonnet-4-5) + --force Overwrite an existing workflow file + --no-secret Skip the offer to set the provider secret via the gh CLI + +Examples: + kit github install + kit github install --model anthropic/claude-sonnet-4-5-20250929 + kit github install --force --no-secret`, + Args: cobra.NoArgs, + RunE: runGitHubInstall, +} + +func init() { + githubInstallCmd.Flags().StringVarP(&githubInstallModel, "model", "m", "", "provider/model to write into the workflow") + githubInstallCmd.Flags().BoolVar(&githubInstallForce, "force", false, "overwrite an existing workflow file") + githubInstallCmd.Flags().BoolVar(&githubInstallNoSecret, "no-secret", false, "skip setting the provider secret via the gh CLI") + + githubCmd.AddCommand(githubInstallCmd) + rootCmd.AddCommand(githubCmd) +} + +func runGitHubInstall(cmd *cobra.Command, _ []string) error { + model, err := resolveGitHubModel() + if err != nil { + return err + } + + provider, _, err := kit.ParseModelString(model) + if err != nil { + return fmt.Errorf("invalid model %q: %w", model, err) + } + + secretName := providerSecretEnvVar(provider) + + if err := writeGitHubWorkflow(model, secretName, githubInstallForce); err != nil { + return err + } + fmt.Printf("✅ Wrote %s\n", githubWorkflowPath) + + maybeSetProviderSecret(cmd.Context(), secretName) + + printGitHubInstallNextSteps(secretName) + log.Info("github workflow scaffolded", "model", model, "secret", secretName) + return nil +} + +// resolveGitHubModel determines the model to embed in the workflow. The +// --model flag takes precedence; otherwise an interactive prompt is shown +// (pre-filled with the default), and non-interactive runs use the default. +func resolveGitHubModel() (string, error) { + if githubInstallModel != "" { + return strings.TrimSpace(githubInstallModel), nil + } + + if !isInteractive() { + return defaultGitHubModel, nil + } + + model := defaultGitHubModel + err := huh.NewInput(). + Title("Model"). + Description("Provider/model Kit should use in CI (e.g. anthropic/claude-sonnet-4-5)"). + Value(&model). + Run() + if err != nil { + return "", fmt.Errorf("model selection cancelled: %w", err) + } + + model = strings.TrimSpace(model) + if model == "" { + return "", fmt.Errorf("model cannot be empty") + } + return model, nil +} + +// providerSecretEnvVar returns the environment variable / repository secret +// name that holds the API key for the given provider. It consults the model +// registry and falls back to "_API_KEY" for unknown providers. +func providerSecretEnvVar(provider string) string { + if info := kit.GetProviderInfo(provider); info != nil && len(info.Env) > 0 { + return info.Env[0] + } + sanitized := strings.ToUpper(strings.NewReplacer("-", "_", ".", "_").Replace(provider)) + return sanitized + "_API_KEY" +} + +// renderGitHubWorkflow builds the workflow YAML for the given model and +// provider secret name. +func renderGitHubWorkflow(model, secretName string) string { + return fmt.Sprintf(`name: kit +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] +jobs: + kit: + if: | + (github.event.comment.author_association == 'OWNER' || + github.event.comment.author_association == 'MEMBER' || + github.event.comment.author_association == 'COLLABORATOR') && + (startsWith(github.event.comment.body, '/kit ') || + github.event.comment.body == '/kit') + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + issues: write + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: mark3labs/kit-action@v1 + with: + model: %s + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + %s: ${{ secrets.%s }} +`, model, secretName, secretName) +} + +// writeGitHubWorkflow writes the generated workflow to githubWorkflowPath, +// creating parent directories as needed. It refuses to overwrite an existing +// file unless force is true. +func writeGitHubWorkflow(model, secretName string, force bool) error { + if _, err := os.Stat(githubWorkflowPath); err == nil && !force { + return fmt.Errorf("%s already exists; re-run with --force to overwrite", githubWorkflowPath) + } else if err != nil && !os.IsNotExist(err) { + return fmt.Errorf("checking %s: %w", githubWorkflowPath, err) + } + + if err := os.MkdirAll(filepath.Dir(githubWorkflowPath), 0o755); err != nil { + return fmt.Errorf("creating %s: %w", filepath.Dir(githubWorkflowPath), err) + } + + content := renderGitHubWorkflow(model, secretName) + if err := os.WriteFile(githubWorkflowPath, []byte(content), 0o644); err != nil { + return fmt.Errorf("writing %s: %w", githubWorkflowPath, err) + } + return nil +} + +// maybeSetProviderSecret offers to set the provider API key as a repository +// secret via the gh CLI when it is available, interactive, the secret value is +// present in the environment, and the user did not pass --no-secret. +func maybeSetProviderSecret(ctx context.Context, secretName string) { + if githubInstallNoSecret || !isInteractive() { + return + } + + if _, err := exec.LookPath("gh"); err != nil { + return + } + + value := os.Getenv(secretName) + if value == "" { + fmt.Printf("ℹ️ %s is not set in your environment; set the repository secret manually with:\n", secretName) + fmt.Printf(" gh secret set %s\n", secretName) + return + } + + var confirm bool + if err := huh.NewConfirm(). + Title(fmt.Sprintf("Set the %s repository secret via gh?", secretName)). + Description("Uses the value from your current environment."). + Value(&confirm). + Run(); err != nil || !confirm { + return + } + + // Feed the secret value via stdin rather than a command-line argument so + // the API key never appears in the process argument list. + cmd := exec.CommandContext(ctx, "gh", "secret", "set", secretName) + cmd.Stdin = strings.NewReader(value) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + fmt.Printf("⚠️ Failed to set secret via gh: %v\n", err) + fmt.Printf(" Set it manually with: gh secret set %s\n", secretName) + return + } + fmt.Printf("✅ Set repository secret %s\n", secretName) +} + +// printGitHubInstallNextSteps prints the manual follow-up actions a user must +// take after the workflow is scaffolded. +func printGitHubInstallNextSteps(secretName string) { + fmt.Println("\nNext steps:") + fmt.Printf(" 1. Commit the workflow: git add %s && git commit -m \"ci: add kit workflow\"\n", githubWorkflowPath) + fmt.Printf(" 2. Set the %s repository secret (Settings → Secrets → Actions), if not already set.\n", secretName) + fmt.Println(" 3. Comment '/kit ' on an issue or pull request to trigger Kit.") +} diff --git a/cmd/github_test.go b/cmd/github_test.go new file mode 100644 index 00000000..b745de14 --- /dev/null +++ b/cmd/github_test.go @@ -0,0 +1,102 @@ +package cmd + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestProviderSecretEnvVar(t *testing.T) { + tests := []struct { + provider string + want string + }{ + {"anthropic", "ANTHROPIC_API_KEY"}, + {"openai", "OPENAI_API_KEY"}, + // Unknown provider falls back to "_API_KEY" with sanitization. + {"my-custom.provider", "MY_CUSTOM_PROVIDER_API_KEY"}, + } + + for _, tt := range tests { + t.Run(tt.provider, func(t *testing.T) { + got := providerSecretEnvVar(tt.provider) + if got != tt.want { + t.Errorf("providerSecretEnvVar(%q) = %q, want %q", tt.provider, got, tt.want) + } + }) + } +} + +func TestRenderGitHubWorkflow(t *testing.T) { + out := renderGitHubWorkflow("anthropic/claude-sonnet-4-5-20250929", "ANTHROPIC_API_KEY") + + wantSubstrings := []string{ + "name: kit", + "issue_comment:", + "pull_request_review_comment:", + "startsWith(github.event.comment.body, '/kit ')", + "github.event.comment.body == '/kit'", + "github.event.comment.author_association == 'OWNER'", + "github.event.comment.author_association == 'COLLABORATOR'", + "persist-credentials: false", + "uses: mark3labs/kit-action@v1", + "model: anthropic/claude-sonnet-4-5-20250929", + "GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}", + "ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}", + "contents: write", + "pull-requests: write", + "issues: write", + } + for _, want := range wantSubstrings { + if !strings.Contains(out, want) { + t.Errorf("rendered workflow missing %q\n---\n%s", want, out) + } + } +} + +func TestWriteGitHubWorkflow(t *testing.T) { + dir := t.TempDir() + cwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = os.Chdir(cwd) }) + if err := os.Chdir(dir); err != nil { + t.Fatal(err) + } + + // First write succeeds and creates nested directories. + if err := writeGitHubWorkflow("anthropic/claude-sonnet-4-5", "ANTHROPIC_API_KEY", false); err != nil { + t.Fatalf("writeGitHubWorkflow: %v", err) + } + data, err := os.ReadFile(githubWorkflowPath) + if err != nil { + t.Fatalf("reading workflow: %v", err) + } + if !strings.Contains(string(data), "model: anthropic/claude-sonnet-4-5") { + t.Errorf("workflow missing model line:\n%s", data) + } + + // Second write without force must refuse to clobber. + if err := writeGitHubWorkflow("anthropic/claude-sonnet-4-5", "ANTHROPIC_API_KEY", false); err == nil { + t.Error("expected error when overwriting without --force, got nil") + } + + // With force it overwrites. + if err := writeGitHubWorkflow("openai/gpt-5", "OPENAI_API_KEY", true); err != nil { + t.Fatalf("writeGitHubWorkflow with force: %v", err) + } + data, err = os.ReadFile(githubWorkflowPath) + if err != nil { + t.Fatalf("reading workflow: %v", err) + } + if !strings.Contains(string(data), "OPENAI_API_KEY") { + t.Errorf("forced overwrite did not update content:\n%s", data) + } + + // Sanity: the file lives at the expected nested path. + if _, err := os.Stat(filepath.Join(dir, githubWorkflowPath)); err != nil { + t.Errorf("workflow not at expected path: %v", err) + } +} diff --git a/www/pages/cli/commands.md b/www/pages/cli/commands.md index b46263bd..a48fc869 100644 --- a/www/pages/cli/commands.md +++ b/www/pages/cli/commands.md @@ -76,6 +76,35 @@ kit --no-skills "prompt" Skills are auto-discovered from `~/.config/kit/skills/`, `.kit/skills/`, and `.agents/skills/` by default. Use `--skills-dir` to override the project-local search root, or `--skill` to load files explicitly (which disables auto-discovery). `--no-skills` suppresses all skill loading regardless of other flags. +## GitHub integration + +Scaffold a GitHub Actions workflow that runs Kit as an automated collaborator/reviewer. The workflow triggers when someone comments `/kit ...` on an issue or pull request review, runs the agent non-interactively in the runner, and lets it respond. + +```bash +kit github install # Scaffold .github/workflows/kit.yml +kit github install --model anthropic/claude-sonnet-4-5-20250929 # Skip the model prompt +kit github install --force # Overwrite an existing workflow file +kit github install --no-secret # Skip the offer to set the provider secret via the gh CLI +``` + +By default the command prompts for the model (pre-filled with a sensible default). If the [`gh` CLI](https://cli.github.com/) is detected on your `PATH` and the provider API key is present in your environment, you'll be offered the option to store it as a repository secret automatically. + +The generated workflow: + +- Triggers only on `issue_comment` and `pull_request_review_comment` (`types: [created]`). +- Runs only when the comment begins with the `/kit` command token. +- Restricts triggers to repository owners, members, and collaborators (via `author_association`). +- Uses least-privilege `permissions` and `persist-credentials: false`. +- Authenticates git/PR operations with the built-in `secrets.GITHUB_TOKEN` and the provider via a repository secret (e.g. `ANTHROPIC_API_KEY`). + +After committing the workflow and setting the provider secret, comment `/kit ` on any issue or pull request to trigger Kit. + +| Flag | Description | +|------|-------------| +| `--model` | Provider/model to write into the workflow | +| `--force` | Overwrite an existing workflow file | +| `--no-secret` | Skip the offer to set the provider secret via the `gh` CLI | + ## Interactive slash commands These commands are available inside the Kit TUI during an interactive session: diff --git a/www/pages/index.md b/www/pages/index.md index ace41fce..0badaa97 100644 --- a/www/pages/index.md +++ b/www/pages/index.md @@ -20,6 +20,7 @@ A powerful, extensible AI coding agent CLI with multi-provider support, built-in - **Interactive TUI** — Rich terminal interface powered by Bubble Tea with streaming, syntax highlighting, and custom rendering - **Session Management** — Tree-based conversation history with branching support - **Non-Interactive Mode** — Script-friendly positional args with JSON output +- **GitHub Integration** — Scaffold a GitHub Actions workflow with `kit github install` to run Kit as a collaborator/reviewer on `/kit` comments - **ACP Server** — Run Kit as an [Agent Client Protocol](https://agentclientprotocol.com) agent over stdio - **Go SDK** — Embed Kit in your own applications