Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,10 @@ One module resolves aliases, defaults, path resolution, platform fallbacks, and
| `CURSOR_BRIDGE_API_KEY` | — | If set, require `Authorization: Bearer <key>` 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 |
Expand Down
9 changes: 9 additions & 0 deletions src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
14 changes: 14 additions & 0 deletions src/lib/env.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
7 changes: 7 additions & 0 deletions src/lib/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down
1 change: 1 addition & 0 deletions src/lib/handlers/chat-completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
1 change: 1 addition & 0 deletions src/lib/handlers/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
76 changes: 76 additions & 0 deletions src/lib/resolve-mode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
56 changes: 56 additions & 0 deletions src/lib/resolve-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand All @@ -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;
}
86 changes: 86 additions & 0 deletions src/lib/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>((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<void>((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<void>((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",
Expand Down