diff --git a/entry b/entry index 9a1768e..98fe2f9 100755 --- a/entry +++ b/entry @@ -135585,12 +135585,6 @@ var agentsManifest = { var AgentName = type.enumerated(...Object.keys(agentsManifest)); var Effort = type.enumerated("mini", "auto", "max"); -// mcp/bash.ts -import { spawn } from "node:child_process"; -import { randomUUID as randomUUID2 } from "node:crypto"; -import { closeSync, openSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; - // utils/log.ts var core = __toESM(require_core(), 1); var import_table = __toESM(require_src(), 1); @@ -135767,6 +135761,12 @@ function formatJsonValue(value2) { return compact.length > 80 || compact.includes("\n") ? JSON.stringify(value2, null, 2) : compact; } +// mcp/bash.ts +import { spawn } from "node:child_process"; +import { randomUUID as randomUUID2 } from "node:crypto"; +import { closeSync, openSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; + // node_modules/.pnpm/@toon-format+toon@1.4.0/node_modules/@toon-format/toon/dist/index.mjs var LIST_ITEM_MARKER = "-"; var LIST_ITEM_PREFIX = "- "; @@ -143291,10 +143291,12 @@ function UploadFileTool(ctx) { } // mcp/server.ts -function initToolState(ctx) { - const progressCommentIdStr = ctx.runInfo.workflowRunInfo.progressCommentId; - const progressCommentId = progressCommentIdStr ? parseInt(progressCommentIdStr, 10) : null; +function initToolState(params) { + const progressCommentId = params.progressCommentId ? parseInt(params.progressCommentId, 10) : null; const resolvedId = Number.isNaN(progressCommentId) ? null : progressCommentId; + if (progressCommentId) { + log.info(`\xBB using pre-created progress comment: ${progressCommentId}`); + } return { progressCommentId: resolvedId, backgroundProcesses: /* @__PURE__ */ new Map() @@ -162544,12 +162546,13 @@ var JsonPayload = type({ "~pullfrog": "true", version: "string", "agent?": AgentName.or("undefined"), - "prompt?": "string", + prompt: "string", "eventInstructions?": "string", "repoInstructions?": "string", "event?": "object", "effort?": Effort.or("undefined"), - "timeout?": type.string.or("undefined") + "timeout?": type.string.or("undefined"), + "progressCommentId?": type.string.or("undefined") }); var COLLABORATOR_PERMISSIONS = ["admin", "maintain", "write"]; function isCollaborator(event) { @@ -162579,9 +162582,23 @@ function resolveCwd(cwd2) { if (isAbsolute(cwd2)) return cwd2; return workspace ? resolve(workspace, cwd2) : cwd2; } -function resolvePayload(repoSettings) { - const inputs = Inputs.assert({ - prompt: core4.getInput("prompt", { required: true }), +function resolvePromptInput() { + const prompt = core4.getInput("prompt", { required: true }); + let parsed2; + try { + parsed2 = JSON.parse(prompt); + } catch { + return prompt; + } + if (!parsed2 || typeof parsed2 !== "object" || !("~pullfrog" in parsed2)) { + return prompt; + } + const jsonPayload = JsonPayload.assert(parsed2); + validateCompatibility(jsonPayload.version, package_default.version); + return jsonPayload; +} +function resolveNonPromptInputs() { + return Inputs.omit("prompt").assert({ effort: core4.getInput("effort") || void 0, timeout: core4.getInput("timeout") || void 0, agent: core4.getInput("agent") || void 0, @@ -162591,19 +162608,11 @@ function resolvePayload(repoSettings) { write: core4.getInput("write") || void 0, bash: core4.getInput("bash") || void 0 }); +} +function resolvePayload(resolvedPromptInput, repoSettings) { + const [prompt, jsonPayload] = typeof resolvedPromptInput !== "string" ? [resolvedPromptInput.prompt, resolvedPromptInput] : [resolvedPromptInput, void 0]; + const inputs = resolveNonPromptInputs(); const agent2 = inputs.agent !== void 0 && isAgentName(inputs.agent) ? inputs.agent : void 0; - let jsonPayload = null; - try { - const parsed2 = JSON.parse(inputs.prompt); - if (parsed2 && typeof parsed2 === "object" && "~pullfrog" in parsed2) { - jsonPayload = JsonPayload.assert(parsed2); - } - } catch (error50) { - if (error50 instanceof type.errors) { - throw new Error(`invalid pullfrog payload: ${error50.summary}`); - } - } - if (jsonPayload) validateCompatibility(jsonPayload.version, package_default.version); const rawEvent = jsonPayload?.event; const event = isPayloadEvent(rawEvent) ? rawEvent : { trigger: "unknown" }; const jsonAgent = jsonPayload?.agent; @@ -162624,9 +162633,7 @@ function resolvePayload(repoSettings) { "~pullfrog": true, version: jsonPayload?.version ?? package_default.version, agent: resolvedAgent, - // inverted: jsonPayload.prompt extracts the text from the JSON payload, - // whereas inputs.prompt IS the raw JSON string when internally dispatched - prompt: jsonPayload?.prompt ?? inputs.prompt, + prompt, eventInstructions: jsonPayload?.eventInstructions, repoInstructions: jsonPayload?.repoInstructions, event, @@ -162835,35 +162842,6 @@ var Timer = class { } }; -// utils/workflowRun.ts -async function fetchWorkflowRunInfo(params) { - const apiUrl = process.env.API_URL || "https://pullfrog.com"; - const timeoutMs = 3e4; - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), timeoutMs); - try { - const response = await fetch(`${apiUrl}/api/workflow-run/${params.runId}`, { - method: "GET", - headers: { - // apiToken not strictly required (route only exposes public IDs) - // but claims in token enable preview API URL forwarding - Authorization: `Bearer ${params.apiToken}`, - "Content-Type": "application/json" - }, - signal: controller.signal - }); - clearTimeout(timeoutId); - if (!response.ok) { - return { progressCommentId: null }; - } - const data = await response.json(); - return data; - } catch { - clearTimeout(timeoutId); - return { progressCommentId: null }; - } -} - // utils/workflow.ts async function resolveRun(params) { const runId = process.env.GITHUB_RUN_ID || ""; @@ -162872,10 +162850,6 @@ async function resolveRun(params) { throw new Error(`GITHUB_REPOSITORY env var must be set to "owner/repo", got: ${githubRepo}`); } const [owner, repo] = githubRepo.split("/"); - const workflowRunInfo = runId ? await fetchWorkflowRunInfo({ runId, apiToken: params.apiToken }) : { progressCommentId: null }; - if (workflowRunInfo.progressCommentId) { - log.info(`\xBB using pre-created progress comment: ${workflowRunInfo.progressCommentId}`); - } let jobId; const jobName = process.env.GITHUB_JOB; if (jobName && runId) { @@ -162890,7 +162864,7 @@ async function resolveRun(params) { log.debug(`\xBB found job ID: ${jobId}`); } } - return { runId, jobId, workflowRunInfo }; + return { runId, jobId }; } // main.ts @@ -162900,17 +162874,20 @@ async function main() { normalizeEnv(); const timer = new Timer(); let activityTimeout = null; + const resolvedPromptInput = resolvePromptInput(); + const toolState = initToolState({ + progressCommentId: typeof resolvedPromptInput !== "string" ? resolvedPromptInput.progressCommentId : void 0 + }); + setupExitHandler(toolState); const tokenRef = __using(_stack2, await resolveInstallationToken(), true); const octokit = createOctokit(tokenRef.token); const runContext = await resolveRunContextData({ octokit, token: tokenRef.token }); timer.checkpoint("runContextData"); - const runInfo = await resolveRun({ octokit, apiToken: runContext.apiToken }); - const toolState = initToolState({ runInfo }); - setupExitHandler(toolState); + const runInfo = await resolveRun({ octokit }); try { var _stack = []; try { - const payload = resolvePayload(runContext.repoSettings); + const payload = resolvePayload(resolvedPromptInput, runContext.repoSettings); if (payload.cwd && process.cwd() !== payload.cwd) { process.chdir(payload.cwd); } diff --git a/external.ts b/external.ts index ebefc02..988251a 100644 --- a/external.ts +++ b/external.ts @@ -47,7 +47,7 @@ export const agentsManifest = { // agent name type - union of agent slugs export type AgentName = keyof typeof agentsManifest; -export const AgentName = type.enumerated(...Object.keys(agentsManifest)); +export const AgentName = type.enumerated(...(Object.keys(agentsManifest) as AgentName[])); export type AgentApiKeyName = (typeof agentsManifest)[AgentName]["apiKeyNames"][number]; @@ -259,6 +259,8 @@ export interface WriteablePayload { timeout?: string | undefined; /** working directory for the agent */ cwd?: string | undefined; + /** pre-created progress comment ID for updating status */ + progressCommentId?: string | undefined; } // immutable payload type for agent execution diff --git a/main.ts b/main.ts index e38b2fc..ce2682c 100644 --- a/main.ts +++ b/main.ts @@ -16,7 +16,7 @@ import { setupExitHandler } from "./utils/exitHandler.ts"; import { createOctokit } from "./utils/github.ts"; import { resolveInstructions } from "./utils/instructions.ts"; import { normalizeEnv } from "./utils/normalizeEnv.ts"; -import { resolvePayload } from "./utils/payload.ts"; +import { resolvePayload, resolvePromptInput } from "./utils/payload.ts"; import { handleAgentResult } from "./utils/run.ts"; import { resolveRunContextData } from "./utils/runContextData.ts"; import { createTempDirectory, setupGit } from "./utils/setup.ts"; @@ -42,22 +42,28 @@ export async function main(): Promise { const timer = new Timer(); let activityTimeout: ActivityTimeout | null = null; + // parse prompt early to extract progressCommentId for toolState + const resolvedPromptInput = resolvePromptInput(); + + const toolState = initToolState({ + progressCommentId: + typeof resolvedPromptInput !== "string" ? resolvedPromptInput.progressCommentId : undefined, + }); + + setupExitHandler(toolState); + await using tokenRef = await resolveInstallationToken(); const octokit = createOctokit(tokenRef.token); const runContext = await resolveRunContextData({ octokit, token: tokenRef.token }); timer.checkpoint("runContextData"); - const runInfo = await resolveRun({ octokit, apiToken: runContext.apiToken }); - const toolState = initToolState({ runInfo }); - - setupExitHandler(toolState); + const runInfo = await resolveRun({ octokit }); try { - // resolve payload after runContextData so permissions can use DB settings // precedence: action inputs > json payload > repoSettings > fallbacks - const payload = resolvePayload(runContext.repoSettings); + const payload = resolvePayload(resolvedPromptInput, runContext.repoSettings); if (payload.cwd && process.cwd() !== payload.cwd) { process.chdir(payload.cwd); } diff --git a/mcp/server.ts b/mcp/server.ts index fa2ae3e..685168c 100644 --- a/mcp/server.ts +++ b/mcp/server.ts @@ -35,17 +35,20 @@ export interface ToolState { output?: string; } -import type { ResolveRunResult } from "../utils/workflow.ts"; - interface InitToolStateParams { - runInfo: ResolveRunResult; + progressCommentId: string | undefined; } -export function initToolState(ctx: InitToolStateParams): ToolState { - const progressCommentIdStr = ctx.runInfo.workflowRunInfo.progressCommentId; - const progressCommentId = progressCommentIdStr ? parseInt(progressCommentIdStr, 10) : null; +export function initToolState(params: InitToolStateParams): ToolState { + const progressCommentId = params.progressCommentId + ? parseInt(params.progressCommentId, 10) + : null; const resolvedId = Number.isNaN(progressCommentId) ? null : progressCommentId; + if (progressCommentId) { + log.info(`» using pre-created progress comment: ${progressCommentId}`); + } + return { progressCommentId: resolvedId, backgroundProcesses: new Map(), @@ -65,6 +68,7 @@ export interface ToolContext { jobId: string | undefined; } +import { log } from "../utils/cli.ts"; import type { RunContextData } from "../utils/runContextData.ts"; import { BashTool, KillBackgroundTool } from "./bash.ts"; import { CheckoutPrTool } from "./checkout.ts"; diff --git a/utils/payload.test.ts b/utils/payload.test.ts index c1a3597..7aeb4d6 100644 --- a/utils/payload.test.ts +++ b/utils/payload.test.ts @@ -49,16 +49,19 @@ describe("Inputs schema", () => { }); describe("JsonPayload schema", () => { - it("requires ~pullfrog and version", () => { - const result = JsonPayload.assert({ "~pullfrog": true, version: "1.2.3" }); - expect(result).toMatchObject({ "~pullfrog": true, version: "1.2.3" }); + it("requires ~pullfrog and version and prompt", () => { + const result = JsonPayload.assert({ + "~pullfrog": true, + version: "1.2.3", + prompt: "test prompt", + }); + expect(result).toMatchObject({ "~pullfrog": true, version: "1.2.3", prompt: "test prompt" }); expect(() => JsonPayload.assert({})).toThrow(); expect(() => JsonPayload.assert({ "~pullfrog": true })).toThrow(); expect(() => JsonPayload.assert({ version: "1.2.3" })).toThrow(); }); it.each([ - ["prompt", "test prompt"], ["agent", "claude"], ["agent", "codex"], ["agent", "cursor"], @@ -72,12 +75,17 @@ describe("JsonPayload schema", () => { ["timeout", "30s"], ["event", { trigger: "unknown" }], ] as const)("should accept optional %s with value %s", (prop, value) => { - const input = { "~pullfrog": true, version: "1.2.3", [prop]: value }; + const input = { "~pullfrog": true, version: "1.2.3", prompt: "test prompt", [prop]: value }; expect(() => JsonPayload.assert(input)).not.toThrow(); }); it.each([["agent"], ["effort"]] as const)("should reject invalid %s values", (prop) => { - const input = { "~pullfrog": true, version: "1.2.3", [prop]: "invalid" as any }; + const input = { + "~pullfrog": true, + version: "1.2.3", + prompt: "test prompt", + [prop]: "invalid" as any, + }; expect(() => JsonPayload.assert(input)).toThrow(); }); }); diff --git a/utils/payload.ts b/utils/payload.ts index 9532f96..91cea01 100644 --- a/utils/payload.ts +++ b/utils/payload.ts @@ -17,12 +17,13 @@ export const JsonPayload = type({ "~pullfrog": "true", version: "string", "agent?": AgentName.or("undefined"), - "prompt?": "string", + prompt: "string", "eventInstructions?": "string", "repoInstructions?": "string", "event?": "object", "effort?": Effort.or("undefined"), "timeout?": type.string.or("undefined"), + "progressCommentId?": type.string.or("undefined"), }); // permission levels that indicate collaborator status (have push access) @@ -68,9 +69,32 @@ function resolveCwd(cwd: string | undefined): string | undefined { return workspace ? resolve(workspace, cwd) : cwd; } -export function resolvePayload(repoSettings: RepoSettings) { - const inputs = Inputs.assert({ - prompt: core.getInput("prompt", { required: true }), +export type ResolvedPromptInput = string | typeof JsonPayload.infer; + +export function resolvePromptInput(): ResolvedPromptInput { + const prompt = core.getInput("prompt", { required: true }); + + let parsed: unknown; + try { + parsed = JSON.parse(prompt); + } catch { + // JSON parse error is fine (plain text prompt) + return prompt; + } + + if (!parsed || typeof parsed !== "object" || !("~pullfrog" in parsed)) { + // if it doesn't look like a pullfrog payload, return the plain text prompt + return prompt; + } + + // validation errors should propagate + const jsonPayload = JsonPayload.assert(parsed); + validateCompatibility(jsonPayload.version, packageJson.version); + return jsonPayload; +} + +function resolveNonPromptInputs() { + return Inputs.omit("prompt").assert({ effort: core.getInput("effort") || undefined, timeout: core.getInput("timeout") || undefined, agent: core.getInput("agent") || undefined, @@ -80,30 +104,23 @@ export function resolvePayload(repoSettings: RepoSettings) { write: core.getInput("write") || undefined, bash: core.getInput("bash") || undefined, }); +} + +export function resolvePayload( + resolvedPromptInput: ResolvedPromptInput, + repoSettings: RepoSettings +) { + const [prompt, jsonPayload] = + typeof resolvedPromptInput !== "string" + ? [resolvedPromptInput.prompt, resolvedPromptInput] + : [resolvedPromptInput, undefined]; + + const inputs = resolveNonPromptInputs(); // validate agent name const agent: AgentName | undefined = inputs.agent !== undefined && isAgentName(inputs.agent) ? inputs.agent : undefined; - // try to parse prompt as JSON payload (internal invocation) - let jsonPayload: typeof JsonPayload.infer | null = null; - try { - const parsed = JSON.parse(inputs.prompt); - // if it looks like a pullfrog payload but fails validation, that's an error - if (parsed && typeof parsed === "object" && "~pullfrog" in parsed) { - jsonPayload = JsonPayload.assert(parsed); - } - } catch (error) { - // JSON parse error is fine (plain text prompt), but validation error should propagate - if (error instanceof type.errors) { - throw new Error(`invalid pullfrog payload: ${error.summary}`); - } - // not JSON, treat as plain string prompt - } - - // validate version compatibility from jsonPayload - if (jsonPayload) validateCompatibility(jsonPayload.version, packageJson.version); - // resolve event - use type guard for jsonPayload.event, fallback to unknown trigger const rawEvent = jsonPayload?.event; const event: PayloadEvent = isPayloadEvent(rawEvent) ? rawEvent : { trigger: "unknown" }; @@ -141,9 +158,7 @@ export function resolvePayload(repoSettings: RepoSettings) { "~pullfrog": true as const, version: jsonPayload?.version ?? packageJson.version, agent: resolvedAgent, - // inverted: jsonPayload.prompt extracts the text from the JSON payload, - // whereas inputs.prompt IS the raw JSON string when internally dispatched - prompt: jsonPayload?.prompt ?? inputs.prompt, + prompt, eventInstructions: jsonPayload?.eventInstructions, repoInstructions: jsonPayload?.repoInstructions, event, diff --git a/utils/workflow.ts b/utils/workflow.ts index 86405b9..798ed3a 100644 --- a/utils/workflow.ts +++ b/utils/workflow.ts @@ -1,16 +1,13 @@ import { log } from "./cli.ts"; import type { OctokitWithPlugins } from "./github.ts"; -import { fetchWorkflowRunInfo, type WorkflowRunInfo } from "./workflowRun.ts"; interface ResolveRunParams { octokit: OctokitWithPlugins; - apiToken: string; } export interface ResolveRunResult { runId: string; jobId: string | undefined; - workflowRunInfo: WorkflowRunInfo; } /** @@ -25,14 +22,6 @@ export async function resolveRun(params: ResolveRunParams): Promise { - const apiUrl = process.env.API_URL || "https://pullfrog.com"; - const timeoutMs = 30000; - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), timeoutMs); - - try { - const response = await fetch(`${apiUrl}/api/workflow-run/${params.runId}`, { - method: "GET", - headers: { - // apiToken not strictly required (route only exposes public IDs) - // but claims in token enable preview API URL forwarding - Authorization: `Bearer ${params.apiToken}`, - "Content-Type": "application/json", - }, - signal: controller.signal, - }); - - clearTimeout(timeoutId); - - if (!response.ok) { - return { progressCommentId: null }; - } - - const data = (await response.json()) as WorkflowRunInfo; - return data; - } catch { - clearTimeout(timeoutId); - return { progressCommentId: null }; - } -}