cursor adapter: auto-pass trust flag for non-interactive runs
This commit is contained in:
@@ -1,5 +1,8 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
|
import type { Dirent } from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
import type { AdapterExecutionContext, AdapterExecutionResult } from "@paperclipai/adapter-utils";
|
import type { AdapterExecutionContext, AdapterExecutionResult } from "@paperclipai/adapter-utils";
|
||||||
import {
|
import {
|
||||||
asString,
|
asString,
|
||||||
@@ -17,6 +20,13 @@ import {
|
|||||||
import { DEFAULT_CURSOR_LOCAL_MODEL } from "../index.js";
|
import { DEFAULT_CURSOR_LOCAL_MODEL } from "../index.js";
|
||||||
import { parseCursorJsonl, isCursorUnknownSessionError } from "./parse.js";
|
import { parseCursorJsonl, isCursorUnknownSessionError } from "./parse.js";
|
||||||
import { normalizeCursorStreamLine } from "../shared/stream.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 {
|
function firstNonEmptyLine(text: string): string {
|
||||||
return (
|
return (
|
||||||
@@ -54,6 +64,76 @@ function normalizeMode(rawMode: string): "plan" | "ask" | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cursorSkillsHome(): string {
|
||||||
|
return path.join(os.homedir(), ".cursor", "skills");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolvePaperclipSkillsDir(): Promise<string | null> {
|
||||||
|
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<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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<AdapterExecutionResult> {
|
export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult> {
|
||||||
const { runId, agent, runtime, config, context, onLog, onMeta, authToken } = ctx;
|
const { runId, agent, runtime, config, context, onLog, onMeta, authToken } = ctx;
|
||||||
|
|
||||||
@@ -81,6 +161,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||||||
const effectiveWorkspaceCwd = useConfiguredInsteadOfAgentHome ? "" : workspaceCwd;
|
const effectiveWorkspaceCwd = useConfiguredInsteadOfAgentHome ? "" : workspaceCwd;
|
||||||
const cwd = effectiveWorkspaceCwd || configuredCwd || process.cwd();
|
const cwd = effectiveWorkspaceCwd || configuredCwd || process.cwd();
|
||||||
await ensureAbsoluteDirectory(cwd, { createIfMissing: true });
|
await ensureAbsoluteDirectory(cwd, { createIfMissing: true });
|
||||||
|
await ensureCursorSkillsInjected(onLog);
|
||||||
|
|
||||||
const envConfig = parseObject(config.env);
|
const envConfig = parseObject(config.env);
|
||||||
const hasExplicitApiKey =
|
const hasExplicitApiKey =
|
||||||
@@ -163,6 +244,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||||||
if (fromExtraArgs.length > 0) return fromExtraArgs;
|
if (fromExtraArgs.length > 0) return fromExtraArgs;
|
||||||
return asStringArray(config.args);
|
return asStringArray(config.args);
|
||||||
})();
|
})();
|
||||||
|
const autoTrustEnabled = !hasCursorTrustBypassArg(extraArgs);
|
||||||
|
|
||||||
const runtimeSessionParams = parseObject(runtime.sessionParams);
|
const runtimeSessionParams = parseObject(runtime.sessionParams);
|
||||||
const runtimeSessionId = asString(runtimeSessionParams.sessionId, runtime.sessionId ?? "");
|
const runtimeSessionId = asString(runtimeSessionParams.sessionId, runtime.sessionId ?? "");
|
||||||
@@ -201,16 +283,22 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const commandNotes = (() => {
|
const commandNotes = (() => {
|
||||||
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) {
|
if (instructionsPrefix.length > 0) {
|
||||||
return [
|
notes.push(
|
||||||
`Loaded agent instructions from ${instructionsFilePath}`,
|
`Loaded agent instructions from ${instructionsFilePath}`,
|
||||||
`Prepended instructions + path directive to prompt (relative references from ${instructionsDir}).`,
|
`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.`,
|
`Configured instructionsFilePath ${instructionsFilePath}, but file could not be read; continuing without injected instructions.`,
|
||||||
];
|
);
|
||||||
|
return notes;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const renderedPrompt = renderTemplate(promptTemplate, {
|
const renderedPrompt = renderTemplate(promptTemplate, {
|
||||||
@@ -229,6 +317,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||||||
if (resumeSessionId) args.push("--resume", resumeSessionId);
|
if (resumeSessionId) args.push("--resume", resumeSessionId);
|
||||||
if (model) args.push("--model", model);
|
if (model) args.push("--model", model);
|
||||||
if (mode) args.push("--mode", mode);
|
if (mode) args.push("--mode", mode);
|
||||||
|
if (autoTrustEnabled) args.push("--trust");
|
||||||
if (extraArgs.length > 0) args.push(...extraArgs);
|
if (extraArgs.length > 0) args.push(...extraArgs);
|
||||||
args.push(prompt);
|
args.push(prompt);
|
||||||
return args;
|
return args;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type {
|
|||||||
} from "@paperclipai/adapter-utils";
|
} from "@paperclipai/adapter-utils";
|
||||||
import {
|
import {
|
||||||
asString,
|
asString,
|
||||||
|
asStringArray,
|
||||||
parseObject,
|
parseObject,
|
||||||
ensureAbsoluteDirectory,
|
ensureAbsoluteDirectory,
|
||||||
ensureCommandResolvable,
|
ensureCommandResolvable,
|
||||||
@@ -14,6 +15,7 @@ import {
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { DEFAULT_CURSOR_LOCAL_MODEL } from "../index.js";
|
import { DEFAULT_CURSOR_LOCAL_MODEL } from "../index.js";
|
||||||
import { parseCursorJsonl } from "./parse.js";
|
import { parseCursorJsonl } from "./parse.js";
|
||||||
|
import { hasCursorTrustBypassArg } from "../shared/trust.js";
|
||||||
|
|
||||||
function summarizeStatus(checks: AdapterEnvironmentCheck[]): AdapterEnvironmentTestResult["status"] {
|
function summarizeStatus(checks: AdapterEnvironmentCheck[]): AdapterEnvironmentTestResult["status"] {
|
||||||
if (checks.some((check) => check.level === "error")) return "fail";
|
if (checks.some((check) => check.level === "error")) return "fail";
|
||||||
@@ -128,8 +130,16 @@ export async function testEnvironment(
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const model = asString(config.model, DEFAULT_CURSOR_LOCAL_MODEL).trim();
|
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];
|
const args = ["-p", "--mode", "ask", "--output-format", "json", "--workspace", cwd];
|
||||||
if (model) args.push("--model", model);
|
if (model) args.push("--model", model);
|
||||||
|
if (autoTrustEnabled) args.push("--trust");
|
||||||
|
if (extraArgs.length > 0) args.push(...extraArgs);
|
||||||
args.push("Respond with hello.");
|
args.push("Respond with hello.");
|
||||||
|
|
||||||
const probe = await runChildProcess(
|
const probe = await runChildProcess(
|
||||||
|
|||||||
9
packages/adapters/cursor-local/src/shared/trust.ts
Normal file
9
packages/adapters/cursor-local/src/shared/trust.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export function hasCursorTrustBypassArg(args: readonly string[]): boolean {
|
||||||
|
return args.some(
|
||||||
|
(arg) =>
|
||||||
|
arg === "--trust" ||
|
||||||
|
arg === "--yolo" ||
|
||||||
|
arg === "-f" ||
|
||||||
|
arg.startsWith("--trust="),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,6 +4,29 @@ import os from "node:os";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { testEnvironment } from "@paperclipai/adapter-cursor-local/server";
|
import { testEnvironment } from "@paperclipai/adapter-cursor-local/server";
|
||||||
|
|
||||||
|
async function writeFakeAgentCommand(binDir: string, argsCapturePath: string): Promise<string> {
|
||||||
|
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", () => {
|
describe("cursor environment diagnostics", () => {
|
||||||
it("creates a missing working directory when cwd is absolute", async () => {
|
it("creates a missing working directory when cwd is absolute", async () => {
|
||||||
const cwd = path.join(
|
const cwd = path.join(
|
||||||
@@ -29,4 +52,68 @@ describe("cursor environment diagnostics", () => {
|
|||||||
expect(stats.isDirectory()).toBe(true);
|
expect(stats.isDirectory()).toBe(true);
|
||||||
await fs.rm(path.dirname(cwd), { recursive: true, force: 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 });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user