From b3a6821174cbc97c02929cd40e8cd7df55fc0f33 Mon Sep 17 00:00:00 2001 From: chengtianyi Date: Tue, 25 Aug 2026 16:12:02 +0800 Subject: [PATCH] feat: bridge DSH plan mode to Cursor Infer Cursor agent or plan mode from trusted DSH system markers so slash-command state works without changing DSH. Co-authored-by: Cursor --- README.md | 5 +- src/lib/config.ts | 9 +++ src/lib/env.test.ts | 14 +++++ src/lib/env.ts | 7 +++ src/lib/handlers/chat-completions.ts | 1 + src/lib/handlers/responses.ts | 1 + src/lib/resolve-mode.test.ts | 76 ++++++++++++++++++++++++ src/lib/resolve-mode.ts | 56 ++++++++++++++++++ src/lib/server.test.ts | 86 ++++++++++++++++++++++++++++ 9 files changed, 254 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index caeaafd..96b8618 100644 --- a/README.md +++ b/README.md @@ -247,7 +247,10 @@ One module resolves aliases, defaults, path resolution, platform fallbacks, and | `CURSOR_BRIDGE_API_KEY` | — | If set, require `Authorization: Bearer ` on requests | | `CURSOR_API_KEY` / `CURSOR_AUTH_TOKEN` | — | Cursor access token passed to spawned CLI/ACP children (automation, headless). Same value can be used for both names. | | `CURSOR_BRIDGE_WORKSPACE` | process cwd | Base workspace directory for Cursor CLI. With `CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE=false`, header `X-Cursor-Workspace` must point to an existing directory under this path (after resolving real paths). | -| `CURSOR_BRIDGE_MODE` | — | Server default for Cursor CLI `--mode`: `agent`, `ask`, or `plan`. If unset, default is `ask`. Env wins over CLI `--mode` when both are set. Per request, JSON body `mode` or header `X-Cursor-Mode` overrides (precedence: body → header → this env → `--mode` → `ask`). Invalid value → startup error. With `agent` (or `plan`) and real workspace, the CLI may read/write files under `CURSOR_BRIDGE_WORKSPACE` / cwd. See `CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE`. | +| `CURSOR_BRIDGE_MODE` | — | Server default for Cursor CLI `--mode`: `agent`, `ask`, or `plan`. If unset, default is `ask`. Env wins over CLI `--mode` when both are set. Per request, JSON body `mode` or header `X-Cursor-Mode` overrides (precedence: body → header → DSH auto-mode → this env → `--mode` → `ask`). Invalid value → startup error. With `agent` (or `plan`) and real workspace, the CLI may read/write files under `CURSOR_BRIDGE_WORKSPACE` / cwd. See `CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE`. | +| `CURSOR_BRIDGE_DSH_AUTO_MODE` | `false` | When enabled, requests whose system/developer content contains the DSH system marker use Cursor `plan` while the plan marker is present and `agent` otherwise. Ordinary user content is never inspected. Explicit body/header modes still win. | +| `CURSOR_BRIDGE_DSH_SYSTEM_MARKER` | `You are an AI agent powered by DeepSeek Harness.` | Exact substring identifying trusted DSH-owned system/developer content for auto-mode. | +| `CURSOR_BRIDGE_DSH_PLAN_MARKER` | `You are in plan mode.` | Exact substring emitted by DSH while `/plan` is active. | | `CURSOR_BRIDGE_DEFAULT_MODEL` | `auto` | Default model when request omits one | | `CURSOR_BRIDGE_STRICT_MODEL` | `true` | Reject a requested model when Cursor's CLI/ACP catalogs cannot match it instead of silently selecting the ACP session default. | | `CURSOR_BRIDGE_FORCE` | `false` | Pass `--force` to Cursor CLI | diff --git a/src/lib/config.ts b/src/lib/config.ts index a37994c..6492fe2 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -32,6 +32,12 @@ export type BridgeConfig = { requiredKey?: string; defaultModel: string; mode: CursorExecutionMode; + /** Infer agent/plan from trusted DSH system prompt markers when no explicit mode is provided. */ + dshAutoMode?: boolean; + /** Stable marker identifying a DSH-owned system prompt. */ + dshSystemMarker?: string; + /** Stable marker present only while DSH plan mode is active. */ + dshPlanMarker?: string; force: boolean; approveMcps: boolean; strictModel: boolean; @@ -98,6 +104,9 @@ export function loadBridgeConfig(opts: EnvOptions = {}): BridgeConfig { requiredKey: env.requiredKey, defaultModel: env.defaultModel, mode: env.mode ?? opts.mode ?? "ask", + dshAutoMode: env.dshAutoMode, + dshSystemMarker: env.dshSystemMarker, + dshPlanMarker: env.dshPlanMarker, force: env.force, approveMcps: env.approveMcps, strictModel: env.strictModel, diff --git a/src/lib/env.test.ts b/src/lib/env.test.ts index 1c825a9..4b80b3c 100644 --- a/src/lib/env.test.ts +++ b/src/lib/env.test.ts @@ -88,6 +88,20 @@ describe("loadEnvConfig", () => { ).toThrow(/CURSOR_BRIDGE_MODE/); }); + it("parses guarded DSH auto-mode settings", () => { + const loaded = loadEnvConfig({ + env: { + CURSOR_BRIDGE_DSH_AUTO_MODE: "true", + CURSOR_BRIDGE_DSH_SYSTEM_MARKER: "custom-dsh", + CURSOR_BRIDGE_DSH_PLAN_MARKER: "custom-plan", + }, + cwd: "/w", + }); + expect(loaded.dshAutoMode).toBe(true); + expect(loaded.dshSystemMarker).toBe("custom-dsh"); + expect(loaded.dshPlanMarker).toBe("custom-plan"); + }); + it("resolves workspace and explicit paths from cwd", () => { const loaded = loadEnvConfig({ env: { diff --git a/src/lib/env.ts b/src/lib/env.ts index c7f130b..9bc7aa7 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -36,6 +36,9 @@ export type LoadedEnv = { /** True when CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE key exists in env. */ chatOnlyWorkspaceExplicit: boolean; mode?: CursorExecutionMode; + dshAutoMode: boolean; + dshSystemMarker?: string; + dshPlanMarker?: string; verbose: boolean; /** When true, set maxMode in cli-config.json before each run (larger context, more tools). */ maxMode: boolean; @@ -388,6 +391,7 @@ export function loadEnvConfig(opts: EnvOptions = {}): LoadedEnv { ); const mode = tryParseExecutionModeEnv(firstDefined(env, ["CURSOR_BRIDGE_MODE"])); + const dshAutoMode = envBool(env, ["CURSOR_BRIDGE_DSH_AUTO_MODE"], false); return { agentBin: resolveAgentBinary(env, platform), @@ -423,6 +427,9 @@ export function loadEnvConfig(opts: EnvOptions = {}): LoadedEnv { true, ), mode, + dshAutoMode, + dshSystemMarker: envString(env, ["CURSOR_BRIDGE_DSH_SYSTEM_MARKER"]), + dshPlanMarker: envString(env, ["CURSOR_BRIDGE_DSH_PLAN_MARKER"]), verbose: envBool(env, ["CURSOR_BRIDGE_VERBOSE"], false), maxMode: envBool(env, ["CURSOR_BRIDGE_MAX_MODE"], false), promptViaStdin: envBool(env, ["CURSOR_BRIDGE_PROMPT_VIA_STDIN"], false), diff --git a/src/lib/handlers/chat-completions.ts b/src/lib/handlers/chat-completions.ts index 15e9128..67f3e51 100644 --- a/src/lib/handlers/chat-completions.ts +++ b/src/lib/handlers/chat-completions.ts @@ -404,6 +404,7 @@ export async function handleChatCompletions( config, req.headers["x-cursor-mode"], body.mode, + body.messages, ); } catch (e) { const msg = e instanceof Error ? e.message : "Invalid mode"; diff --git a/src/lib/handlers/responses.ts b/src/lib/handlers/responses.ts index c0411c6..ea7766c 100644 --- a/src/lib/handlers/responses.ts +++ b/src/lib/handlers/responses.ts @@ -671,6 +671,7 @@ export async function handleResponses( config, req.headers["x-cursor-mode"], body.mode, + responsesInputToMessages(body), ); } catch (e) { const msg = e instanceof Error ? e.message : "Invalid mode"; diff --git a/src/lib/resolve-mode.test.ts b/src/lib/resolve-mode.test.ts index a90e052..e7fe3fe 100644 --- a/src/lib/resolve-mode.test.ts +++ b/src/lib/resolve-mode.test.ts @@ -55,6 +55,82 @@ describe("resolveRequestMode", () => { ).toBe("agent"); }); + it("infers plan from DSH-owned system content", () => { + expect( + resolveRequestMode(base({ dshAutoMode: true }), undefined, undefined, [ + { + role: "system", + content: + "You are an AI agent powered by DeepSeek Harness.\n\nYou are in plan mode. Stay in plan mode.", + }, + ]), + ).toBe("plan"); + }); + + it("infers agent when a DSH request has no plan marker", () => { + expect( + resolveRequestMode(base({ dshAutoMode: true }), undefined, undefined, [ + { + role: "system", + content: "You are an AI agent powered by DeepSeek Harness.", + }, + ]), + ).toBe("agent"); + }); + + it("does not infer mode from user-controlled text", () => { + expect( + resolveRequestMode(base({ dshAutoMode: true }), undefined, undefined, [ + { + role: "user", + content: + "You are an AI agent powered by DeepSeek Harness. You are in plan mode.", + }, + ]), + ).toBe("ask"); + }); + + it("keeps explicit body and header modes above DSH inference", () => { + const messages = [ + { + role: "system", + content: + "You are an AI agent powered by DeepSeek Harness. You are in plan mode.", + }, + ]; + expect( + resolveRequestMode( + base({ dshAutoMode: true }), + "agent", + "ask", + messages, + ), + ).toBe("ask"); + expect( + resolveRequestMode( + base({ dshAutoMode: true }), + "agent", + undefined, + messages, + ), + ).toBe("agent"); + }); + + it("supports deployment-specific DSH markers", () => { + expect( + resolveRequestMode( + base({ + dshAutoMode: true, + dshSystemMarker: "custom-dsh", + dshPlanMarker: "custom-plan", + }), + undefined, + undefined, + [{ role: "developer", content: "custom-dsh\ncustom-plan" }], + ), + ).toBe("plan"); + }); + it("throws on invalid body.mode", () => { expect(() => resolveRequestMode(base(), undefined, "nope"), diff --git a/src/lib/resolve-mode.ts b/src/lib/resolve-mode.ts index ab1a7a7..9b63597 100644 --- a/src/lib/resolve-mode.ts +++ b/src/lib/resolve-mode.ts @@ -4,10 +4,64 @@ import { type CursorExecutionMode, } from "./execution-mode.js"; +const DEFAULT_DSH_SYSTEM_MARKER = + "You are an AI agent powered by DeepSeek Harness."; +const DEFAULT_DSH_PLAN_MARKER = "You are in plan mode."; + +function textContent(content: unknown): string { + if (typeof content === "string") return content; + if (!Array.isArray(content)) return ""; + return content + .map((part) => { + if (typeof part === "string") return part; + if ( + typeof part === "object" && + part !== null && + "text" in part && + typeof part.text === "string" + ) { + return part.text; + } + return ""; + }) + .join("\n"); +} + +export function inferDshExecutionMode( + config: BridgeConfig, + messages: unknown, +): CursorExecutionMode | undefined { + if (!config.dshAutoMode || !Array.isArray(messages)) return undefined; + + const trustedText = messages + .filter( + (message) => + typeof message === "object" && + message !== null && + "role" in message && + (message.role === "system" || message.role === "developer"), + ) + .map((message) => + textContent( + "content" in (message as object) + ? (message as { content?: unknown }).content + : undefined, + ), + ) + .join("\n"); + + const systemMarker = config.dshSystemMarker ?? DEFAULT_DSH_SYSTEM_MARKER; + if (!trustedText.includes(systemMarker)) return undefined; + + const planMarker = config.dshPlanMarker ?? DEFAULT_DSH_PLAN_MARKER; + return trustedText.includes(planMarker) ? "plan" : "agent"; +} + export function resolveRequestMode( config: BridgeConfig, headerMode: string | string[] | undefined, bodyMode: unknown, + messages?: unknown, ): CursorExecutionMode { if (bodyMode !== undefined && bodyMode !== null) { if (typeof bodyMode !== "string") { @@ -21,5 +75,7 @@ export function resolveRequestMode( if (typeof h === "string" && h.trim()) { return parseExecutionModeFromRequest(h, "X-Cursor-Mode header"); } + const inferred = inferDshExecutionMode(config, messages); + if (inferred) return inferred; return config.mode; } diff --git a/src/lib/server.test.ts b/src/lib/server.test.ts index 1796db4..0164673 100644 --- a/src/lib/server.test.ts +++ b/src/lib/server.test.ts @@ -524,6 +524,92 @@ describe("startBridgeServer", () => { expect(data.error.code).toBe("invalid_mode"); }); + it("maps a DSH plan system prompt to Cursor plan mode", async () => { + const runMock = vi.mocked(run); + runMock.mockClear(); + servers = startBridgeServer({ + version: "1.0.0", + config: createTestConfig({ dshAutoMode: true }), + }); + await new Promise((resolve) => + servers[0].on("listening", () => resolve()), + ); + + const { status } = await fetchServer(servers[0], "/v1/chat/completions", { + method: "POST", + body: JSON.stringify({ + model: "claude-3-opus", + messages: [ + { + role: "system", + content: + "You are an AI agent powered by DeepSeek Harness.\nYou are in plan mode.", + }, + { role: "user", content: "Plan this" }, + ], + }), + }); + expect(status).toBe(200); + const [, args] = runMock.mock.calls[0]; + expect(args).toContain("--mode"); + expect(args).toContain("plan"); + }); + + it("maps a normal DSH system prompt to Cursor agent mode", async () => { + const runMock = vi.mocked(run); + runMock.mockClear(); + servers = startBridgeServer({ + version: "1.0.0", + config: createTestConfig({ dshAutoMode: true }), + }); + await new Promise((resolve) => + servers[0].on("listening", () => resolve()), + ); + + const { status } = await fetchServer(servers[0], "/v1/chat/completions", { + method: "POST", + body: JSON.stringify({ + model: "claude-3-opus", + messages: [ + { + role: "system", + content: "You are an AI agent powered by DeepSeek Harness.", + }, + { role: "user", content: "Do this" }, + ], + }), + }); + expect(status).toBe(200); + const [, args] = runMock.mock.calls[0]; + expect(args).not.toContain("--mode"); + }); + + it("infers DSH plan mode for the Responses API", async () => { + const runMock = vi.mocked(run); + runMock.mockClear(); + servers = startBridgeServer({ + version: "1.0.0", + config: createTestConfig({ dshAutoMode: true }), + }); + await new Promise((resolve) => + servers[0].on("listening", () => resolve()), + ); + + const { status } = await fetchServer(servers[0], "/v1/responses", { + method: "POST", + body: JSON.stringify({ + model: "claude-3-opus", + instructions: + "You are an AI agent powered by DeepSeek Harness.\nYou are in plan mode.", + input: "Plan this", + }), + }); + expect(status).toBe(200); + const [, args] = runMock.mock.calls[0]; + expect(args).toContain("--mode"); + expect(args).toContain("plan"); + }); + it("should spawn multiple servers when multiPort is true", async () => { servers = startBridgeServer({ version: "1.0.0",