Skip to content

Commit 87d895d

Browse files
committed
Updates
1 parent b1f769e commit 87d895d

3 files changed

Lines changed: 91 additions & 94 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ For information on how to run each project, see the README in each directory.
2525
| [Article summary workflow](/article-summary-workflow) | Create audio summaries of newspaper articles using a human-in-the-loop workflow with [ReactFlow](https://reactflow.dev/), [Trigger.dev Realtime](https://trigger.dev/docs/realtime/overview) and [waitpoints](https://trigger.dev/blog/v4-beta-launch#waitpoints) |
2626
| [Batch LLM evaluator](/batch-llm-evaluator) | Batch processing tool for evaluating LLM responses from a single prompt using Vercel's [AI SDK](https://sdk.vercel.ai/docs/introduction) and [Trigger.dev Realtime](https://trigger.dev/docs/realtime/overview) |
2727
| [Building effective agents](/building-effective-agents) | 5 different patterns for building effective AI agents with Trigger.dev; [Prompt chaining](/building-effective-agents/src/trigger/trigger/translate-copy.ts), [Routing](/building-effective-agents/src/trigger/trigger/routing-questions.ts), [Parallelization](/building-effective-agents/src/trigger/trigger/parallel-llm-calls.ts), [Orchestrator-workers](/building-effective-agents/src/trigger/trigger/orchestrator-workers.ts) |
28+
| [Cursor CLI demo](/cursor-cli-demo) | Run [Cursor's CLI](https://www.cursor.com/docs/cli/overview) in a [Trigger.dev](https://trigger.dev) task, and stream the output to the frontend. |
2829
| [Claude thinking chatbot](/claude-thinking-chatbot) | A chatbot that uses Claude's thinking capabilities to generate responses |
2930
| [Claude agent SDK](/claude-agent-sdk-trigger) | A simple example of how to use the [Claude Agent SDK](https://docs.claude.com/en/docs/agent-sdk/overview) with Trigger.dev |
3031
| [Claude changelog generator](/changelog-generator) | Generate changelogs from a GitHub repository using the [Claude Agent SDK](https://docs.claude.com/en/docs/agent-sdk/overview) with Trigger.dev |

cursor-cli-demo/extensions/cursor-cli.ts

Lines changed: 82 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
11
import type { BuildExtension } from "@trigger.dev/build";
2-
import type { ChildProcess } from "child_process";
32
import { spawn } from "child_process";
43
import { chmodSync, copyFileSync, existsSync, readdirSync } from "fs";
5-
import type { Readable } from "stream";
6-
7-
export type CursorAgentProcess = ChildProcess & {
8-
stdout: Readable;
9-
stderr: Readable;
10-
};
4+
import type { CursorEvent } from "@/lib/cursor-events";
5+
import { logger } from "@trigger.dev/sdk";
116

127
/** Where the build layer copies cursor-agent's resolved files */
138
export const CURSOR_AGENT_DIR = "/usr/local/lib/cursor-agent";
@@ -35,6 +30,16 @@ export function cursorCli(): BuildExtension {
3530
};
3631
}
3732

33+
export type ExitResult = {
34+
exitCode: number;
35+
stderr: string;
36+
};
37+
38+
export type CursorAgent = {
39+
stream: ReadableStream<CursorEvent>;
40+
waitUntilExit: () => Promise<ExitResult>;
41+
};
42+
3843
type SpawnCursorAgentOptions = {
3944
cwd: string;
4045
env?: Record<string, string | undefined>;
@@ -43,14 +48,17 @@ type SpawnCursorAgentOptions = {
4348
/**
4449
* Spawn cursor-agent at runtime inside the Trigger.dev container.
4550
*
51+
* Returns a NDJSON ReadableStream of CursorEvents and a waitUntilExit()
52+
* that resolves with the exit code and stderr.
53+
*
4654
* Handles the /tmp copy + chmod workaround needed because the runtime
4755
* strips execute permissions, and cursor-agent's native .node modules
4856
* require its bundled node (ABI mismatch with container node).
4957
*/
5058
export function spawnCursorAgent(
5159
args: string[],
5260
options: SpawnCursorAgentOptions,
53-
): CursorAgentProcess {
61+
): CursorAgent {
5462
const entryPoint = `${CURSOR_AGENT_DIR}/index.js`;
5563
const bundledNode = `${CURSOR_AGENT_DIR}/node`;
5664
const tmpNode = "/tmp/cursor-node";
@@ -65,14 +73,77 @@ export function spawnCursorAgent(
6573
copyFileSync(bundledNode, tmpNode);
6674
chmodSync(tmpNode, 0o755);
6775

68-
// stdio: ["ignore", "pipe", "pipe"] guarantees stdout/stderr are non-null
69-
return spawn(tmpNode, [entryPoint, ...args], {
76+
const child = spawn(tmpNode, [entryPoint, ...args], {
7077
stdio: ["ignore", "pipe", "pipe"],
7178
env: {
7279
...process.env,
7380
...options.env,
7481
CURSOR_INVOKED_AS: "cursor-agent",
7582
},
7683
cwd: options.cwd,
77-
}) as CursorAgentProcess;
84+
});
85+
86+
// Collect stderr
87+
let stderr = "";
88+
child.stderr!.on("data", (chunk: Buffer) => {
89+
stderr += chunk.toString();
90+
logger.warn("cursor-agent stderr", { text: chunk.toString() });
91+
});
92+
93+
// Build NDJSON ReadableStream from stdout
94+
let buffer = "";
95+
let streamClosed = false;
96+
const stream = new ReadableStream<CursorEvent>({
97+
start(controller) {
98+
const safeClose = () => {
99+
if (!streamClosed) {
100+
streamClosed = true;
101+
controller.close();
102+
}
103+
};
104+
105+
child.stdout!.on("data", (chunk: Buffer) => {
106+
buffer += chunk.toString();
107+
const lines = buffer.split("\n");
108+
buffer = lines.pop() ?? "";
109+
for (const line of lines) {
110+
if (line.trim()) {
111+
try {
112+
controller.enqueue(JSON.parse(line));
113+
} catch {
114+
logger.warn("Malformed NDJSON line", { line });
115+
}
116+
}
117+
}
118+
});
119+
120+
child.stdout!.on("end", () => {
121+
if (buffer.trim()) {
122+
try {
123+
controller.enqueue(JSON.parse(buffer));
124+
} catch {
125+
// skip
126+
}
127+
}
128+
safeClose();
129+
});
130+
131+
child.stdout!.on("error", () => safeClose());
132+
child.on("error", () => safeClose());
133+
},
134+
});
135+
136+
// waitUntilExit resolves when the child process exits
137+
const waitUntilExit = (): Promise<ExitResult> =>
138+
new Promise((resolve) => {
139+
if (child.exitCode !== null) {
140+
resolve({ exitCode: child.exitCode, stderr });
141+
return;
142+
}
143+
child.on("close", (code) => {
144+
resolve({ exitCode: code ?? 1, stderr });
145+
});
146+
});
147+
148+
return { stream, waitUntilExit };
78149
}
Lines changed: 8 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { logger, metadata, task } from "@trigger.dev/sdk";
1+
import { logger, streams, task } from "@trigger.dev/sdk";
22
import { mkdirSync } from "fs";
33
import type { CursorEvent } from "@/lib/cursor-events";
44
import { spawnCursorAgent } from "../extensions/cursor-cli";
@@ -24,96 +24,21 @@ export const cursorAgentTask = task({
2424

2525
logger.info("Spawning cursor-agent", { workspace, model });
2626

27-
const child = spawnCursorAgent(
27+
const agent = spawnCursorAgent(
2828
["-p", "--force", "--output-format", "stream-json", "--model", model, payload.prompt],
2929
{ cwd: workspace, env: { CURSOR_API_KEY: process.env.CURSOR_API_KEY } },
3030
);
3131

32-
let spawnError: Error | null = null;
33-
child.on("error", (err) => {
34-
spawnError = err;
35-
logger.error("cursor-agent spawn error", { message: err.message });
36-
});
32+
const { waitUntilComplete } = streams.pipe("cursor-events", agent.stream);
3733

38-
let stderr = "";
39-
let rawStdout = "";
40-
child.stderr.on("data", (chunk: Buffer) => {
41-
stderr += chunk.toString();
42-
logger.warn("cursor-agent stderr", { text: chunk.toString() });
43-
});
44-
45-
let buffer = "";
46-
let streamClosed = false;
47-
const ndjsonStream = new ReadableStream<CursorEvent>({
48-
start(controller) {
49-
const safeClose = () => {
50-
if (!streamClosed) {
51-
streamClosed = true;
52-
controller.close();
53-
}
54-
};
55-
56-
child.stdout.on("data", (chunk: Buffer) => {
57-
rawStdout += chunk.toString();
58-
buffer += chunk.toString();
59-
const lines = buffer.split("\n");
60-
buffer = lines.pop() ?? "";
61-
for (const line of lines) {
62-
if (line.trim()) {
63-
try {
64-
controller.enqueue(JSON.parse(line));
65-
} catch {
66-
logger.warn("Malformed NDJSON line", { line });
67-
}
68-
}
69-
}
70-
});
71-
72-
child.stdout.on("end", () => {
73-
if (buffer.trim()) {
74-
try {
75-
controller.enqueue(JSON.parse(buffer));
76-
} catch {
77-
// skip
78-
}
79-
}
80-
safeClose();
81-
});
82-
83-
child.stdout.on("error", () => safeClose());
84-
child.on("error", () => safeClose());
85-
},
86-
});
87-
88-
const stream = await metadata.stream("cursor-events", ndjsonStream);
89-
90-
for await (const event of stream) {
91-
logger.debug("cursor event", { type: event.type });
92-
}
93-
94-
const exitCode = await new Promise<number>((resolve) => {
95-
if (child.exitCode !== null) {
96-
resolve(child.exitCode);
97-
return;
98-
}
99-
child.on("close", (code) => resolve(code ?? 1));
100-
});
101-
102-
if (spawnError !== null) {
103-
throw new Error(
104-
`cursor-agent failed to start: ${(spawnError as Error).message}`,
105-
);
106-
}
34+
const { exitCode, stderr } = await agent.waitUntilExit();
35+
await waitUntilComplete();
10736

10837
if (exitCode !== 0) {
109-
logger.error("cursor-agent failed", { exitCode, stderr, rawStdout: rawStdout.slice(0, 2000) });
110-
throw new Error(stderr || rawStdout.slice(0, 500) || `cursor-agent exited with code ${exitCode}`);
38+
logger.error("cursor-agent failed", { exitCode, stderr });
39+
throw new Error(stderr || `cursor-agent exited with code ${exitCode}`);
11140
}
11241

113-
return {
114-
exitCode,
115-
prompt: payload.prompt,
116-
stderr: exitCode !== 0 ? stderr : undefined,
117-
};
42+
return { exitCode, prompt: payload.prompt };
11843
},
11944
});

0 commit comments

Comments
 (0)