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,184 @@
import { describe, expect, it, vi } from "vitest";
import { isCursorUnknownSessionError, parseCursorJsonl } from "@paperclipai/adapter-cursor-local/server";
import { parseCursorStdoutLine } from "@paperclipai/adapter-cursor-local/ui";
import { printCursorStreamEvent } from "@paperclipai/adapter-cursor-local/cli";
describe("cursor parser", () => {
it("extracts session, summary, usage, cost, and terminal error message", () => {
const stdout = [
JSON.stringify({ type: "system", subtype: "init", session_id: "chat_123", model: "gpt-5" }),
JSON.stringify({
type: "assistant",
message: {
content: [{ type: "output_text", text: "hello" }],
},
}),
JSON.stringify({
type: "result",
subtype: "success",
session_id: "chat_123",
usage: {
input_tokens: 100,
cached_input_tokens: 25,
output_tokens: 40,
},
total_cost_usd: 0.001,
result: "Task complete",
}),
JSON.stringify({ type: "error", message: "model access denied" }),
].join("\n");
const parsed = parseCursorJsonl(stdout);
expect(parsed.sessionId).toBe("chat_123");
expect(parsed.summary).toBe("hello");
expect(parsed.usage).toEqual({
inputTokens: 100,
cachedInputTokens: 25,
outputTokens: 40,
});
expect(parsed.costUsd).toBeCloseTo(0.001, 6);
expect(parsed.errorMessage).toBe("model access denied");
});
});
describe("cursor stale session detection", () => {
it("treats missing/unknown session messages as an unknown session error", () => {
expect(isCursorUnknownSessionError("", "unknown session id chat_123")).toBe(true);
expect(isCursorUnknownSessionError("", "chat abc not found")).toBe(true);
});
});
describe("cursor ui stdout parser", () => {
it("parses assistant, thinking, and tool lifecycle events", () => {
const ts = "2026-03-05T00:00:00.000Z";
expect(
parseCursorStdoutLine(
JSON.stringify({
type: "assistant",
message: {
content: [
{ type: "output_text", text: "I will run a command." },
{ type: "thinking", text: "Checking repository state" },
{ type: "tool_call", name: "bash", input: { command: "ls -1" } },
{ type: "tool_result", tool_use_id: "tool_1", output: "AGENTS.md\n", status: "ok" },
],
},
}),
ts,
),
).toEqual([
{ kind: "assistant", ts, text: "I will run a command." },
{ kind: "thinking", ts, text: "Checking repository state" },
{ kind: "tool_call", ts, name: "bash", input: { command: "ls -1" } },
{ kind: "tool_result", ts, toolUseId: "tool_1", content: "AGENTS.md\n", isError: false },
]);
});
it("parses result usage and errors", () => {
const ts = "2026-03-05T00:00:00.000Z";
expect(
parseCursorStdoutLine(
JSON.stringify({
type: "result",
subtype: "success",
result: "Done",
usage: {
input_tokens: 10,
output_tokens: 5,
cached_input_tokens: 2,
},
total_cost_usd: 0.00042,
is_error: false,
}),
ts,
),
).toEqual([
{
kind: "result",
ts,
text: "Done",
inputTokens: 10,
outputTokens: 5,
cachedTokens: 2,
costUsd: 0.00042,
subtype: "success",
isError: false,
errors: [],
},
]);
});
});
function stripAnsi(value: string): string {
return value.replace(/\x1b\[[0-9;]*m/g, "");
}
describe("cursor cli formatter", () => {
it("prints init, assistant, tool, and result events", () => {
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
try {
printCursorStreamEvent(
JSON.stringify({ type: "system", subtype: "init", session_id: "chat_abc", model: "gpt-5" }),
false,
);
printCursorStreamEvent(
JSON.stringify({
type: "assistant",
message: {
content: [{ type: "output_text", text: "hello" }],
},
}),
false,
);
printCursorStreamEvent(
JSON.stringify({
type: "assistant",
message: {
content: [{ type: "tool_call", name: "bash", input: { command: "ls -1" } }],
},
}),
false,
);
printCursorStreamEvent(
JSON.stringify({
type: "assistant",
message: {
content: [{ type: "tool_result", output: "AGENTS.md", status: "ok" }],
},
}),
false,
);
printCursorStreamEvent(
JSON.stringify({
type: "result",
subtype: "success",
result: "Done",
usage: { input_tokens: 10, output_tokens: 5, cached_input_tokens: 2 },
total_cost_usd: 0.00042,
}),
false,
);
const lines = spy.mock.calls
.map((call) => call.map((v) => String(v)).join(" "))
.map(stripAnsi);
expect(lines).toEqual(
expect.arrayContaining([
"Cursor init (session: chat_abc, model: gpt-5)",
"assistant: hello",
"tool_call: bash",
"tool_result",
"AGENTS.md",
"result: subtype=success",
"tokens: in=10 out=5 cached=2 cost=$0.000420",
"assistant: Done",
]),
);
} finally {
spy.mockRestore();
}
});
});