Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 62 additions & 2 deletions base-action/src/run-claude-sdk.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,73 @@
import * as core from "@actions/core";
import { readFile, writeFile } from "fs/promises";
import { readFile, writeFile, access } from "fs/promises";
import { dirname, join } from "path";
import { query } from "@anthropic-ai/claude-agent-sdk";
import type {
SDKMessage,
SDKResultMessage,
SDKUserMessage,
} from "@anthropic-ai/claude-agent-sdk";
import type { ParsedSdkOptions } from "./parse-sdk-options";

const EXECUTION_FILE = `${process.env.RUNNER_TEMP}/claude-execution-output.json`;

/**
* Check if a file exists
*/
async function fileExists(path: string): Promise<boolean> {
try {
await access(path);
return true;
} catch {
return false;
}
}

/**
* Creates a prompt configuration for the SDK.
* If a user request file exists alongside the prompt file, returns a multi-block
* SDKUserMessage that enables slash command processing in the CLI.
* Otherwise, returns the prompt as a simple string.
*/
async function createPromptConfig(
promptPath: string,
): Promise<string | AsyncIterable<SDKUserMessage>> {
const promptContent = await readFile(promptPath, "utf-8");

// Check for user request file in the same directory
const userRequestPath = join(dirname(promptPath), "claude-user-request.txt");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Quality: Magic string should be a constant

The filename "claude-user-request.txt" is hardcoded here and in src/create-prompt/index.ts:945. Define a shared constant:

export const USER_REQUEST_FILENAME = "claude-user-request.txt";

const hasUserRequest = await fileExists(userRequestPath);

if (!hasUserRequest) {
// No user request file - use simple string prompt
return promptContent;
}

// User request file exists - create multi-block message
const userRequest = await readFile(userRequestPath, "utf-8");
console.log("Using multi-block message with user request:", userRequest);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security: Sensitive data logging

User requests may contain sensitive information. Consider conditional logging based on showFullOutput to match the pattern used elsewhere in the file (line 125-129).


// Create an async generator that yields a single multi-block message
// The context/instructions go first, then the user's actual request last
// This allows the CLI to detect and process slash commands in the user request
async function* createMultiBlockMessage(): AsyncGenerator<SDKUserMessage> {
yield {
type: "user",
session_id: "",
message: {
role: "user",
content: [
{ type: "text", text: promptContent }, // Instructions + GitHub context
{ type: "text", text: userRequest }, // User's request (may be a slash command)
],
},
parent_tool_use_id: null,
};
}

return createMultiBlockMessage();
}

/**
* Sanitizes SDK output to match CLI sanitization behavior
*/
Expand Down Expand Up @@ -63,7 +122,8 @@ export async function runClaudeWithSdk(
promptPath: string,
{ sdkOptions, showFullOutput, hasJsonSchema }: ParsedSdkOptions,
): Promise<void> {
const prompt = await readFile(promptPath, "utf-8");
// Create prompt configuration - may be a string or multi-block message
const prompt = await createPromptConfig(promptPath);

if (!showFullOutput) {
console.log(
Expand Down
55 changes: 55 additions & 0 deletions src/create-prompt/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type { ParsedGitHubContext } from "../github/context";
import type { CommonFields, PreparedContext, EventData } from "./types";
import { GITHUB_SERVER_URL } from "../github/api/config";
import type { Mode, ModeContext } from "../modes/types";
import { extractUserRequest } from "../utils/extract-user-request";
export type { CommonFields, PreparedContext } from "./types";

// Tag mode defaults - these tools are needed for tag mode to function
Expand Down Expand Up @@ -847,6 +848,44 @@ f. If you are unable to complete certain steps, such as running a linter or test
return promptContent;
}

/**
* Extracts the user's request from the prepared context and GitHub data.
* This is used to send the user's actual command/request as a separate
* content block, enabling slash command processing in the CLI.
*/
function extractUserRequestFromContext(
context: PreparedContext,
githubData: FetchDataResult,
): string | null {
const { eventData, triggerPhrase } = context;

// For comment events, extract from comment body
if (
"commentBody" in eventData &&
eventData.commentBody &&
(eventData.eventName === "issue_comment" ||
eventData.eventName === "pull_request_review_comment" ||
eventData.eventName === "pull_request_review")
) {
return extractUserRequest(eventData.commentBody, triggerPhrase);
}

// For issue/PR events triggered by body content, extract from the body
if (githubData.contextData?.body) {
const request = extractUserRequest(
githubData.contextData.body,
triggerPhrase,
);
if (request) {
return request;
}
}

// For assigned/labeled events without explicit trigger in body,
// return null to indicate the full context should be used
return null;
}

export async function createPrompt(
mode: Mode,
modeContext: ModeContext,
Expand Down Expand Up @@ -895,6 +934,22 @@ export async function createPrompt(
promptContent,
);

// Extract and write the user request separately for SDK multi-block messaging
// This allows the CLI to process slash commands (e.g., "@claude /review-pr")
const userRequest = extractUserRequestFromContext(
preparedContext,
githubData,
);
if (userRequest) {
await writeFile(
`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts/claude-user-request.txt`,
userRequest,
);
console.log("===== USER REQUEST =====");
console.log(userRequest);
console.log("========================");
}

// Set allowed tools
const hasActionsReadPermission = false;

Expand Down
89 changes: 89 additions & 0 deletions src/utils/extract-user-request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* Extracts the user's request from a trigger comment.
*
* Given a comment like "@claude /review-pr please check the auth module",
* this extracts "/review-pr please check the auth module".
*
* @param commentBody - The full comment body containing the trigger phrase
* @param triggerPhrase - The trigger phrase (e.g., "@claude")
* @returns The user's request (text after the trigger phrase), or null if not found
*/
export function extractUserRequest(
commentBody: string | undefined,
triggerPhrase: string,
): string | null {
if (!commentBody) {
return null;
}

// Escape special regex characters in the trigger phrase
const escapedTrigger = triggerPhrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");

// Match the trigger phrase followed by optional whitespace and capture the rest
// The trigger phrase can appear anywhere in the comment
const regex = new RegExp(`${escapedTrigger}\\s*(.*)`, "is");
const match = commentBody.match(regex);

if (match && match[1]) {
// Trim and return the captured text after the trigger phrase
const request = match[1].trim();
return request || null;
}

return null;
}

/**
* Extracts the user's request from various GitHub event types.
*
* For comment events: extracts from the comment body
* For issue/PR events: extracts from the body or title
*
* @param eventData - The parsed event data containing comment/body info
* @param triggerPhrase - The trigger phrase (e.g., "@claude")
* @returns The user's request, or a default message if not found
*/
export function extractUserRequestFromEvent(
eventData: {
eventName: string;
commentBody?: string;
issueBody?: string;
prBody?: string;
},
triggerPhrase: string,
): string {
// For comment events, extract from comment body
if (
eventData.eventName === "issue_comment" ||
eventData.eventName === "pull_request_review_comment" ||
eventData.eventName === "pull_request_review"
) {
const request = extractUserRequest(eventData.commentBody, triggerPhrase);
if (request) {
return request;
}
}

// For issue events, try extracting from issue body
if (eventData.eventName === "issues" && eventData.issueBody) {
const request = extractUserRequest(eventData.issueBody, triggerPhrase);
if (request) {
return request;
}
}

// For PR events, try extracting from PR body
if (
(eventData.eventName === "pull_request" ||
eventData.eventName === "pull_request_target") &&
eventData.prBody
) {
const request = extractUserRequest(eventData.prBody, triggerPhrase);
if (request) {
return request;
}
}

// Default: return a generic request to analyze the context
return "Please analyze the context and help with this request.";
}
Loading
Loading