mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 06:54:13 +08:00
feat: implement Claude Code GitHub Action v1.0 with auto-detection and slash commands
Major features: - Mode auto-detection based on GitHub event type - Unified prompt field replacing override_prompt and direct_prompt - Slash command system with pre-built commands - Full backward compatibility with v0.x Key changes: - Add mode detector for automatic mode selection - Implement slash command loader with YAML frontmatter support - Update action.yml with new prompt input - Create pre-built slash commands for common tasks - Update all tests for v1.0 compatibility Breaking changes (with compatibility): - Mode input now optional (auto-detected) - override_prompt deprecated (use prompt) - direct_prompt deprecated (use prompt)
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import * as core from "@actions/core";
|
||||
import { writeFile, mkdir } from "fs/promises";
|
||||
import type { FetchDataResult } from "../github/data/fetcher";
|
||||
import { resolveSlashCommand } from "../slash-commands/loader";
|
||||
import {
|
||||
formatContext,
|
||||
formatBody,
|
||||
@@ -118,6 +119,7 @@ export function prepareContext(
|
||||
const customInstructions = context.inputs.customInstructions;
|
||||
const allowedTools = context.inputs.allowedTools;
|
||||
const disallowedTools = context.inputs.disallowedTools;
|
||||
const prompt = context.inputs.prompt; // v1.0: Unified prompt field
|
||||
const directPrompt = context.inputs.directPrompt;
|
||||
const overridePrompt = context.inputs.overridePrompt;
|
||||
const isPR = context.isPR;
|
||||
@@ -157,6 +159,7 @@ export function prepareContext(
|
||||
...(disallowedTools.length > 0 && {
|
||||
disallowedTools: disallowedTools.join(","),
|
||||
}),
|
||||
...(prompt && { prompt }),
|
||||
...(directPrompt && { directPrompt }),
|
||||
...(overridePrompt && { overridePrompt }),
|
||||
...(claudeBranch && { claudeBranch }),
|
||||
@@ -524,21 +527,50 @@ function substitutePromptVariables(
|
||||
return result;
|
||||
}
|
||||
|
||||
export function generatePrompt(
|
||||
export async function generatePrompt(
|
||||
context: PreparedContext,
|
||||
githubData: FetchDataResult,
|
||||
useCommitSigning: boolean,
|
||||
mode: Mode,
|
||||
): string {
|
||||
if (context.overridePrompt) {
|
||||
return substitutePromptVariables(
|
||||
context.overridePrompt,
|
||||
context,
|
||||
githubData,
|
||||
);
|
||||
): Promise<string> {
|
||||
// Check for unified prompt field first (v1.0)
|
||||
let prompt = context.prompt || context.overridePrompt || context.directPrompt || "";
|
||||
|
||||
// Handle slash commands
|
||||
if (prompt.startsWith("/")) {
|
||||
const variables = {
|
||||
repository: context.repository,
|
||||
pr_number: context.eventData.isPR && 'prNumber' in context.eventData ? context.eventData.prNumber : "",
|
||||
issue_number: !context.eventData.isPR && 'issueNumber' in context.eventData ? context.eventData.issueNumber : "",
|
||||
branch: context.eventData.claudeBranch || "",
|
||||
base_branch: context.eventData.baseBranch || "",
|
||||
trigger_user: context.triggerUsername,
|
||||
};
|
||||
|
||||
const resolved = await resolveSlashCommand(prompt, variables);
|
||||
|
||||
// Apply any tools from the slash command
|
||||
if (resolved.tools && resolved.tools.length > 0) {
|
||||
const currentAllowedTools = process.env.ALLOWED_TOOLS || "";
|
||||
const newTools = resolved.tools.join(",");
|
||||
const combinedTools = currentAllowedTools ? `${currentAllowedTools},${newTools}` : newTools;
|
||||
core.exportVariable("ALLOWED_TOOLS", combinedTools);
|
||||
}
|
||||
|
||||
// Apply any settings from the slash command
|
||||
if (resolved.settings) {
|
||||
core.exportVariable("SLASH_COMMAND_SETTINGS", JSON.stringify(resolved.settings));
|
||||
}
|
||||
|
||||
prompt = resolved.expandedPrompt;
|
||||
}
|
||||
|
||||
// Use the mode's prompt generator
|
||||
|
||||
// If we have a prompt, use it (with variable substitution)
|
||||
if (prompt) {
|
||||
return substitutePromptVariables(prompt, context, githubData);
|
||||
}
|
||||
|
||||
// Otherwise use the mode's default prompt generator
|
||||
return mode.generatePrompt(context, githubData, useCommitSigning);
|
||||
}
|
||||
|
||||
@@ -840,8 +872,8 @@ export async function createPrompt(
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
// Generate the prompt directly
|
||||
const promptContent = generatePrompt(
|
||||
// Generate the prompt directly (now async due to slash commands)
|
||||
const promptContent = await generatePrompt(
|
||||
preparedContext,
|
||||
githubData,
|
||||
context.inputs.useCommitSigning,
|
||||
|
||||
@@ -6,8 +6,9 @@ export type CommonFields = {
|
||||
customInstructions?: string;
|
||||
allowedTools?: string;
|
||||
disallowedTools?: string;
|
||||
directPrompt?: string;
|
||||
overridePrompt?: string;
|
||||
prompt?: string; // v1.0: Unified prompt field
|
||||
directPrompt?: string; // Deprecated
|
||||
overridePrompt?: string; // Deprecated
|
||||
};
|
||||
|
||||
type PullRequestReviewCommentEvent = {
|
||||
|
||||
@@ -10,29 +10,27 @@ import { setupGitHubToken } from "../github/token";
|
||||
import { checkWritePermissions } from "../github/validation/permissions";
|
||||
import { createOctokit } from "../github/api/client";
|
||||
import { parseGitHubContext, isEntityContext } from "../github/context";
|
||||
import { getMode, isValidMode, DEFAULT_MODE } from "../modes/registry";
|
||||
import type { ModeName } from "../modes/types";
|
||||
import { getMode } from "../modes/registry";
|
||||
import { prepare } from "../prepare";
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
// Step 1: Get mode first to determine authentication method
|
||||
const modeInput = process.env.MODE || DEFAULT_MODE;
|
||||
// Parse GitHub context first to enable mode detection
|
||||
const context = parseGitHubContext();
|
||||
|
||||
// Validate mode input
|
||||
if (!isValidMode(modeInput)) {
|
||||
throw new Error(`Invalid mode: ${modeInput}`);
|
||||
}
|
||||
const validatedMode: ModeName = modeInput;
|
||||
// Auto-detect mode based on context, with optional override
|
||||
const modeOverride = process.env.MODE;
|
||||
const mode = getMode(context, modeOverride);
|
||||
const modeName = mode.name;
|
||||
|
||||
// Step 2: Setup GitHub token based on mode
|
||||
// Setup GitHub token based on mode
|
||||
let githubToken: string;
|
||||
if (validatedMode === "experimental-review") {
|
||||
// For experimental-review mode, use the default GitHub Action token
|
||||
if (modeName === "review" || modeName === "experimental-review") {
|
||||
// For review mode, use the default GitHub Action token
|
||||
githubToken = process.env.DEFAULT_WORKFLOW_TOKEN || "";
|
||||
if (!githubToken) {
|
||||
throw new Error(
|
||||
"DEFAULT_WORKFLOW_TOKEN not found for experimental-review mode",
|
||||
"DEFAULT_WORKFLOW_TOKEN not found for review mode",
|
||||
);
|
||||
}
|
||||
console.log("Using default GitHub Action token for review mode");
|
||||
@@ -43,8 +41,6 @@ async function run() {
|
||||
}
|
||||
const octokit = createOctokit(githubToken);
|
||||
|
||||
// Step 2: Parse GitHub context (once for all operations)
|
||||
const context = parseGitHubContext();
|
||||
|
||||
// Step 3: Check write permissions (only for entity contexts)
|
||||
if (isEntityContext(context)) {
|
||||
@@ -59,8 +55,7 @@ async function run() {
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Get mode and check trigger conditions
|
||||
const mode = getMode(validatedMode, context);
|
||||
// Check trigger conditions
|
||||
const containsTrigger = mode.shouldTrigger(context);
|
||||
|
||||
// Set output for action.yml to check
|
||||
|
||||
@@ -35,7 +35,6 @@ export type ScheduleEvent = {
|
||||
};
|
||||
};
|
||||
import type { ModeName } from "../modes/types";
|
||||
import { DEFAULT_MODE, isValidMode } from "../modes/registry";
|
||||
|
||||
// Event name constants for better maintainability
|
||||
const ENTITY_EVENT_NAMES = [
|
||||
@@ -63,15 +62,16 @@ type BaseContext = {
|
||||
};
|
||||
actor: string;
|
||||
inputs: {
|
||||
mode: ModeName;
|
||||
mode?: ModeName; // Optional for v1.0 backward compatibility
|
||||
prompt: string; // New unified prompt field
|
||||
triggerPhrase: string;
|
||||
assigneeTrigger: string;
|
||||
labelTrigger: string;
|
||||
allowedTools: string[];
|
||||
disallowedTools: string[];
|
||||
customInstructions: string;
|
||||
directPrompt: string;
|
||||
overridePrompt: string;
|
||||
directPrompt: string; // Deprecated, kept for compatibility
|
||||
overridePrompt: string; // Deprecated, kept for compatibility
|
||||
baseBranch?: string;
|
||||
branchPrefix: string;
|
||||
useStickyComment: boolean;
|
||||
@@ -105,10 +105,8 @@ export type GitHubContext = ParsedGitHubContext | AutomationContext;
|
||||
export function parseGitHubContext(): GitHubContext {
|
||||
const context = github.context;
|
||||
|
||||
const modeInput = process.env.MODE ?? DEFAULT_MODE;
|
||||
if (!isValidMode(modeInput)) {
|
||||
throw new Error(`Invalid mode: ${modeInput}.`);
|
||||
}
|
||||
// Mode is optional in v1.0 (auto-detected)
|
||||
const modeInput = process.env.MODE ? process.env.MODE as ModeName : undefined;
|
||||
|
||||
const commonFields = {
|
||||
runId: process.env.GITHUB_RUN_ID!,
|
||||
@@ -120,15 +118,19 @@ export function parseGitHubContext(): GitHubContext {
|
||||
},
|
||||
actor: context.actor,
|
||||
inputs: {
|
||||
mode: modeInput as ModeName,
|
||||
mode: modeInput,
|
||||
// v1.0: Unified prompt field with fallback to legacy fields
|
||||
prompt: process.env.PROMPT ||
|
||||
process.env.OVERRIDE_PROMPT ||
|
||||
process.env.DIRECT_PROMPT || "",
|
||||
triggerPhrase: process.env.TRIGGER_PHRASE ?? "@claude",
|
||||
assigneeTrigger: process.env.ASSIGNEE_TRIGGER ?? "",
|
||||
labelTrigger: process.env.LABEL_TRIGGER ?? "",
|
||||
allowedTools: parseMultilineInput(process.env.ALLOWED_TOOLS ?? ""),
|
||||
disallowedTools: parseMultilineInput(process.env.DISALLOWED_TOOLS ?? ""),
|
||||
customInstructions: process.env.CUSTOM_INSTRUCTIONS ?? "",
|
||||
directPrompt: process.env.DIRECT_PROMPT ?? "",
|
||||
overridePrompt: process.env.OVERRIDE_PROMPT ?? "",
|
||||
directPrompt: process.env.DIRECT_PROMPT ?? "", // Deprecated
|
||||
overridePrompt: process.env.OVERRIDE_PROMPT ?? "", // Deprecated
|
||||
baseBranch: process.env.BASE_BRANCH,
|
||||
branchPrefix: process.env.BRANCH_PREFIX ?? "claude/",
|
||||
useStickyComment: process.env.USE_STICKY_COMMENT === "true",
|
||||
|
||||
77
src/modes/detector.ts
Normal file
77
src/modes/detector.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import type { GitHubContext } from "../github/context";
|
||||
import {
|
||||
isEntityContext,
|
||||
isAutomationContext,
|
||||
isPullRequestEvent,
|
||||
isIssueCommentEvent,
|
||||
isPullRequestReviewCommentEvent,
|
||||
} from "../github/context";
|
||||
import { checkContainsTrigger } from "../github/validation/trigger";
|
||||
|
||||
export type AutoDetectedMode = "review" | "tag" | "agent";
|
||||
|
||||
export function detectMode(context: GitHubContext): AutoDetectedMode {
|
||||
if (isPullRequestEvent(context)) {
|
||||
const allowedActions = ["opened", "synchronize", "reopened"];
|
||||
const action = context.payload.action;
|
||||
if (allowedActions.includes(action)) {
|
||||
return "review";
|
||||
}
|
||||
}
|
||||
|
||||
if (isEntityContext(context)) {
|
||||
if (
|
||||
isIssueCommentEvent(context) ||
|
||||
isPullRequestReviewCommentEvent(context)
|
||||
) {
|
||||
if (checkContainsTrigger(context)) {
|
||||
return "tag";
|
||||
}
|
||||
}
|
||||
|
||||
if (context.eventName === "issues") {
|
||||
if (checkContainsTrigger(context)) {
|
||||
return "tag";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isAutomationContext(context)) {
|
||||
return "agent";
|
||||
}
|
||||
|
||||
return "agent";
|
||||
}
|
||||
|
||||
export function getModeDescription(mode: AutoDetectedMode): string {
|
||||
switch (mode) {
|
||||
case "review":
|
||||
return "Automated code review mode for pull requests";
|
||||
case "tag":
|
||||
return "Interactive mode triggered by @claude mentions";
|
||||
case "agent":
|
||||
return "Automation mode for scheduled tasks and workflows";
|
||||
default:
|
||||
return "Unknown mode";
|
||||
}
|
||||
}
|
||||
|
||||
export function shouldUseTrackingComment(mode: AutoDetectedMode): boolean {
|
||||
return mode === "tag";
|
||||
}
|
||||
|
||||
export function getDefaultPromptForMode(
|
||||
mode: AutoDetectedMode,
|
||||
context: GitHubContext,
|
||||
): string | undefined {
|
||||
switch (mode) {
|
||||
case "review":
|
||||
return "/review";
|
||||
case "tag":
|
||||
return undefined;
|
||||
case "agent":
|
||||
return context.inputs?.directPrompt || context.inputs?.overridePrompt;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,8 @@
|
||||
/**
|
||||
* Mode Registry for claude-code-action
|
||||
* Mode Registry for claude-code-action v1.0
|
||||
*
|
||||
* This module provides access to all available execution modes.
|
||||
*
|
||||
* To add a new mode:
|
||||
* 1. Add the mode name to VALID_MODES below
|
||||
* 2. Create the mode implementation in a new directory (e.g., src/modes/new-mode/)
|
||||
* 3. Import and add it to the modes object below
|
||||
* 4. Update action.yml description to mention the new mode
|
||||
* This module provides access to all available execution modes and handles
|
||||
* automatic mode detection based on GitHub event types.
|
||||
*/
|
||||
|
||||
import type { Mode, ModeName } from "./types";
|
||||
@@ -15,41 +10,44 @@ import { tagMode } from "./tag";
|
||||
import { agentMode } from "./agent";
|
||||
import { reviewMode } from "./review";
|
||||
import type { GitHubContext } from "../github/context";
|
||||
import { isAutomationContext } from "../github/context";
|
||||
import { detectMode, type AutoDetectedMode } from "./detector";
|
||||
|
||||
export const DEFAULT_MODE = "tag" as const;
|
||||
export const VALID_MODES = ["tag", "agent", "experimental-review"] as const;
|
||||
export const VALID_MODES = ["tag", "agent", "review"] as const;
|
||||
|
||||
/**
|
||||
* All available modes.
|
||||
* Add new modes here as they are created.
|
||||
* All available modes in v1.0
|
||||
*/
|
||||
const modes = {
|
||||
tag: tagMode,
|
||||
agent: agentMode,
|
||||
"experimental-review": reviewMode,
|
||||
} as const satisfies Record<ModeName, Mode>;
|
||||
review: reviewMode,
|
||||
} as const satisfies Record<AutoDetectedMode, Mode>;
|
||||
|
||||
/**
|
||||
* Retrieves a mode by name and validates it can handle the event type.
|
||||
* @param name The mode name to retrieve
|
||||
* @param context The GitHub context to validate against
|
||||
* @returns The requested mode
|
||||
* @throws Error if the mode is not found or cannot handle the event
|
||||
* Automatically detects and retrieves the appropriate mode based on the GitHub context.
|
||||
* In v1.0, modes are auto-selected based on event type.
|
||||
* @param context The GitHub context
|
||||
* @param explicitMode Optional explicit mode override (for backward compatibility)
|
||||
* @returns The appropriate mode for the context
|
||||
*/
|
||||
export function getMode(name: ModeName, context: GitHubContext): Mode {
|
||||
const mode = modes[name];
|
||||
if (!mode) {
|
||||
const validModes = VALID_MODES.join("', '");
|
||||
throw new Error(
|
||||
`Invalid mode '${name}'. Valid modes are: '${validModes}'. Please check your workflow configuration.`,
|
||||
);
|
||||
export function getMode(
|
||||
context: GitHubContext,
|
||||
explicitMode?: string,
|
||||
): Mode {
|
||||
let modeName: AutoDetectedMode;
|
||||
|
||||
if (explicitMode && isValidModeV1(explicitMode)) {
|
||||
console.log(`Using explicit mode: ${explicitMode}`);
|
||||
modeName = mapLegacyMode(explicitMode);
|
||||
} else {
|
||||
modeName = detectMode(context);
|
||||
console.log(`Auto-detected mode: ${modeName} for event: ${context.eventName}`);
|
||||
}
|
||||
|
||||
// Validate mode can handle the event type
|
||||
if (name === "tag" && isAutomationContext(context)) {
|
||||
const mode = modes[modeName];
|
||||
if (!mode) {
|
||||
throw new Error(
|
||||
`Tag mode cannot handle ${context.eventName} events. Use 'agent' mode for automation events.`,
|
||||
`Mode '${modeName}' not found. This should not happen. Please report this issue.`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -57,10 +55,29 @@ export function getMode(name: ModeName, context: GitHubContext): Mode {
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if a string is a valid mode name.
|
||||
* Maps legacy mode names to v1.0 mode names
|
||||
*/
|
||||
function mapLegacyMode(name: string): AutoDetectedMode {
|
||||
if (name === "experimental-review") {
|
||||
return "review";
|
||||
}
|
||||
return name as AutoDetectedMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if a string is a valid v1.0 mode name.
|
||||
* @param name The string to check
|
||||
* @returns True if the name is a valid mode name
|
||||
*/
|
||||
export function isValidMode(name: string): name is ModeName {
|
||||
return VALID_MODES.includes(name as ModeName);
|
||||
export function isValidModeV1(name: string): boolean {
|
||||
const v1Modes = ["tag", "agent", "review", "experimental-review"];
|
||||
return v1Modes.includes(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy type guard for backward compatibility
|
||||
* @deprecated Use auto-detection instead
|
||||
*/
|
||||
export function isValidMode(name: string): name is ModeName {
|
||||
return isValidModeV1(name);
|
||||
}
|
||||
|
||||
@@ -89,10 +89,14 @@ export const reviewMode: Mode = {
|
||||
context: PreparedContext,
|
||||
githubData: FetchDataResult,
|
||||
): string {
|
||||
// Support overridePrompt
|
||||
if (context.overridePrompt) {
|
||||
return context.overridePrompt;
|
||||
// Support v1.0 unified prompt or legacy overridePrompt
|
||||
const userPrompt = context.prompt || context.overridePrompt;
|
||||
if (userPrompt) {
|
||||
return userPrompt;
|
||||
}
|
||||
|
||||
// Default to /review slash command content
|
||||
// This will be expanded by the slash command system
|
||||
|
||||
const {
|
||||
contextData,
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { PreparedContext } from "../create-prompt/types";
|
||||
import type { FetchDataResult } from "../github/data/fetcher";
|
||||
import type { Octokits } from "../github/api/client";
|
||||
|
||||
export type ModeName = "tag" | "agent" | "experimental-review";
|
||||
export type ModeName = "tag" | "agent" | "experimental-review" | "review";
|
||||
|
||||
export type ModeContext = {
|
||||
mode: ModeName;
|
||||
|
||||
167
src/slash-commands/loader.ts
Normal file
167
src/slash-commands/loader.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { readFile, readdir, stat } from "fs/promises";
|
||||
import { join, dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import * as yaml from "js-yaml";
|
||||
|
||||
export interface SlashCommandMetadata {
|
||||
tools?: string[];
|
||||
settings?: Record<string, any>;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface SlashCommand {
|
||||
name: string;
|
||||
metadata: SlashCommandMetadata;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface ResolvedCommand {
|
||||
expandedPrompt: string;
|
||||
tools?: string[];
|
||||
settings?: Record<string, any>;
|
||||
}
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const COMMANDS_DIR = join(__dirname, "../../slash-commands");
|
||||
|
||||
export async function resolveSlashCommand(
|
||||
prompt: string,
|
||||
variables?: Record<string, string | undefined>,
|
||||
): Promise<ResolvedCommand> {
|
||||
if (!prompt.startsWith("/")) {
|
||||
return handleLegacyPrompts(prompt);
|
||||
}
|
||||
|
||||
const parts = prompt.slice(1).split(" ");
|
||||
const commandPath = parts[0];
|
||||
const args = parts.slice(1);
|
||||
|
||||
if (!commandPath) {
|
||||
return { expandedPrompt: prompt };
|
||||
}
|
||||
|
||||
const commandParts = commandPath.split("/");
|
||||
|
||||
try {
|
||||
const command = await loadCommand(commandParts);
|
||||
if (!command) {
|
||||
console.warn(`Slash command not found: ${commandPath}`);
|
||||
return { expandedPrompt: prompt };
|
||||
}
|
||||
|
||||
let expandedContent = command.content;
|
||||
|
||||
if (args.length > 0) {
|
||||
expandedContent = expandedContent.replace(
|
||||
/\{args\}/g,
|
||||
args.join(" "),
|
||||
);
|
||||
}
|
||||
|
||||
if (variables) {
|
||||
Object.entries(variables).forEach(([key, value]) => {
|
||||
if (value !== undefined) {
|
||||
const regex = new RegExp(`\\{${key}\\}`, "g");
|
||||
expandedContent = expandedContent.replace(regex, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
expandedPrompt: expandedContent,
|
||||
tools: command.metadata.tools,
|
||||
settings: command.metadata.settings,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error loading slash command: ${error}`);
|
||||
return { expandedPrompt: prompt };
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCommand(
|
||||
commandParts: string[],
|
||||
): Promise<SlashCommand | null> {
|
||||
const possiblePaths = [
|
||||
join(COMMANDS_DIR, ...commandParts) + ".md",
|
||||
join(COMMANDS_DIR, ...commandParts, "default.md"),
|
||||
join(COMMANDS_DIR, commandParts[0] + ".md"),
|
||||
];
|
||||
|
||||
for (const filePath of possiblePaths) {
|
||||
try {
|
||||
const fileContent = await readFile(filePath, "utf-8");
|
||||
return parseCommandFile(commandParts.join("/"), fileContent);
|
||||
} catch (error) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseCommandFile(name: string, content: string): SlashCommand {
|
||||
let metadata: SlashCommandMetadata = {};
|
||||
let commandContent = content;
|
||||
|
||||
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
||||
if (frontmatterMatch && frontmatterMatch[1]) {
|
||||
try {
|
||||
const parsedYaml = yaml.load(frontmatterMatch[1]);
|
||||
if (parsedYaml && typeof parsedYaml === 'object') {
|
||||
metadata = parsedYaml as SlashCommandMetadata;
|
||||
}
|
||||
commandContent = frontmatterMatch[2]?.trim() || content;
|
||||
} catch (error) {
|
||||
console.warn(`Failed to parse frontmatter for command ${name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
metadata,
|
||||
content: commandContent,
|
||||
};
|
||||
}
|
||||
|
||||
export async function listAvailableCommands(): Promise<string[]> {
|
||||
const commands: string[] = [];
|
||||
|
||||
async function scanDirectory(dir: string, prefix = ""): Promise<void> {
|
||||
try {
|
||||
const entries = await readdir(dir);
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry);
|
||||
const entryStat = await stat(fullPath);
|
||||
|
||||
if (entryStat.isDirectory()) {
|
||||
await scanDirectory(fullPath, prefix ? `${prefix}/${entry}` : entry);
|
||||
} else if (entry.endsWith(".md")) {
|
||||
const commandName = entry.replace(".md", "");
|
||||
if (commandName !== "default") {
|
||||
commands.push(prefix ? `${prefix}/${commandName}` : commandName);
|
||||
} else if (prefix) {
|
||||
commands.push(prefix);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error scanning directory ${dir}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
await scanDirectory(COMMANDS_DIR);
|
||||
return commands.sort();
|
||||
}
|
||||
|
||||
function handleLegacyPrompts(prompt: string): ResolvedCommand {
|
||||
const legacyKeys = ["override_prompt", "direct_prompt"];
|
||||
for (const key of legacyKeys) {
|
||||
const envValue = process.env[key.toUpperCase()];
|
||||
if (envValue) {
|
||||
console.log(`Using legacy ${key} as prompt`);
|
||||
return { expandedPrompt: envValue };
|
||||
}
|
||||
}
|
||||
return { expandedPrompt: prompt };
|
||||
}
|
||||
Reference in New Issue
Block a user