feat: add cursor local adapter across server ui and cli

This commit is contained in:
Dotta
2026-03-05 06:31:22 -06:00
parent b4a02ebc3f
commit 8a85173150
35 changed files with 1871 additions and 20 deletions

View File

@@ -0,0 +1,47 @@
import type { AdapterConfigFieldsProps } from "../types";
import {
Field,
DraftInput,
} from "../../components/agent-config-primitives";
import { ChoosePathButton } from "../../components/PathInstructionsModal";
const inputClass =
"w-full rounded-md border border-border px-2.5 py-1.5 bg-transparent outline-none text-sm font-mono placeholder:text-muted-foreground/40";
const instructionsFileHint =
"Absolute path to a markdown file (e.g. AGENTS.md) that defines this agent's behavior. Injected into the prompt at runtime.";
export function CursorLocalConfigFields({
isCreate,
values,
set,
config,
eff,
mark,
}: AdapterConfigFieldsProps) {
return (
<Field label="Agent instructions file" hint={instructionsFileHint}>
<div className="flex items-center gap-2">
<DraftInput
value={
isCreate
? values!.instructionsFilePath ?? ""
: eff(
"adapterConfig",
"instructionsFilePath",
String(config.instructionsFilePath ?? ""),
)
}
onCommit={(v) =>
isCreate
? set!({ instructionsFilePath: v })
: mark("adapterConfig", "instructionsFilePath", v || undefined)
}
immediate
className={inputClass}
placeholder="/absolute/path/to/AGENTS.md"
/>
<ChoosePathButton />
</div>
</Field>
);
}

View File

@@ -0,0 +1,12 @@
import type { UIAdapterModule } from "../types";
import { parseCursorStdoutLine } from "@paperclipai/adapter-cursor-local/ui";
import { CursorLocalConfigFields } from "./config-fields";
import { buildCursorLocalConfig } from "@paperclipai/adapter-cursor-local/ui";
export const cursorLocalUIAdapter: UIAdapterModule = {
type: "cursor",
label: "Cursor CLI (local)",
parseStdoutLine: parseCursorStdoutLine,
ConfigFields: CursorLocalConfigFields,
buildAdapterConfig: buildCursorLocalConfig,
};

View File

@@ -1,13 +1,14 @@
import type { UIAdapterModule } from "./types";
import { claudeLocalUIAdapter } from "./claude-local";
import { codexLocalUIAdapter } from "./codex-local";
import { cursorLocalUIAdapter } from "./cursor";
import { openCodeLocalUIAdapter } from "./opencode-local";
import { openClawUIAdapter } from "./openclaw";
import { processUIAdapter } from "./process";
import { httpUIAdapter } from "./http";
const adaptersByType = new Map<string, UIAdapterModule>(
[claudeLocalUIAdapter, codexLocalUIAdapter, openCodeLocalUIAdapter, openClawUIAdapter, processUIAdapter, httpUIAdapter].map((a) => [a.type, a]),
[claudeLocalUIAdapter, codexLocalUIAdapter, openCodeLocalUIAdapter, cursorLocalUIAdapter, openClawUIAdapter, processUIAdapter, httpUIAdapter].map((a) => [a.type, a]),
);
export function getUIAdapter(type: string): UIAdapterModule {

View File

@@ -15,6 +15,7 @@ import {
DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX,
DEFAULT_CODEX_LOCAL_MODEL,
} from "@paperclipai/adapter-codex-local";
import { DEFAULT_CURSOR_LOCAL_MODEL } from "@paperclipai/adapter-cursor-local";
import { DEFAULT_OPENCODE_LOCAL_MODEL } from "@paperclipai/adapter-opencode-local";
import {
Popover,
@@ -140,6 +141,12 @@ const opencodeVariantOptions = [
{ id: "max", label: "Max" },
] as const;
const cursorModeOptions = [
{ id: "", label: "Auto" },
{ id: "plan", label: "Plan" },
{ id: "ask", label: "Ask" },
] as const;
const claudeThinkingEffortOptions = [
{ id: "", label: "Auto" },
{ id: "low", label: "Low" },
@@ -267,7 +274,8 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
const isLocal =
adapterType === "claude_local" ||
adapterType === "codex_local" ||
adapterType === "opencode_local";
adapterType === "opencode_local" ||
adapterType === "cursor";
const uiAdapter = useMemo(() => getUIAdapter(adapterType), [adapterType]);
// Fetch adapter models for the effective adapter type
@@ -329,12 +337,16 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
const thinkingEffortKey =
adapterType === "codex_local"
? "modelReasoningEffort"
: adapterType === "cursor"
? "mode"
: adapterType === "opencode_local"
? "variant"
: "effort";
const thinkingEffortOptions =
adapterType === "codex_local"
? codexThinkingEffortOptions
: adapterType === "cursor"
? cursorModeOptions
: adapterType === "opencode_local"
? opencodeVariantOptions
: claudeThinkingEffortOptions;
@@ -346,6 +358,8 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
"modelReasoningEffort",
String(config.modelReasoningEffort ?? config.reasoningEffort ?? ""),
)
: adapterType === "cursor"
? eff("adapterConfig", "mode", String(config.mode ?? ""))
: adapterType === "opencode_local"
? eff("adapterConfig", "variant", String(config.variant ?? ""))
: eff("adapterConfig", "effort", String(config.effort ?? ""));
@@ -466,12 +480,14 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
nextValues.model = DEFAULT_CODEX_LOCAL_MODEL;
nextValues.dangerouslyBypassSandbox =
DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX;
} else if (t === "cursor") {
nextValues.model = DEFAULT_CURSOR_LOCAL_MODEL;
} else if (t === "opencode_local") {
nextValues.model = DEFAULT_OPENCODE_LOCAL_MODEL;
}
set!(nextValues);
} else {
// Clear all adapter config and explicitly blank out model + both effort keys
// Clear all adapter config and explicitly blank out model + effort/mode keys
// so the old adapter's values don't bleed through via eff()
setOverlay((prev) => ({
...prev,
@@ -480,12 +496,15 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
model:
t === "codex_local"
? DEFAULT_CODEX_LOCAL_MODEL
: t === "cursor"
? DEFAULT_CURSOR_LOCAL_MODEL
: t === "opencode_local"
? DEFAULT_OPENCODE_LOCAL_MODEL
: "",
effort: "",
modelReasoningEffort: "",
variant: "",
mode: "",
...(t === "codex_local"
? {
dangerouslyBypassApprovalsAndSandbox:
@@ -584,6 +603,8 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
placeholder={
adapterType === "codex_local"
? "codex"
: adapterType === "cursor"
? "agent"
: adapterType === "opencode_local"
? "opencode"
: "claude"
@@ -855,7 +876,7 @@ function AdapterEnvironmentResult({ result }: { result: AdapterEnvironmentTestRe
/* ---- Internal sub-components ---- */
const ENABLED_ADAPTER_TYPES = new Set(["claude_local", "codex_local", "opencode_local"]);
const ENABLED_ADAPTER_TYPES = new Set(["claude_local", "codex_local", "opencode_local", "cursor"]);
/** Display list includes all real adapter types plus UI-only coming-soon entries. */
const ADAPTER_DISPLAY_LIST: { value: string; label: string; comingSoon: boolean }[] = [
@@ -864,7 +885,6 @@ const ADAPTER_DISPLAY_LIST: { value: string; label: string; comingSoon: boolean
label: adapterLabels[t] ?? t,
comingSoon: !ENABLED_ADAPTER_TYPES.has(t),
})),
{ value: "cursor", label: "Cursor", comingSoon: true },
];
function AdapterTypeDropdown({

View File

@@ -19,7 +19,7 @@ const adapterLabels: Record<string, string> = {
codex_local: "Codex (local)",
opencode_local: "OpenCode (local)",
openclaw: "OpenClaw",
cursor: "Cursor",
cursor: "Cursor (local)",
process: "Process",
http: "HTTP",
};

View File

@@ -23,6 +23,7 @@ import {
DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX,
DEFAULT_CODEX_LOCAL_MODEL
} from "@paperclipai/adapter-codex-local";
import { DEFAULT_CURSOR_LOCAL_MODEL } from "@paperclipai/adapter-cursor-local";
import { DEFAULT_OPENCODE_LOCAL_MODEL } from "@paperclipai/adapter-opencode-local";
import { AsciiArtAnimation } from "./AsciiArtAnimation";
import { ChoosePathButton } from "./PathInstructionsModal";
@@ -51,6 +52,7 @@ type AdapterType =
| "claude_local"
| "codex_local"
| "opencode_local"
| "cursor"
| "process"
| "http"
| "openclaw";
@@ -153,11 +155,13 @@ export function OnboardingWizard() {
enabled: onboardingOpen && step === 2
});
const isLocalAdapter =
adapterType === "claude_local" || adapterType === "codex_local" || adapterType === "opencode_local";
adapterType === "claude_local" || adapterType === "codex_local" || adapterType === "opencode_local" || adapterType === "cursor";
const effectiveAdapterCommand =
command.trim() ||
(adapterType === "codex_local"
? "codex"
: adapterType === "cursor"
? "agent"
: adapterType === "opencode_local"
? "opencode"
: "claude");
@@ -219,6 +223,8 @@ export function OnboardingWizard() {
model:
adapterType === "codex_local"
? model || DEFAULT_CODEX_LOCAL_MODEL
: adapterType === "cursor"
? model || DEFAULT_CURSOR_LOCAL_MODEL
: adapterType === "opencode_local"
? model || DEFAULT_OPENCODE_LOCAL_MODEL
: model,
@@ -598,8 +604,7 @@ export function OnboardingWizard() {
value: "cursor" as const,
label: "Cursor",
icon: MousePointer2,
desc: "Cursor AI agent",
comingSoon: true
desc: "Local Cursor agent"
},
{
value: "process" as const,
@@ -633,6 +638,8 @@ export function OnboardingWizard() {
setAdapterType(nextType);
if (nextType === "codex_local" && !model) {
setModel(DEFAULT_CODEX_LOCAL_MODEL);
} else if (nextType === "cursor" && !model) {
setModel(DEFAULT_CURSOR_LOCAL_MODEL);
} else if (nextType === "opencode_local" && !model) {
setModel(DEFAULT_OPENCODE_LOCAL_MODEL);
}
@@ -656,7 +663,8 @@ export function OnboardingWizard() {
{/* Conditional adapter fields */}
{(adapterType === "claude_local" ||
adapterType === "codex_local" ||
adapterType === "opencode_local") && (
adapterType === "opencode_local" ||
adapterType === "cursor") && (
<div className="space-y-3">
<div>
<div className="flex items-center gap-1.5 mb-1">
@@ -789,7 +797,9 @@ export function OnboardingWizard() {
<div className="rounded-md border border-border/70 bg-muted/20 px-2.5 py-2 text-[11px] space-y-1.5">
<p className="font-medium">Manual debug</p>
<p className="text-muted-foreground font-mono break-all">
{adapterType === "codex_local"
{adapterType === "cursor"
? `${effectiveAdapterCommand} -p --mode ask --output-format json \"Respond with hello.\"`
: adapterType === "codex_local"
? `${effectiveAdapterCommand} exec --json -`
: adapterType === "opencode_local"
? `${effectiveAdapterCommand} run --format json \"Respond with hello.\"`
@@ -799,13 +809,20 @@ export function OnboardingWizard() {
Prompt:{" "}
<span className="font-mono">Respond with hello.</span>
</p>
{adapterType === "codex_local" || adapterType === "opencode_local" ? (
{adapterType === "cursor" || adapterType === "codex_local" || adapterType === "opencode_local" ? (
<p className="text-muted-foreground">
If auth fails, set{" "}
<span className="font-mono">OPENAI_API_KEY</span> in
<span className="font-mono">
{adapterType === "cursor" ? "CURSOR_API_KEY" : "OPENAI_API_KEY"}
</span>{" "}
in
env or run{" "}
<span className="font-mono">
{adapterType === "codex_local" ? "codex login" : "opencode auth login"}
{adapterType === "cursor"
? "agent login"
: adapterType === "codex_local"
? "codex login"
: "opencode auth login"}
</span>.
</p>
) : (

View File

@@ -54,7 +54,7 @@ export const adapterLabels: Record<string, string> = {
codex_local: "Codex (local)",
opencode_local: "OpenCode (local)",
openclaw: "OpenClaw",
cursor: "Cursor",
cursor: "Cursor (local)",
process: "Process",
http: "HTTP",
};

View File

@@ -24,6 +24,7 @@ const adapterLabels: Record<string, string> = {
claude_local: "Claude",
codex_local: "Codex",
opencode_local: "OpenCode",
cursor: "Cursor",
openclaw: "OpenClaw",
process: "Process",
http: "HTTP",

View File

@@ -20,12 +20,12 @@ const adapterLabels: Record<string, string> = {
codex_local: "Codex (local)",
opencode_local: "OpenCode (local)",
openclaw: "OpenClaw",
cursor: "Cursor",
cursor: "Cursor (local)",
process: "Process",
http: "HTTP",
};
const ENABLED_INVITE_ADAPTERS = new Set(["claude_local", "codex_local", "opencode_local"]);
const ENABLED_INVITE_ADAPTERS = new Set(["claude_local", "codex_local", "opencode_local", "cursor"]);
function dateTime(value: string) {
return new Date(value).toLocaleString();

View File

@@ -119,6 +119,7 @@ const adapterLabels: Record<string, string> = {
claude_local: "Claude",
codex_local: "Codex",
opencode_local: "OpenCode",
cursor: "Cursor",
openclaw: "OpenClaw",
process: "Process",
http: "HTTP",