Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 5 additions & 0 deletions .changeset/summary-verbatim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pungrumpy/cursor-action": patch
---

Publish the agent's answer verbatim in `summary`. An answer that happened to be a JSON object was previously reduced to its `response`, `summary`, `result`, `output`, or `text` field, a leftover from the CLI's `--output-format json` era; workflows that ask for structured output now receive the whole object.
57 changes: 9 additions & 48 deletions __tests__/output.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,54 +50,15 @@ describe("setOutputs", () => {
expect(mockSummaryChain.write).toHaveBeenCalled();
});

it("extracts summary from JSON response field", async () => {
const result = {
exitCode: 0,
stderr: "",
stdout: JSON.stringify({ response: "JSON response text" }),
};
const outputs = await setOutputs(result);
expect(outputs.summary).toBe("JSON response text");
});

it("extracts summary from JSON summary field", async () => {
const result = {
exitCode: 0,
stderr: "",
stdout: JSON.stringify({ summary: "Summary field text" }),
};
const outputs = await setOutputs(result);
expect(outputs.summary).toBe("Summary field text");
});

it("extracts summary from JSON result field", async () => {
const result = {
exitCode: 0,
stderr: "",
stdout: JSON.stringify({ result: "Result field text" }),
};
const outputs = await setOutputs(result);
expect(outputs.summary).toBe("Result field text");
});

it("extracts summary from JSON output field", async () => {
const result = {
exitCode: 0,
stderr: "",
stdout: JSON.stringify({ output: "Output field text" }),
};
const outputs = await setOutputs(result);
expect(outputs.summary).toBe("Output field text");
});

it("extracts summary from JSON text field", async () => {
const result = {
exitCode: 0,
stderr: "",
stdout: JSON.stringify({ text: "Text field value" }),
};
const outputs = await setOutputs(result);
expect(outputs.summary).toBe("Text field value");
it("passes a JSON-shaped answer through unchanged", async () => {
const stdout = JSON.stringify({
findings: ["hardcoded token in config.ts"],
summary: "only this field used to survive",
});
const outputs = await setOutputs({ exitCode: 0, stderr: "", stdout });

expect(outputs.summary).toBe(stdout);
expect(mockSetOutput).toHaveBeenCalledWith("summary", stdout);
});

it("handles empty stdout gracefully", async () => {
Expand Down
21 changes: 6 additions & 15 deletions dist/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,12 @@ const getInputs = () => {
};
//#endregion
//#region src/output.ts
const parseSummary = (stdout) => {
const trimmed = stdout.trim();
if (!trimmed) return "";
try {
const parsed = JSON.parse(trimmed);
if (typeof parsed === "object" && parsed !== null) {
if (typeof parsed.response === "string") return parsed.response.trim();
if (typeof parsed.summary === "string") return parsed.summary.trim();
if (typeof parsed.result === "string") return parsed.result.trim();
if (typeof parsed.output === "string") return parsed.output.trim();
if (typeof parsed.text === "string") return parsed.text.trim();
}
} catch {}
return trimmed.replaceAll(/\u001B\[[0-9;]*[mGKHF]/gu, "");
};
/**
* The SDK returns the final assistant message as plain text. Only whitespace
* and stray terminal colour codes are removed; a JSON-shaped answer is
* published as-is so callers can parse the whole object.
*/
const parseSummary = (stdout) => stdout.trim().replaceAll(/\u001B\[[0-9;]*[mGKHF]/gu, "");
const buildSummaryTableRows = (result) => {
const rows = [["Status", result.exitCode === 0 ? "✅ Success" : `❌ Failed (exit ${result.exitCode})`], ["Exit Code", String(result.exitCode)]];
if (result.status) rows.push(["Agent Status", result.status]);
Expand Down
2 changes: 1 addition & 1 deletion docs/content/behaviour.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ description: What the action does at runtime and how it reports results.
1. Inputs are validated. An empty `prompt`, an unknown `permissions` value, or a non-positive `timeout` fails the step before anything is sent.
2. The API key is registered with `::add-mask::`.
3. A local SDK agent is created against the resolved `working-directory` (`Agent.create({ local: { cwd } })`) and the prompt is sent.
4. Streamed text is collected, then replaced by the final run result when the SDK provides one.
4. Streamed text is collected, then replaced by the final run result when the SDK provides one. `summary` is that text verbatim, with surrounding whitespace trimmed; a JSON-shaped answer is not unwrapped.
5. A job summary is written with the status, exit code, agent response, duration, token usage, and any stderr or diagnostics.

## Timeouts and failures
Expand Down
40 changes: 7 additions & 33 deletions src/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,18 @@ import { setOutput, setSecret, summary } from "@actions/core";

import type { ActionOutputs, AgentResult } from "./types";

const parseSummary = (stdout: string): string => {
const trimmed = stdout.trim();
if (!trimmed) {
return "";
}

try {
const parsed = JSON.parse(trimmed);
if (typeof parsed === "object" && parsed !== null) {
if (typeof parsed.response === "string") {
return parsed.response.trim();
}
if (typeof parsed.summary === "string") {
return parsed.summary.trim();
}
if (typeof parsed.result === "string") {
return parsed.result.trim();
}
if (typeof parsed.output === "string") {
return parsed.output.trim();
}
if (typeof parsed.text === "string") {
return parsed.text.trim();
}
}
} catch {
// Not JSON
}

const clean = trimmed.replaceAll(
/**
* The SDK returns the final assistant message as plain text. Only whitespace
* and stray terminal colour codes are removed; a JSON-shaped answer is
* published as-is so callers can parse the whole object.
*/
const parseSummary = (stdout: string): string =>
stdout.trim().replaceAll(
// eslint-disable-next-line no-control-regex
/\u001B\[[0-9;]*[mGKHF]/gu,
""
);

return clean;
};

const buildSummaryTableRows = (result: AgentResult): string[][] => {
const status =
result.exitCode === 0
Expand Down
Loading