Add a supervised Cursor CLI runtime for routed Cursor model sessions. - Stream Cursor prompt output into Fusion session callbacks with resume support. - Support Windows Cursor shims, inactivity-based supervision, and cursor-agent to cursor fallback execution. - Route Cursor CLI models through the bundled plugin and document the runtime contract. Files changed: .changeset/fn-9097-cursor-cli-runtime.md | 7 ++ docs/cursor-cli-contract.md | 21 ++++ docs/settings-reference.md | 4 +- .../cli-runtime-routing-conformance.test.ts | 11 +- .../engine/src/agents/agent-session-helpers.ts | 6 +- packages/engine/src/agents/cli-provider-routing.ts | 27 ++--- plugins/fusion-plugin-cursor-runtime/README.md | 21 ++++ plugins/fusion-plugin-cursor-runtime/package.json | 1 + .../src/__tests__/cli-spawn.test.ts | 25 ++++- .../src/__tests__/prompt-transport.test.ts | 58 ++++++++++ .../src/__tests__/runtime-adapter.test.ts | 44 +++++--- .../src/__tests__/stream-parser.test.ts | 12 +++ .../fusion-plugin-cursor-runtime/src/cli-spawn.ts | 57 +++++++++- plugins/fusion-plugin-cursor-runtime/src/index.ts | 4 +- .../src/prompt-transport.ts | 120 +++++++++++++++++++++ .../src/runtime-adapter.ts | 59 ++++++---- .../src/stream-parser.ts | 43 ++++++++ plugins/fusion-plugin-cursor-runtime/src/types.ts | 18 ++-- pnpm-lock.yaml | 3 + 19 files changed, 458 insertions(+), 83 deletions(-) Fusion-Task-Id: FN-9097 Fusion-Task-Lineage: a4eed861-6059-4528-9f80-3426f5ccad58 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
44 lines
2.9 KiB
TypeScript
44 lines
2.9 KiB
TypeScript
export type CursorStreamEvent =
|
|
| { kind: "system-init"; sessionId?: string; cwd?: string; model?: string }
|
|
| { kind: "thinking-delta"; text: string }
|
|
| { kind: "assistant-text"; text: string }
|
|
| { kind: "tool-call-started" | "tool-call-completed"; callId?: string; name: string; args?: Record<string, unknown>; result?: unknown }
|
|
| { kind: "result"; sessionId?: string; text?: string; isError: boolean; usage?: unknown }
|
|
| { kind: "unknown" };
|
|
|
|
/*
|
|
FNXC:CursorCli 2026-08-15-15:16:
|
|
Cursor's verified stream-json fixture has one JSON object per line, but tool call ids
|
|
can contain newlines after parsing. Keep ids opaque and make malformed stream data a
|
|
non-fatal unknown event so a partial CLI line cannot crash the engine.
|
|
*/
|
|
export function parseCursorStreamLine(line: string): CursorStreamEvent {
|
|
if (!line.trim()) return { kind: "unknown" };
|
|
let value: Record<string, unknown>;
|
|
try {
|
|
const parsed: unknown = JSON.parse(line);
|
|
if (!parsed || typeof parsed !== "object") return { kind: "unknown" };
|
|
value = parsed as Record<string, unknown>;
|
|
} catch { return { kind: "unknown" }; }
|
|
const type = value.type;
|
|
const sessionId = typeof value.session_id === "string" ? value.session_id : undefined;
|
|
if (type === "system" && value.subtype === "init") return { kind: "system-init", sessionId, cwd: typeof value.cwd === "string" ? value.cwd : undefined, model: typeof value.model === "string" ? value.model : undefined };
|
|
if (type === "thinking" && value.subtype === "delta" && typeof value.text === "string") return { kind: "thinking-delta", text: value.text };
|
|
if (type === "assistant") {
|
|
const message = value.message as { content?: unknown } | undefined;
|
|
const content = Array.isArray(message?.content) ? message.content : [];
|
|
const text = content.filter((item): item is { text: string } => Boolean(item) && typeof item === "object" && typeof (item as { text?: unknown }).text === "string").map((item) => item.text).join("");
|
|
return text ? { kind: "assistant-text", text } : { kind: "unknown" };
|
|
}
|
|
if (type === "tool_call" && (value.subtype === "started" || value.subtype === "completed")) {
|
|
const call = value.tool_call as Record<string, unknown> | undefined;
|
|
const key = call && Object.keys(call).find((name) => name.endsWith("ToolCall"));
|
|
if (!key) return { kind: "unknown" };
|
|
const details = call[key] as Record<string, unknown> | undefined;
|
|
const name = key.slice(0, -"ToolCall".length) || "unknown";
|
|
return { kind: value.subtype === "started" ? "tool-call-started" : "tool-call-completed", callId: typeof value.call_id === "string" ? value.call_id : undefined, name, args: details?.args && typeof details.args === "object" ? details.args as Record<string, unknown> : undefined, result: details?.result };
|
|
}
|
|
if (type === "result") return { kind: "result", sessionId, text: typeof value.result === "string" ? value.result : undefined, isError: value.is_error === true, usage: value.usage };
|
|
return { kind: "unknown" };
|
|
}
|