From 8f70e79240e6d97594f45427e1b58f3fba1837d6 Mon Sep 17 00:00:00 2001 From: Dotta Date: Thu, 5 Mar 2026 08:28:12 -0600 Subject: [PATCH] cursor adapter: auto-pass trust flag for non-interactive runs --- .../cursor-local/src/server/execute.ts | 99 ++++++++++++++++++- .../adapters/cursor-local/src/server/test.ts | 10 ++ .../adapters/cursor-local/src/shared/trust.ts | 9 ++ .../cursor-local-adapter-environment.test.ts | 87 ++++++++++++++++ 4 files changed, 200 insertions(+), 5 deletions(-) create mode 100644 packages/adapters/cursor-local/src/shared/trust.ts diff --git a/packages/adapters/cursor-local/src/server/execute.ts b/packages/adapters/cursor-local/src/server/execute.ts index 5a5af21e..5f29a178 100644 --- a/packages/adapters/cursor-local/src/server/execute.ts +++ b/packages/adapters/cursor-local/src/server/execute.ts @@ -1,5 +1,8 @@ import fs from "node:fs/promises"; +import type { Dirent } from "node:fs"; +import os from "node:os"; import path from "node:path"; +import { fileURLToPath } from "node:url"; import type { AdapterExecutionContext, AdapterExecutionResult } from "@paperclipai/adapter-utils"; import { asString, @@ -17,6 +20,13 @@ import { import { DEFAULT_CURSOR_LOCAL_MODEL } from "../index.js"; import { parseCursorJsonl, isCursorUnknownSessionError } from "./parse.js"; import { normalizeCursorStreamLine } from "../shared/stream.js"; +import { hasCursorTrustBypassArg } from "../shared/trust.js"; + +const __moduleDir = path.dirname(fileURLToPath(import.meta.url)); +const PAPERCLIP_SKILLS_CANDIDATES = [ + path.resolve(__moduleDir, "../../skills"), + path.resolve(__moduleDir, "../../../../../skills"), +]; function firstNonEmptyLine(text: string): string { return ( @@ -54,6 +64,76 @@ function normalizeMode(rawMode: string): "plan" | "ask" | null { return null; } +function cursorSkillsHome(): string { + return path.join(os.homedir(), ".cursor", "skills"); +} + +async function resolvePaperclipSkillsDir(): Promise { + for (const candidate of PAPERCLIP_SKILLS_CANDIDATES) { + const isDir = await fs.stat(candidate).then((s) => s.isDirectory()).catch(() => false); + if (isDir) return candidate; + } + return null; +} + +type EnsureCursorSkillsInjectedOptions = { + skillsDir?: string | null; + skillsHome?: string; + linkSkill?: (source: string, target: string) => Promise; +}; + +export async function ensureCursorSkillsInjected( + onLog: AdapterExecutionContext["onLog"], + options: EnsureCursorSkillsInjectedOptions = {}, +) { + const skillsDir = options.skillsDir ?? await resolvePaperclipSkillsDir(); + if (!skillsDir) return; + + const skillsHome = options.skillsHome ?? cursorSkillsHome(); + try { + await fs.mkdir(skillsHome, { recursive: true }); + } catch (err) { + await onLog( + "stderr", + `[paperclip] Failed to prepare Cursor skills directory ${skillsHome}: ${err instanceof Error ? err.message : String(err)}\n`, + ); + return; + } + + let entries: Dirent[]; + try { + entries = await fs.readdir(skillsDir, { withFileTypes: true }); + } catch (err) { + await onLog( + "stderr", + `[paperclip] Failed to read Paperclip skills from ${skillsDir}: ${err instanceof Error ? err.message : String(err)}\n`, + ); + return; + } + + const linkSkill = options.linkSkill ?? ((source: string, target: string) => fs.symlink(source, target)); + for (const entry of entries) { + if (!entry.isDirectory()) continue; + const source = path.join(skillsDir, entry.name); + const target = path.join(skillsHome, entry.name); + const existing = await fs.lstat(target).catch(() => null); + if (existing) continue; + + try { + await linkSkill(source, target); + await onLog( + "stderr", + `[paperclip] Injected Cursor skill "${entry.name}" into ${skillsHome}\n`, + ); + } catch (err) { + await onLog( + "stderr", + `[paperclip] Failed to inject Cursor skill "${entry.name}" into ${skillsHome}: ${err instanceof Error ? err.message : String(err)}\n`, + ); + } + } +} + export async function execute(ctx: AdapterExecutionContext): Promise { const { runId, agent, runtime, config, context, onLog, onMeta, authToken } = ctx; @@ -81,6 +161,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise 0) return fromExtraArgs; return asStringArray(config.args); })(); + const autoTrustEnabled = !hasCursorTrustBypassArg(extraArgs); const runtimeSessionParams = parseObject(runtime.sessionParams); const runtimeSessionId = asString(runtimeSessionParams.sessionId, runtime.sessionId ?? ""); @@ -201,16 +283,22 @@ export async function execute(ctx: AdapterExecutionContext): Promise { - if (!instructionsFilePath) return [] as string[]; + const notes: string[] = []; + if (autoTrustEnabled) { + notes.push("Auto-added --trust to bypass interactive workspace trust prompt."); + } + if (!instructionsFilePath) return notes; if (instructionsPrefix.length > 0) { - return [ + notes.push( `Loaded agent instructions from ${instructionsFilePath}`, `Prepended instructions + path directive to prompt (relative references from ${instructionsDir}).`, - ]; + ); + return notes; } - return [ + notes.push( `Configured instructionsFilePath ${instructionsFilePath}, but file could not be read; continuing without injected instructions.`, - ]; + ); + return notes; })(); const renderedPrompt = renderTemplate(promptTemplate, { @@ -229,6 +317,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise 0) args.push(...extraArgs); args.push(prompt); return args; diff --git a/packages/adapters/cursor-local/src/server/test.ts b/packages/adapters/cursor-local/src/server/test.ts index 8acae579..d6fca2aa 100644 --- a/packages/adapters/cursor-local/src/server/test.ts +++ b/packages/adapters/cursor-local/src/server/test.ts @@ -5,6 +5,7 @@ import type { } from "@paperclipai/adapter-utils"; import { asString, + asStringArray, parseObject, ensureAbsoluteDirectory, ensureCommandResolvable, @@ -14,6 +15,7 @@ import { import path from "node:path"; import { DEFAULT_CURSOR_LOCAL_MODEL } from "../index.js"; import { parseCursorJsonl } from "./parse.js"; +import { hasCursorTrustBypassArg } from "../shared/trust.js"; function summarizeStatus(checks: AdapterEnvironmentCheck[]): AdapterEnvironmentTestResult["status"] { if (checks.some((check) => check.level === "error")) return "fail"; @@ -128,8 +130,16 @@ export async function testEnvironment( }); } else { const model = asString(config.model, DEFAULT_CURSOR_LOCAL_MODEL).trim(); + const extraArgs = (() => { + const fromExtraArgs = asStringArray(config.extraArgs); + if (fromExtraArgs.length > 0) return fromExtraArgs; + return asStringArray(config.args); + })(); + const autoTrustEnabled = !hasCursorTrustBypassArg(extraArgs); const args = ["-p", "--mode", "ask", "--output-format", "json", "--workspace", cwd]; if (model) args.push("--model", model); + if (autoTrustEnabled) args.push("--trust"); + if (extraArgs.length > 0) args.push(...extraArgs); args.push("Respond with hello."); const probe = await runChildProcess( diff --git a/packages/adapters/cursor-local/src/shared/trust.ts b/packages/adapters/cursor-local/src/shared/trust.ts new file mode 100644 index 00000000..f04a8b4d --- /dev/null +++ b/packages/adapters/cursor-local/src/shared/trust.ts @@ -0,0 +1,9 @@ +export function hasCursorTrustBypassArg(args: readonly string[]): boolean { + return args.some( + (arg) => + arg === "--trust" || + arg === "--yolo" || + arg === "-f" || + arg.startsWith("--trust="), + ); +} diff --git a/server/src/__tests__/cursor-local-adapter-environment.test.ts b/server/src/__tests__/cursor-local-adapter-environment.test.ts index d04293f2..039a7a45 100644 --- a/server/src/__tests__/cursor-local-adapter-environment.test.ts +++ b/server/src/__tests__/cursor-local-adapter-environment.test.ts @@ -4,6 +4,29 @@ import os from "node:os"; import path from "node:path"; import { testEnvironment } from "@paperclipai/adapter-cursor-local/server"; +async function writeFakeAgentCommand(binDir: string, argsCapturePath: string): Promise { + const commandPath = path.join(binDir, "agent"); + const script = `#!/usr/bin/env node +const fs = require("node:fs"); +const outPath = process.env.PAPERCLIP_TEST_ARGS_PATH; +if (outPath) { + fs.writeFileSync(outPath, JSON.stringify(process.argv.slice(2)), "utf8"); +} +console.log(JSON.stringify({ + type: "assistant", + message: { content: [{ type: "output_text", text: "hello" }] }, +})); +console.log(JSON.stringify({ + type: "result", + subtype: "success", + result: "hello", +})); +`; + await fs.writeFile(commandPath, script, "utf8"); + await fs.chmod(commandPath, 0o755); + return commandPath; +} + describe("cursor environment diagnostics", () => { it("creates a missing working directory when cwd is absolute", async () => { const cwd = path.join( @@ -29,4 +52,68 @@ describe("cursor environment diagnostics", () => { expect(stats.isDirectory()).toBe(true); await fs.rm(path.dirname(cwd), { recursive: true, force: true }); }); + + it("adds --trust to hello probe args by default", async () => { + const root = path.join( + os.tmpdir(), + `paperclip-cursor-local-probe-${Date.now()}-${Math.random().toString(16).slice(2)}`, + ); + const binDir = path.join(root, "bin"); + const cwd = path.join(root, "workspace"); + const argsCapturePath = path.join(root, "args.json"); + await fs.mkdir(binDir, { recursive: true }); + await writeFakeAgentCommand(binDir, argsCapturePath); + + const result = await testEnvironment({ + companyId: "company-1", + adapterType: "cursor", + config: { + command: "agent", + cwd, + env: { + CURSOR_API_KEY: "test-key", + PAPERCLIP_TEST_ARGS_PATH: argsCapturePath, + PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ""}`, + }, + }, + }); + + expect(result.status).toBe("pass"); + const args = JSON.parse(await fs.readFile(argsCapturePath, "utf8")) as string[]; + expect(args).toContain("--trust"); + await fs.rm(root, { recursive: true, force: true }); + }); + + it("does not auto-add --trust when extraArgs already bypass trust", async () => { + const root = path.join( + os.tmpdir(), + `paperclip-cursor-local-probe-extra-${Date.now()}-${Math.random().toString(16).slice(2)}`, + ); + const binDir = path.join(root, "bin"); + const cwd = path.join(root, "workspace"); + const argsCapturePath = path.join(root, "args.json"); + await fs.mkdir(binDir, { recursive: true }); + await writeFakeAgentCommand(binDir, argsCapturePath); + + const result = await testEnvironment({ + companyId: "company-1", + adapterType: "cursor", + config: { + command: "agent", + cwd, + extraArgs: ["--yolo"], + env: { + CURSOR_API_KEY: "test-key", + PAPERCLIP_TEST_ARGS_PATH: argsCapturePath, + PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ""}`, + }, + }, + }); + + expect(result.status).toBe("pass"); + const args = JSON.parse(await fs.readFile(argsCapturePath, "utf8")) as string[]; + expect(args).toContain("--yolo"); + expect(args).not.toContain("--trust"); + await fs.rm(root, { recursive: true, force: true }); + }); });