diff --git a/packages/adapters/cursor-local/src/index.ts b/packages/adapters/cursor-local/src/index.ts index 5c72a958..01eabfbd 100644 --- a/packages/adapters/cursor-local/src/index.ts +++ b/packages/adapters/cursor-local/src/index.ts @@ -65,7 +65,7 @@ Core fields: - instructionsFilePath (string, optional): absolute path to a markdown instructions file prepended to the run prompt - promptTemplate (string, optional): run prompt template - model (string, optional): Cursor model id (for example auto or gpt-5.3-codex) -- mode (string, optional): Cursor execution mode passed as --mode (plan|ask) +- mode (string, optional): Cursor execution mode passed as --mode (plan|ask). Defaults to ask when omitted. - command (string, optional): defaults to "agent" - extraArgs (string[], optional): additional CLI args - env (object, optional): KEY=VALUE environment variables diff --git a/packages/adapters/cursor-local/src/server/execute.ts b/packages/adapters/cursor-local/src/server/execute.ts index 5f29a178..39f94d7a 100644 --- a/packages/adapters/cursor-local/src/server/execute.ts +++ b/packages/adapters/cursor-local/src/server/execute.ts @@ -64,6 +64,20 @@ function normalizeMode(rawMode: string): "plan" | "ask" | null { return null; } +function renderPaperclipEnvNote(env: Record): string { + const paperclipKeys = Object.keys(env) + .filter((key) => key.startsWith("PAPERCLIP_")) + .sort(); + if (paperclipKeys.length === 0) return ""; + return [ + "Paperclip runtime note:", + `The following PAPERCLIP_* environment variables are available in this run: ${paperclipKeys.join(", ")}`, + "Do not assume these variables are missing without checking your shell environment.", + "", + "", + ].join("\n"); +} + function cursorSkillsHome(): string { return path.join(os.homedir(), ".cursor", "skills"); } @@ -143,7 +157,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise { const args = ["-p", "--output-format", "stream-json", "--workspace", cwd]; diff --git a/server/src/__tests__/cursor-local-execute.test.ts b/server/src/__tests__/cursor-local-execute.test.ts new file mode 100644 index 00000000..27638695 --- /dev/null +++ b/server/src/__tests__/cursor-local-execute.test.ts @@ -0,0 +1,123 @@ +import { describe, expect, it } from "vitest"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { execute } from "@paperclipai/adapter-cursor-local/server"; + +async function writeFakeCursorCommand(commandPath: string): Promise { + const script = `#!/usr/bin/env node +const fs = require("node:fs"); + +const capturePath = process.env.PAPERCLIP_TEST_CAPTURE_PATH; +const payload = { + argv: process.argv.slice(2), + prompt: process.argv.at(-1) ?? "", + paperclipEnvKeys: Object.keys(process.env) + .filter((key) => key.startsWith("PAPERCLIP_")) + .sort(), +}; +if (capturePath) { + fs.writeFileSync(capturePath, JSON.stringify(payload), "utf8"); +} +console.log(JSON.stringify({ + type: "system", + subtype: "init", + session_id: "cursor-session-1", + model: "auto", +})); +console.log(JSON.stringify({ + type: "assistant", + message: { content: [{ type: "output_text", text: "hello" }] }, +})); +console.log(JSON.stringify({ + type: "result", + subtype: "success", + session_id: "cursor-session-1", + result: "ok", +})); +`; + await fs.writeFile(commandPath, script, "utf8"); + await fs.chmod(commandPath, 0o755); +} + +type CapturePayload = { + argv: string[]; + prompt: string; + paperclipEnvKeys: string[]; +}; + +describe("cursor execute", () => { + it("injects paperclip env vars and prompt note by default", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-cursor-execute-")); + const workspace = path.join(root, "workspace"); + const commandPath = path.join(root, "agent"); + const capturePath = path.join(root, "capture.json"); + await fs.mkdir(workspace, { recursive: true }); + await writeFakeCursorCommand(commandPath); + + const previousHome = process.env.HOME; + process.env.HOME = root; + + let invocationPrompt = ""; + try { + const result = await execute({ + runId: "run-1", + agent: { + id: "agent-1", + companyId: "company-1", + name: "Cursor Coder", + adapterType: "cursor", + adapterConfig: {}, + }, + runtime: { + sessionId: null, + sessionParams: null, + sessionDisplayId: null, + taskKey: null, + }, + config: { + command: commandPath, + cwd: workspace, + model: "auto", + env: { + PAPERCLIP_TEST_CAPTURE_PATH: capturePath, + }, + promptTemplate: "Follow the paperclip heartbeat.", + }, + context: {}, + authToken: "run-jwt-token", + onLog: async () => {}, + onMeta: async (meta) => { + invocationPrompt = meta.prompt ?? ""; + }, + }); + + expect(result.exitCode).toBe(0); + expect(result.errorMessage).toBeNull(); + + const capture = JSON.parse(await fs.readFile(capturePath, "utf8")) as CapturePayload; + expect(capture.argv).toContain("--mode"); + expect(capture.argv).toContain("ask"); + expect(capture.paperclipEnvKeys).toEqual( + expect.arrayContaining([ + "PAPERCLIP_AGENT_ID", + "PAPERCLIP_API_KEY", + "PAPERCLIP_API_URL", + "PAPERCLIP_COMPANY_ID", + "PAPERCLIP_RUN_ID", + ]), + ); + expect(capture.prompt).toContain("Paperclip runtime note:"); + expect(capture.prompt).toContain("PAPERCLIP_API_KEY"); + expect(invocationPrompt).toContain("Paperclip runtime note:"); + expect(invocationPrompt).toContain("PAPERCLIP_API_URL"); + } finally { + if (previousHome === undefined) { + delete process.env.HOME; + } else { + process.env.HOME = previousHome; + } + await fs.rm(root, { recursive: true, force: true }); + } + }); +});