mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-07 08:54:42 +00:00
fix(cursor): show plan usage in Cursor agents' context details (#6353)
Cursor agents never showed plan usage cards under their context details. ACP sessions had no usageSession(), and the Cursor usage source only ran global discovery, so per-agent discovery always came back empty. Cursor ACP sessions now report a usage session (provider "cursor" plus their launch env). The Cursor usage source resolves the cursor-agent auth.json for that env. Sessions on CURSOR_API_KEY or CURSOR_AUTH_TOKEN, the in-memory store, or the macOS Keychain (the default there) report no login.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import type { PluginServerContext } from "@getpaseo/plugin/server";
|
||||
import { inputSchema } from "./shared/input.js";
|
||||
import { fetchUsage, discover } from "./server/usage.js";
|
||||
import { fetchUsage, discover, discoverSession } from "./server/usage.js";
|
||||
|
||||
export default function contribute(server: PluginServerContext) {
|
||||
server.registerUsageSource({
|
||||
@@ -8,7 +8,7 @@ export default function contribute(server: PluginServerContext) {
|
||||
label: "Cursor",
|
||||
icon: "icon.svg",
|
||||
input: inputSchema,
|
||||
discover: (scope) => (scope.kind === "global" ? discover() : Promise.resolve([])),
|
||||
discover: (scope) => (scope.kind === "global" ? discover() : discoverSession(scope)),
|
||||
fetch: fetchUsage,
|
||||
});
|
||||
return () => {};
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { discover, fetchUsage } from "./usage.js";
|
||||
import { discover, discoverSession, fetchUsage } from "./usage.js";
|
||||
import type { UsageReport } from "@getpaseo/plugin/server/usage";
|
||||
|
||||
// node:sqlite has no @types/node@20 typings; require it with a narrow local type.
|
||||
@@ -256,6 +256,78 @@ describe("cursor usage source", () => {
|
||||
writeFileSync(join(directory, "state.vscdb"), "invalid database");
|
||||
expect(await discover()).toEqual([]);
|
||||
});
|
||||
|
||||
describe("session discovery", () => {
|
||||
let sessionHome: string;
|
||||
beforeEach(() => {
|
||||
sessionHome = mkdtempSync(join(tmpdir(), "usage-session-home-"));
|
||||
});
|
||||
afterEach(() => {
|
||||
rmSync(sessionHome, { recursive: true, force: true });
|
||||
});
|
||||
function cursorSession(env: Record<string, string>, provider = "cursor") {
|
||||
return { kind: "session" as const, provider, env: { HOME: sessionHome, ...env } };
|
||||
}
|
||||
function writeAuthJson(path: string): void {
|
||||
mkdirSync(join(path, ".."), { recursive: true });
|
||||
writeFileSync(path, JSON.stringify({ accessToken: "session-token" }));
|
||||
}
|
||||
|
||||
it("reads the login cursor-agent uses under the session's XDG_CONFIG_HOME", async () => {
|
||||
const configHome = join(sessionHome, "xdg");
|
||||
const authPath = join(configHome, "cursor", "auth.json");
|
||||
writeAuthJson(authPath);
|
||||
expect(
|
||||
await discoverSession(cursorSession({ XDG_CONFIG_HOME: configHome }), "linux"),
|
||||
).toEqual([{ key: "default", input: { store: "file", locator: authPath } }]);
|
||||
});
|
||||
|
||||
it("reads the login under the session's HOME, not the daemon's", async () => {
|
||||
writeCursorAuthJson(homeDir, "daemon-token");
|
||||
writeCursorAuthJson(sessionHome, "session-token");
|
||||
expect(await discoverSession(cursorSession({}), "linux")).toEqual([
|
||||
{
|
||||
key: "default",
|
||||
input: { store: "file", locator: join(sessionHome, ".config", "cursor", "auth.json") },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("reads %APPDATA%/Cursor/auth.json on Windows", async () => {
|
||||
const appData = join(sessionHome, "AppData", "Roaming");
|
||||
const authPath = join(appData, "Cursor", "auth.json");
|
||||
writeAuthJson(authPath);
|
||||
expect(await discoverSession(cursorSession({ APPDATA: appData }), "win32")).toEqual([
|
||||
{ key: "default", input: { store: "file", locator: authPath } },
|
||||
]);
|
||||
});
|
||||
|
||||
it("reads ~/.cursor/auth.json on macOS only when cursor-agent uses the file store", async () => {
|
||||
const authPath = join(sessionHome, ".cursor", "auth.json");
|
||||
writeAuthJson(authPath);
|
||||
expect(await discoverSession(cursorSession({}), "darwin")).toEqual([]);
|
||||
expect(
|
||||
await discoverSession(cursorSession({ AGENT_CLI_CREDENTIAL_STORE: "file" }), "darwin"),
|
||||
).toEqual([{ key: "default", input: { store: "file", locator: authPath } }]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["another provider", cursorSession({}, "claude")],
|
||||
["a CURSOR_API_KEY session", cursorSession({ CURSOR_API_KEY: "key" })],
|
||||
["a CURSOR_AUTH_TOKEN session", cursorSession({ CURSOR_AUTH_TOKEN: "token" })],
|
||||
["an in-memory credential store", cursorSession({ AGENT_CLI_CREDENTIAL_STORE: "memory" })],
|
||||
])("returns no login for %s", async (_scenario, scope) => {
|
||||
writeCursorAuthJson(sessionHome, "session-token");
|
||||
expect(await discoverSession(scope, "linux")).toEqual([]);
|
||||
});
|
||||
|
||||
it("never falls back to the daemon's default stores", async () => {
|
||||
writeCursorStateDb(homeDir, { "cursorAuth/accessToken": "desktop-token" });
|
||||
writeCursorAuthJson(homeDir, "daemon-token");
|
||||
process.env["CURSOR_ACCESS_TOKEN"] = "daemon-env-token";
|
||||
expect(await discoverSession(cursorSession({}), "linux")).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("discovery returns a locator when fetch finds cursor credentials", async () => {
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
type UsageAccount,
|
||||
type UsageReport,
|
||||
type UsageBalance,
|
||||
type UsageScope,
|
||||
} from "@getpaseo/plugin/server/usage";
|
||||
|
||||
const ApiNumberSchema = z.coerce.number().finite();
|
||||
@@ -209,6 +210,7 @@ async function readToken(input: UsageInput): Promise<string | undefined> {
|
||||
: await readCursorTokenFromAuthJson(input.locator);
|
||||
return token ?? undefined;
|
||||
}
|
||||
|
||||
export async function discover(): Promise<UsageAccount[]> {
|
||||
const home = homedir();
|
||||
const candidates: UsageInput[] = ["CURSOR_ACCESS_TOKEN", "CURSOR_TOKEN"].map((locator) => ({
|
||||
@@ -238,3 +240,30 @@ export async function discover(): Promise<UsageAccount[]> {
|
||||
for (const input of candidates) if (await readToken(input)) return [{ key: "default", input }];
|
||||
return [];
|
||||
}
|
||||
|
||||
// Resolves the login a cursor-agent session runs on. The key matches global discovery so
|
||||
// the session's login joins the host's Cursor card instead of adding a second one.
|
||||
export async function discoverSession(
|
||||
scope: Extract<UsageScope, { kind: "session" }>,
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
): Promise<UsageAccount[]> {
|
||||
if (scope.provider !== "cursor") return [];
|
||||
const locator = cursorAgentAuthPath(scope.env, platform);
|
||||
if (!locator) return [];
|
||||
const input: UsageInput = { store: "file", locator };
|
||||
return (await readToken(input)) ? [{ key: "default", input }] : [];
|
||||
}
|
||||
|
||||
// Mirrors cursor-agent's credential store. Env credentials win over the stored login and
|
||||
// can't be named by an input. AGENT_CLI_CREDENTIAL_STORE picks "file" or "memory"; by
|
||||
// default macOS keeps the login in the Keychain and other platforms in auth.json.
|
||||
function cursorAgentAuthPath(env: Record<string, string>, platform: NodeJS.Platform) {
|
||||
if (env.CURSOR_API_KEY || env.CURSOR_AUTH_TOKEN) return null;
|
||||
const store = env.AGENT_CLI_CREDENTIAL_STORE;
|
||||
if (store === "memory" || (platform === "darwin" && store !== "file")) return null;
|
||||
const home = env.HOME || env.USERPROFILE || homedir();
|
||||
if (platform === "win32")
|
||||
return join(env.APPDATA || join(home, "AppData", "Roaming"), "Cursor", "auth.json");
|
||||
if (platform === "darwin") return join(home, ".cursor", "auth.json");
|
||||
return join(env.XDG_CONFIG_HOME || join(home, ".config"), "cursor", "auth.json");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user