diff --git a/action.yml b/action.yml index 1a0d1a0..005f0e7 100644 --- a/action.yml +++ b/action.yml @@ -42,7 +42,10 @@ inputs: # Auth configuration anthropic_api_key: - description: "Anthropic API key (required for direct API, not needed for Bedrock/Vertex)" + description: "Anthropic API key (required for direct API, not needed for Bedrock/Vertex). Set to 'use-oauth' when using claude_credentials" + required: false + claude_credentials: + description: "Claude OAuth credentials JSON for Claude AI Max subscription authentication" required: false gitea_token: description: "Gitea token with repo and pull request permissions (defaults to GITHUB_TOKEN)" @@ -96,6 +99,8 @@ runs: GITHUB_TOKEN: ${{ github.token }} GITHUB_RUN_ID: ${{ github.run_id }} GITEA_API_URL: ${{ env.GITHUB_SERVER_URL }} + ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }} + CLAUDE_CREDENTIALS: ${{ inputs.claude_credentials }} - name: Run Claude Code id: claude-code @@ -123,6 +128,7 @@ runs: USE_BEDROCK: ${{ inputs.use_bedrock }} USE_VERTEX: ${{ inputs.use_vertex }} ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }} + CLAUDE_CREDENTIALS: ${{ inputs.claude_credentials }} # GitHub token for repository access GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }} diff --git a/src/claude/oauth-setup.ts b/src/claude/oauth-setup.ts new file mode 100644 index 0000000..f9d3a41 --- /dev/null +++ b/src/claude/oauth-setup.ts @@ -0,0 +1,59 @@ +import { mkdir, writeFile } from "fs/promises"; +import { join } from "path"; +import { homedir } from "os"; + +interface OAuthCredentials { + accessToken: string; + refreshToken: string; + expiresAt: string; +} + +interface ClaudeCredentialsInput { + claudeAiOauth: { + accessToken: string; + refreshToken: string; + expiresAt: number; + scopes: string[]; + }; +} + +export async function setupOAuthCredentials(credentialsJson: string) { + try { + // Parse the credentials JSON + const parsedCredentials: ClaudeCredentialsInput = JSON.parse(credentialsJson); + + if (!parsedCredentials.claudeAiOauth) { + throw new Error("Invalid credentials format: missing claudeAiOauth"); + } + + const { accessToken, refreshToken, expiresAt } = parsedCredentials.claudeAiOauth; + + if (!accessToken || !refreshToken || !expiresAt) { + throw new Error("Invalid credentials format: missing required OAuth fields"); + } + + const claudeDir = join(homedir(), ".claude"); + const credentialsPath = join(claudeDir, ".credentials.json"); + + // Create the .claude directory if it doesn't exist + await mkdir(claudeDir, { recursive: true }); + + // Create the credentials JSON structure + const credentialsData = { + claudeAiOauth: { + accessToken, + refreshToken, + expiresAt, + scopes: ["user:inference", "user:profile"], + }, + }; + + // Write the credentials file + await writeFile(credentialsPath, JSON.stringify(credentialsData, null, 2)); + + console.log(`OAuth credentials written to ${credentialsPath}`); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to setup OAuth credentials: ${errorMessage}`); + } +} \ No newline at end of file diff --git a/src/entrypoints/prepare.ts b/src/entrypoints/prepare.ts index 2f3ec12..5b0d497 100644 --- a/src/entrypoints/prepare.ts +++ b/src/entrypoints/prepare.ts @@ -18,17 +18,27 @@ import { createPrompt } from "../create-prompt"; import { createClient } from "../github/api/client"; import { fetchGitHubData } from "../github/data/fetcher"; import { parseGitHubContext } from "../github/context"; +import { setupOAuthCredentials } from "../claude/oauth-setup"; async function run() { try { - // Step 1: Setup GitHub token + // Step 1: Setup OAuth credentials if provided + const claudeCredentials = process.env.CLAUDE_CREDENTIALS; + const anthropicApiKey = process.env.ANTHROPIC_API_KEY; + + if (claudeCredentials && anthropicApiKey === "use-oauth") { + await setupOAuthCredentials(claudeCredentials); + console.log("OAuth credentials configured for Claude AI Max subscription"); + } + + // Step 2: Setup GitHub token const githubToken = await setupGitHubToken(); const client = createClient(githubToken); - // Step 2: Parse GitHub context (once for all operations) + // Step 3: Parse GitHub context (once for all operations) const context = parseGitHubContext(); - // Step 3: Check write permissions + // Step 4: Check write permissions const hasWritePermissions = await checkWritePermissions( client.api, context, @@ -39,7 +49,7 @@ async function run() { ); } - // Step 4: Check trigger conditions + // Step 5: Check trigger conditions const containsTrigger = await checkTriggerAction(context); // Set outputs that are always needed @@ -51,14 +61,14 @@ async function run() { return; } - // Step 5: Check if actor is human + // Step 6: Check if actor is human await checkHumanActor(client.api, context); - // Step 6: Create initial tracking comment + // Step 7: Create initial tracking comment const commentId = await createInitialComment(client.api, context); core.setOutput("claude_comment_id", commentId.toString()); - // Step 7: Fetch GitHub data (once for both branch setup and prompt creation) + // Step 8: Fetch GitHub data (once for both branch setup and prompt creation) const githubData = await fetchGitHubData({ client: client, repository: `${context.repository.owner}/${context.repository.repo}`, @@ -66,14 +76,14 @@ async function run() { isPR: context.isPR, }); - // Step 8: Setup branch + // Step 9: Setup branch const branchInfo = await setupBranch(client, githubData, context); core.setOutput("BASE_BRANCH", branchInfo.baseBranch); if (branchInfo.claudeBranch) { core.setOutput("CLAUDE_BRANCH", branchInfo.claudeBranch); } - // Step 9: Update initial comment with branch link (only if a claude branch was created) + // Step 10: Update initial comment with branch link (only if a claude branch was created) if (branchInfo.claudeBranch) { await updateTrackingComment( client, @@ -83,7 +93,7 @@ async function run() { ); } - // Step 10: Create prompt file + // Step 11: Create prompt file await createPrompt( commentId, branchInfo.baseBranch, @@ -92,7 +102,7 @@ async function run() { context, ); - // Step 11: Get MCP configuration + // Step 12: Get MCP configuration const mcpConfig = await prepareMcpConfig( githubToken, context.repository.owner,