cursor adapter: clarify paperclip env availability
This commit is contained in:
@@ -65,7 +65,7 @@ Core fields:
|
|||||||
- instructionsFilePath (string, optional): absolute path to a markdown instructions file prepended to the run prompt
|
- instructionsFilePath (string, optional): absolute path to a markdown instructions file prepended to the run prompt
|
||||||
- promptTemplate (string, optional): run prompt template
|
- promptTemplate (string, optional): run prompt template
|
||||||
- model (string, optional): Cursor model id (for example auto or gpt-5.3-codex)
|
- 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"
|
- command (string, optional): defaults to "agent"
|
||||||
- extraArgs (string[], optional): additional CLI args
|
- extraArgs (string[], optional): additional CLI args
|
||||||
- env (object, optional): KEY=VALUE environment variables
|
- env (object, optional): KEY=VALUE environment variables
|
||||||
|
|||||||
@@ -64,6 +64,20 @@ function normalizeMode(rawMode: string): "plan" | "ask" | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderPaperclipEnvNote(env: Record<string, string>): 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 {
|
function cursorSkillsHome(): string {
|
||||||
return path.join(os.homedir(), ".cursor", "skills");
|
return path.join(os.homedir(), ".cursor", "skills");
|
||||||
}
|
}
|
||||||
@@ -143,7 +157,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||||||
);
|
);
|
||||||
const command = asString(config.command, "agent");
|
const command = asString(config.command, "agent");
|
||||||
const model = asString(config.model, DEFAULT_CURSOR_LOCAL_MODEL).trim();
|
const model = asString(config.model, DEFAULT_CURSOR_LOCAL_MODEL).trim();
|
||||||
const mode = normalizeMode(asString(config.mode, ""));
|
const mode = normalizeMode(asString(config.mode, "ask"));
|
||||||
|
|
||||||
const workspaceContext = parseObject(context.paperclipWorkspace);
|
const workspaceContext = parseObject(context.paperclipWorkspace);
|
||||||
const workspaceCwd = asString(workspaceContext.cwd, "");
|
const workspaceCwd = asString(workspaceContext.cwd, "");
|
||||||
@@ -310,7 +324,8 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||||||
run: { id: runId, source: "on_demand" },
|
run: { id: runId, source: "on_demand" },
|
||||||
context,
|
context,
|
||||||
});
|
});
|
||||||
const prompt = `${instructionsPrefix}${renderedPrompt}`;
|
const paperclipEnvNote = renderPaperclipEnvNote(env);
|
||||||
|
const prompt = `${instructionsPrefix}${paperclipEnvNote}${renderedPrompt}`;
|
||||||
|
|
||||||
const buildArgs = (resumeSessionId: string | null) => {
|
const buildArgs = (resumeSessionId: string | null) => {
|
||||||
const args = ["-p", "--output-format", "stream-json", "--workspace", cwd];
|
const args = ["-p", "--output-format", "stream-json", "--workspace", cwd];
|
||||||
|
|||||||
123
server/src/__tests__/cursor-local-execute.test.ts
Normal file
123
server/src/__tests__/cursor-local-execute.test.ts
Normal file
@@ -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<void> {
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user