pi-cursor-sdk 0.1.55 → 0.1.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +60 -2
  2. package/README.md +100 -28
  3. package/docs/cursor-live-smoke-checklist.md +7 -7
  4. package/docs/cursor-model-ux-spec.md +48 -41
  5. package/docs/cursor-native-tool-replay.md +4 -4
  6. package/docs/cursor-native-tool-visual-audit.md +1 -1
  7. package/docs/cursor-testing-lessons.md +20 -7
  8. package/docs/cursor-tool-surfaces.md +13 -2
  9. package/docs/platform-smoke-implementation.md +220 -0
  10. package/docs/platform-smoke.md +183 -247
  11. package/package.json +39 -7
  12. package/platform-smoke.config.mjs +5 -1
  13. package/scripts/cloud-runtime-smoke.d.mts +3 -0
  14. package/scripts/cloud-runtime-smoke.mjs +502 -0
  15. package/scripts/debug-provider-events.mjs +7 -2
  16. package/scripts/isolated-cursor-smoke.sh +4 -6
  17. package/scripts/lib/cursor-child-process.d.mts +1 -0
  18. package/scripts/lib/cursor-child-process.mjs +137 -7
  19. package/scripts/lib/local-resume-smoke-harness.mjs +543 -0
  20. package/scripts/local-resume-cleanup-smoke.mjs +108 -0
  21. package/scripts/local-resume-smoke.d.mts +1 -0
  22. package/scripts/local-resume-smoke.mjs +642 -0
  23. package/scripts/platform-smoke/artifact-anchored-extract.d.mts +10 -0
  24. package/scripts/platform-smoke/artifact-anchored-extract.mjs +111 -0
  25. package/scripts/platform-smoke/artifact-bundle-chunk.mjs +57 -0
  26. package/scripts/platform-smoke/artifact-bundle-contract.mjs +34 -0
  27. package/scripts/platform-smoke/artifact-fs-safety.mjs +311 -0
  28. package/scripts/platform-smoke/artifact-openat-extract.c +335 -0
  29. package/scripts/platform-smoke/artifact-secrets.mjs +155 -0
  30. package/scripts/platform-smoke/artifacts.mjs +293 -65
  31. package/scripts/platform-smoke/card-detect.mjs +16 -4
  32. package/scripts/platform-smoke/crabbox-runner.mjs +45 -3
  33. package/scripts/platform-smoke/doctor.mjs +20 -10
  34. package/scripts/platform-smoke/live-suite-runner.mjs +18 -57
  35. package/scripts/platform-smoke/local-resume-runner.mjs +252 -0
  36. package/scripts/platform-smoke/local-resume-suites.d.mts +15 -0
  37. package/scripts/platform-smoke/local-resume-suites.mjs +104 -0
  38. package/scripts/platform-smoke/scenarios.mjs +16 -2
  39. package/scripts/platform-smoke/target-runtime.mjs +206 -0
  40. package/scripts/platform-smoke/targets.mjs +33 -141
  41. package/scripts/platform-smoke/visual-evidence.mjs +6 -7
  42. package/scripts/platform-smoke/wrapped-line-match.mjs +9 -0
  43. package/scripts/platform-smoke.mjs +40 -27
  44. package/scripts/refresh-cursor-model-snapshots.mjs +18 -6
  45. package/scripts/steering-rpc-smoke.mjs +12 -2
  46. package/scripts/tmux-live-smoke.sh +3 -5
  47. package/shared/cursor-cloud-lifecycle-constants.d.mts +3 -0
  48. package/shared/cursor-cloud-lifecycle-constants.mjs +7 -0
  49. package/shared/cursor-sensitive-text.mjs +7 -1
  50. package/src/context.ts +5 -2
  51. package/src/cursor-agents-context-registration.ts +7 -0
  52. package/src/cursor-agents-context.ts +3 -1
  53. package/src/cursor-api-key.ts +15 -1
  54. package/src/cursor-bridge-contract.ts +3 -0
  55. package/src/cursor-cloud-lifecycle.ts +733 -0
  56. package/src/cursor-cloud-options.ts +206 -0
  57. package/src/cursor-cloud-reporting.ts +246 -0
  58. package/src/cursor-config.ts +659 -0
  59. package/src/cursor-display-only-trace.ts +14 -0
  60. package/src/cursor-display-text.ts +8 -2
  61. package/src/cursor-durable-fs.ts +49 -0
  62. package/src/cursor-fallback-models.generated.ts +2045 -485
  63. package/src/cursor-live-run-accounting.ts +7 -1
  64. package/src/cursor-live-run-coordinator.ts +1 -0
  65. package/src/cursor-pi-tool-bridge-run.ts +14 -4
  66. package/src/cursor-provider-errors.ts +31 -5
  67. package/src/cursor-provider-live-run-drain.ts +5 -0
  68. package/src/cursor-provider-run-finalizer.ts +37 -28
  69. package/src/cursor-provider-run-outcome.ts +7 -2
  70. package/src/cursor-provider-turn-finalize.ts +77 -7
  71. package/src/cursor-provider-turn-prepare.ts +228 -12
  72. package/src/cursor-provider-turn-runner.ts +42 -13
  73. package/src/cursor-provider-turn-send.ts +59 -16
  74. package/src/cursor-provider-turn-types.ts +44 -10
  75. package/src/cursor-runtime-state.ts +478 -0
  76. package/src/cursor-sdk-event-debug.ts +46 -6
  77. package/src/cursor-sdk-process-error-guard.ts +101 -30
  78. package/src/cursor-session-agent-cleanup.ts +328 -0
  79. package/src/cursor-session-agent-resume.ts +439 -0
  80. package/src/cursor-session-agent.ts +109 -13
  81. package/src/cursor-session-scope.ts +35 -2
  82. package/src/cursor-session-send-policy.ts +1 -1
  83. package/src/cursor-skill-tool.ts +30 -11
  84. package/src/cursor-state.ts +112 -69
  85. package/src/cursor-tool-manifest.ts +1 -1
  86. package/src/cursor-usage-accounting.ts +14 -4
  87. package/src/index.ts +11 -2
  88. package/src/model-discovery.ts +10 -56
@@ -0,0 +1,206 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import type { AgentModeOption, AgentOptions, ModelSelection } from "@cursor/sdk";
5
+ import { isCursorCloudEnvironmentType, type CursorResolvedSdkConfig } from "./cursor-config.js";
6
+
7
+ export interface CursorCloudLocalState {
8
+ insideGitRepo: boolean;
9
+ dirty: boolean;
10
+ unpushed: boolean;
11
+ }
12
+
13
+ export interface CursorCloudPreflightIssue {
14
+ code:
15
+ | "cloud_ack_required"
16
+ | "context_handoff_required"
17
+ | "local_state_not_allowed"
18
+ | "env_forwarding_not_implemented"
19
+ | "cloud_environment_type_invalid"
20
+ | "cloud_environment_type_required"
21
+ | "cloud_environment_repo_conflict"
22
+ | "cloud_branch_repo_required"
23
+ | "cloud_repo_invalid";
24
+ message: string;
25
+ }
26
+
27
+ export interface CursorCloudPreflightResult {
28
+ ok: boolean;
29
+ issues: CursorCloudPreflightIssue[];
30
+ }
31
+
32
+ type GitRunner = (cwd: string, args: string[]) => string | undefined;
33
+
34
+ const CLOUD_REPO_URL_MESSAGE = "Cursor cloud repository must be an HTTPS repository URL without embedded credentials, query parameters, or fragments.";
35
+
36
+ export function parseCursorCloudRepositoryUrl(value: string | undefined): string | undefined {
37
+ const trimmed = value?.trim();
38
+ if (!trimmed) return undefined;
39
+ try {
40
+ const url = new URL(trimmed);
41
+ if (
42
+ url.protocol !== "https:" ||
43
+ url.username !== "" ||
44
+ url.password !== "" ||
45
+ url.search !== "" ||
46
+ url.hash !== "" ||
47
+ url.pathname === "/"
48
+ ) return undefined;
49
+ return trimmed;
50
+ } catch {
51
+ return undefined;
52
+ }
53
+ }
54
+
55
+ function git(cwd: string, args: string[]): string | undefined {
56
+ try {
57
+ return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
58
+ } catch {
59
+ return undefined;
60
+ }
61
+ }
62
+
63
+ function hasGitMetadata(cwd: string): boolean {
64
+ let current = cwd;
65
+ while (true) {
66
+ if (existsSync(join(current, ".git"))) return true;
67
+ const parent = dirname(current);
68
+ if (parent === current) return false;
69
+ current = parent;
70
+ }
71
+ }
72
+
73
+ export function inspectCursorCloudLocalState(cwd: string, runGit: GitRunner = git): CursorCloudLocalState {
74
+ const insideGitRepo = runGit(cwd, ["rev-parse", "--is-inside-work-tree"]);
75
+ if (insideGitRepo !== "true") {
76
+ return hasGitMetadata(cwd)
77
+ ? { insideGitRepo: true, dirty: true, unpushed: true }
78
+ : { insideGitRepo: false, dirty: false, unpushed: false };
79
+ }
80
+ const status = runGit(cwd, ["status", "--porcelain=v1"]);
81
+ const dirty = status === undefined || status.length > 0;
82
+ const hasHead = runGit(cwd, ["rev-parse", "--verify", "HEAD"]) !== undefined;
83
+ const upstream = runGit(cwd, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"]);
84
+ const ahead = upstream ? runGit(cwd, ["rev-list", "--count", "@{upstream}..HEAD"]) : undefined;
85
+ const aheadCount = ahead === undefined ? Number.NaN : Number(ahead);
86
+ const unpushed = hasHead && (!upstream || !Number.isFinite(aheadCount) || aheadCount > 0);
87
+ return { insideGitRepo: true, dirty, unpushed };
88
+ }
89
+
90
+ export function buildCursorCloudAgentOptions(options: {
91
+ apiKey: string;
92
+ modelSelection: ModelSelection;
93
+ agentMode: AgentModeOption;
94
+ resolvedConfig: CursorResolvedSdkConfig;
95
+ name?: string;
96
+ }): AgentOptions {
97
+ const { resolvedConfig } = options;
98
+ const environment = resolvedConfig.cloud.environment.value;
99
+ const environmentType = environment?.type;
100
+ const environmentName = environment?.name;
101
+ const configuredRepo = resolvedConfig.cloud.repo.value;
102
+ const repo = parseCursorCloudRepositoryUrl(configuredRepo);
103
+ if (configuredRepo && !repo) throw new Error(CLOUD_REPO_URL_MESSAGE);
104
+ const cloud = {
105
+ ...(isCursorCloudEnvironmentType(environmentType)
106
+ ? {
107
+ env: {
108
+ type: environmentType,
109
+ ...(environmentName ? { name: environmentName } : {}),
110
+ },
111
+ }
112
+ : {}),
113
+ ...(repo
114
+ ? {
115
+ repos: [
116
+ {
117
+ url: repo,
118
+ ...(resolvedConfig.cloud.branch.value ? { startingRef: resolvedConfig.cloud.branch.value } : {}),
119
+ },
120
+ ],
121
+ }
122
+ : {}),
123
+ ...(resolvedConfig.cloud.directPush.value ? { workOnCurrentBranch: true } : {}),
124
+ };
125
+ return {
126
+ apiKey: options.apiKey,
127
+ model: options.modelSelection,
128
+ mode: options.agentMode,
129
+ ...(options.name ? { name: options.name } : {}),
130
+ cloud,
131
+ };
132
+ }
133
+
134
+ export function preflightCursorCloudRuntime(options: {
135
+ resolvedConfig: CursorResolvedSdkConfig;
136
+ localState?: CursorCloudLocalState;
137
+ hasPriorContext?: boolean;
138
+ }): CursorCloudPreflightResult {
139
+ const { resolvedConfig } = options;
140
+ const issues: CursorCloudPreflightIssue[] = [];
141
+ if (!resolvedConfig.cloud.acknowledged.value) {
142
+ issues.push({
143
+ code: "cloud_ack_required",
144
+ message: "Cursor cloud runtime requires first-use acknowledgement; run /cursor-runtime cloud in an interactive session or pass --cursor-cloud-ack / PI_CURSOR_CLOUD_ACK=1.",
145
+ });
146
+ }
147
+ if (options.hasPriorContext && resolvedConfig.cloud.contextHandoff.value === "never") {
148
+ issues.push({
149
+ code: "context_handoff_required",
150
+ message: "Cursor cloud runtime needs --cursor-cloud-context=fresh or --cursor-cloud-context=bootstrap for sessions with prior pi context.",
151
+ });
152
+ }
153
+ if (
154
+ options.localState?.insideGitRepo &&
155
+ (options.localState.dirty || options.localState.unpushed) &&
156
+ !resolvedConfig.cloud.allowLocalState.value
157
+ ) {
158
+ issues.push({
159
+ code: "local_state_not_allowed",
160
+ message: "Cursor cloud runtime cannot see dirty or unpushed local state; pass --cursor-cloud-allow-local-state only after accepting that risk.",
161
+ });
162
+ }
163
+ if (resolvedConfig.cloud.envNames.value.length > 0 || resolvedConfig.cloud.envFromFiles.value) {
164
+ issues.push({
165
+ code: "env_forwarding_not_implemented",
166
+ message: "Cursor cloud env forwarding is not implemented; use Cursor-native environment setup such as .cursor/environment.json or dashboard-managed secrets.",
167
+ });
168
+ }
169
+ if (resolvedConfig.cloud.repo.value && !parseCursorCloudRepositoryUrl(resolvedConfig.cloud.repo.value)) {
170
+ issues.push({
171
+ code: "cloud_repo_invalid",
172
+ message: CLOUD_REPO_URL_MESSAGE,
173
+ });
174
+ }
175
+ if (resolvedConfig.cloud.branch.value && !resolvedConfig.cloud.repo.value) {
176
+ issues.push({
177
+ code: "cloud_branch_repo_required",
178
+ message: "Cursor cloud branch/ref requires --cursor-cloud-repo because the installed Cursor SDK supports startingRef only on cloud.repos entries.",
179
+ });
180
+ }
181
+ const environment = resolvedConfig.cloud.environment.value;
182
+ if (environment?.type && !isCursorCloudEnvironmentType(environment.type)) {
183
+ issues.push({
184
+ code: "cloud_environment_type_invalid",
185
+ message: `Invalid Cursor cloud environment type "${environment.type}"; expected cloud, pool, or machine.`,
186
+ });
187
+ }
188
+ if (environment?.name && !environment.type) {
189
+ issues.push({
190
+ code: "cloud_environment_type_required",
191
+ message: "Cursor cloud environment name requires --cursor-cloud-env-type=cloud|pool|machine or PI_CURSOR_CLOUD_ENV_TYPE.",
192
+ });
193
+ }
194
+ if (environment?.type === "cloud" && environment.name && resolvedConfig.cloud.repo.value) {
195
+ issues.push({
196
+ code: "cloud_environment_repo_conflict",
197
+ message: "Cursor cloud named environments cannot be combined with --cursor-cloud-repo; omit the repo or use a pool/machine environment.",
198
+ });
199
+ }
200
+ return { ok: issues.length === 0, issues };
201
+ }
202
+
203
+ export function formatCursorCloudPreflightError(result: CursorCloudPreflightResult): string {
204
+ const details = result.issues.map((issue) => `- ${issue.message}`).join("\n");
205
+ return `Cursor cloud runtime is not ready to start.\n${details}\nUse --cursor-runtime local or /cursor-runtime local to run with the local Cursor SDK agent.`;
206
+ }
@@ -0,0 +1,246 @@
1
+ import type { Run, RunResult, SDKAgent, SDKArtifact, TokenUsage } from "@cursor/sdk";
2
+ import { truncateCursorDisplayLine } from "./cursor-display-text.js";
3
+ import { scrubSensitiveText } from "./cursor-sensitive-text.js";
4
+ import { asRecord, getArray, getNumber, getRecord, getString } from "./cursor-record-utils.js";
5
+
6
+ const DEFAULT_CURSOR_CLOUD_API_BASE_URL = "https://api.cursor.com";
7
+ const CLOUD_REPORT_TIMEOUT_MS = 5000;
8
+ const MAX_CLOUD_ID_DISPLAY_LENGTH = 160;
9
+ const MAX_CLOUD_REPO_DISPLAY_LENGTH = 240;
10
+ const MAX_CLOUD_BRANCH_DISPLAY_LENGTH = 160;
11
+ const MAX_CLOUD_ARTIFACT_DISPLAY_LENGTH = 240;
12
+ export const MAX_CLOUD_REPORT_BRANCHES = 5;
13
+ export const MAX_CLOUD_REPORT_ARTIFACTS = 10;
14
+
15
+ export interface CursorCloudUsageReport {
16
+ totalUsage?: TokenUsage;
17
+ runUsage?: TokenUsage;
18
+ }
19
+
20
+ export interface CursorCloudRunBranch {
21
+ repoUrl: string;
22
+ branch?: string;
23
+ prUrl?: string;
24
+ }
25
+
26
+ export interface CursorCloudRunReport {
27
+ agentId: string;
28
+ runId: string;
29
+ branches: CursorCloudRunBranch[];
30
+ artifacts?: SDKArtifact[];
31
+ usage?: CursorCloudUsageReport;
32
+ }
33
+
34
+ export interface FetchCursorCloudRawUsageOptions {
35
+ agentId: string;
36
+ runId?: string;
37
+ apiKey: string | undefined;
38
+ baseUrl?: string;
39
+ fetchImpl?: typeof fetch;
40
+ timeoutMs?: number;
41
+ }
42
+
43
+ function withTimeout<T>(promise: Promise<T>, timeoutMs = CLOUD_REPORT_TIMEOUT_MS): Promise<T | undefined> {
44
+ let timer: ReturnType<typeof setTimeout> | undefined;
45
+ const timeout = new Promise<undefined>((resolve) => {
46
+ timer = setTimeout(() => resolve(undefined), timeoutMs);
47
+ timer.unref?.();
48
+ });
49
+ return Promise.race([promise.catch(() => undefined), timeout]).finally(() => {
50
+ if (timer) clearTimeout(timer);
51
+ });
52
+ }
53
+
54
+ function fetchJsonWithAbortTimeout(
55
+ fetchImpl: typeof fetch,
56
+ url: URL,
57
+ init: RequestInit,
58
+ timeoutMs = CLOUD_REPORT_TIMEOUT_MS,
59
+ ): Promise<unknown> {
60
+ const controller = new AbortController();
61
+ let timer: ReturnType<typeof setTimeout> | undefined;
62
+ const request = (async () => {
63
+ const response = await fetchImpl(url, { ...init, signal: controller.signal });
64
+ if (!response.ok) return undefined;
65
+ return response.json();
66
+ })().catch(() => undefined);
67
+ const timeout = new Promise<undefined>((resolve) => {
68
+ timer = setTimeout(() => {
69
+ controller.abort();
70
+ resolve(undefined);
71
+ }, timeoutMs);
72
+ timer.unref?.();
73
+ });
74
+ return Promise.race([request, timeout]).finally(() => {
75
+ if (timer) clearTimeout(timer);
76
+ });
77
+ }
78
+
79
+ function readNonnegativeNumber(record: Record<string, unknown> | undefined, key: string): number | undefined {
80
+ const value = getNumber(record, key);
81
+ return value !== undefined && Number.isFinite(value) && value >= 0 ? value : undefined;
82
+ }
83
+
84
+ function readTokenUsage(value: unknown): TokenUsage | undefined {
85
+ const record = asRecord(value);
86
+ const inputTokens = readNonnegativeNumber(record, "inputTokens");
87
+ const outputTokens = readNonnegativeNumber(record, "outputTokens");
88
+ const cacheReadTokens = readNonnegativeNumber(record, "cacheReadTokens");
89
+ const cacheWriteTokens = readNonnegativeNumber(record, "cacheWriteTokens");
90
+ const totalTokens = readNonnegativeNumber(record, "totalTokens");
91
+ if (
92
+ inputTokens === undefined ||
93
+ outputTokens === undefined ||
94
+ cacheReadTokens === undefined ||
95
+ cacheWriteTokens === undefined ||
96
+ totalTokens === undefined
97
+ ) return undefined;
98
+ const reasoningTokens = readNonnegativeNumber(record, "reasoningTokens");
99
+ return {
100
+ inputTokens,
101
+ outputTokens,
102
+ cacheReadTokens,
103
+ cacheWriteTokens,
104
+ totalTokens,
105
+ ...(reasoningTokens === undefined ? {} : { reasoningTokens }),
106
+ };
107
+ }
108
+
109
+ function readNonemptyString(record: Record<string, unknown> | undefined, key: string): string | undefined {
110
+ const value = getString(record, key);
111
+ return value && truncateCursorDisplayLine(value) ? value : undefined;
112
+ }
113
+
114
+ function normalizeBranches(value: unknown): CursorCloudRunBranch[] {
115
+ if (!Array.isArray(value)) return [];
116
+ return value.flatMap((item) => {
117
+ const record = asRecord(item);
118
+ const repoUrl = readNonemptyString(record, "repoUrl");
119
+ if (!repoUrl) return [];
120
+ const branch = readNonemptyString(record, "branch");
121
+ const prUrl = readNonemptyString(record, "prUrl");
122
+ return branch || prUrl ? [{ repoUrl, ...(branch ? { branch } : {}), ...(prUrl ? { prUrl } : {}) }] : [];
123
+ });
124
+ }
125
+
126
+ function normalizeArtifacts(value: unknown): SDKArtifact[] | undefined {
127
+ if (value === undefined) return undefined;
128
+ if (!Array.isArray(value)) return undefined;
129
+ return value.flatMap((item) => {
130
+ const record = asRecord(item);
131
+ const path = readNonemptyString(record, "path");
132
+ const sizeBytes = readNonnegativeNumber(record, "sizeBytes");
133
+ const updatedAt = readNonemptyString(record, "updatedAt");
134
+ return path && sizeBytes !== undefined && updatedAt ? [{ path, sizeBytes, updatedAt }] : [];
135
+ });
136
+ }
137
+
138
+ function normalizeUsage(value: unknown): CursorCloudUsageReport | undefined {
139
+ const record = asRecord(value);
140
+ const totalUsage = readTokenUsage(getRecord(record, "totalUsage"));
141
+ const runUsage = readTokenUsage(getRecord(record, "runUsage"));
142
+ return totalUsage || runUsage ? { ...(totalUsage ? { totalUsage } : {}), ...(runUsage ? { runUsage } : {}) } : undefined;
143
+ }
144
+
145
+ export async function fetchCursorCloudRawUsage(
146
+ options: FetchCursorCloudRawUsageOptions,
147
+ ): Promise<CursorCloudUsageReport | undefined> {
148
+ if (!options.apiKey || typeof options.fetchImpl !== "function" && typeof fetch !== "function") return undefined;
149
+ try {
150
+ const baseUrl = options.baseUrl ?? DEFAULT_CURSOR_CLOUD_API_BASE_URL;
151
+ const url = new URL(`/v1/agents/${encodeURIComponent(options.agentId)}/usage`, baseUrl);
152
+ if (options.runId) url.searchParams.set("runId", options.runId);
153
+ const fetchImpl = options.fetchImpl ?? fetch;
154
+ const body = asRecord(await fetchJsonWithAbortTimeout(fetchImpl, url, {
155
+ headers: {
156
+ Authorization: `Bearer ${options.apiKey}`,
157
+ "x-cursor-client-type": "sdk",
158
+ },
159
+ }, options.timeoutMs));
160
+ const runs = getArray(body, "runs") ?? [];
161
+ const matchingRun = options.runId
162
+ ? runs.map(asRecord).find((run) => getString(run, "id") === options.runId)
163
+ : undefined;
164
+ return normalizeUsage({
165
+ totalUsage: getRecord(body, "totalUsage"),
166
+ runUsage: getRecord(matchingRun, "usage"),
167
+ });
168
+ } catch {
169
+ return undefined;
170
+ }
171
+ }
172
+
173
+ export async function collectCursorCloudRunReport(options: {
174
+ agent: SDKAgent;
175
+ run: Run;
176
+ waitResult: RunResult;
177
+ apiKey: string | undefined;
178
+ }): Promise<CursorCloudRunReport> {
179
+ const listArtifacts = options.agent.listArtifacts;
180
+ const [artifactResult, usage] = await Promise.all([
181
+ typeof listArtifacts === "function" ? withTimeout(listArtifacts.call(options.agent)) : undefined,
182
+ fetchCursorCloudRawUsage({ agentId: options.run.agentId, runId: options.run.id, apiKey: options.apiKey }),
183
+ ]);
184
+ const waitBranches = getArray(getRecord(asRecord(options.waitResult), "git"), "branches");
185
+ const runBranches = getArray(getRecord(asRecord(options.run), "git"), "branches");
186
+ return {
187
+ agentId: options.run.agentId,
188
+ runId: options.run.id,
189
+ branches: normalizeBranches(waitBranches ?? runBranches),
190
+ artifacts: normalizeArtifacts(artifactResult),
191
+ usage: normalizeUsage(usage),
192
+ };
193
+ }
194
+
195
+ function formatTokenUsage(usage: TokenUsage): string {
196
+ const parts = [
197
+ `input ${usage.inputTokens.toLocaleString("en-US")}`,
198
+ `output ${usage.outputTokens.toLocaleString("en-US")}`,
199
+ `cache read ${usage.cacheReadTokens.toLocaleString("en-US")}`,
200
+ `cache write ${usage.cacheWriteTokens.toLocaleString("en-US")}`,
201
+ `total ${usage.totalTokens.toLocaleString("en-US")}`,
202
+ ];
203
+ if (usage.reasoningTokens !== undefined) parts.splice(2, 0, `reasoning ${usage.reasoningTokens.toLocaleString("en-US")}`);
204
+ return parts.join(", ");
205
+ }
206
+
207
+ function remoteDisplayString(value: unknown, maxLength: number, apiKey: string | undefined): string {
208
+ return typeof value === "string" ? truncateCursorDisplayLine(scrubSensitiveText(value, apiKey), maxLength) : "";
209
+ }
210
+
211
+ function formatBranchLine(branch: CursorCloudRunBranch, apiKey: string | undefined): string[] {
212
+ const lines: string[] = [];
213
+ const branchName = remoteDisplayString(branch.branch, MAX_CLOUD_BRANCH_DISPLAY_LENGTH, apiKey);
214
+ const repoUrl = remoteDisplayString(branch.repoUrl, MAX_CLOUD_REPO_DISPLAY_LENGTH, apiKey);
215
+ const prUrl = remoteDisplayString(branch.prUrl, MAX_CLOUD_REPO_DISPLAY_LENGTH, apiKey);
216
+ if (branchName) lines.push(`- branch: ${branchName}${repoUrl ? ` (${repoUrl})` : ""}`);
217
+ if (prUrl) lines.push(`- PR: ${prUrl}`);
218
+ return lines;
219
+ }
220
+
221
+ export function formatCursorCloudRunReport(report: CursorCloudRunReport, options: { apiKey?: string } = {}): string {
222
+ const agentId = remoteDisplayString(report.agentId, MAX_CLOUD_ID_DISPLAY_LENGTH, options.apiKey) || "unknown";
223
+ const runId = remoteDisplayString(report.runId, MAX_CLOUD_ID_DISPLAY_LENGTH, options.apiKey) || "unknown";
224
+ const branches = normalizeBranches(report.branches);
225
+ const artifacts = normalizeArtifacts(report.artifacts);
226
+ const lines = ["Cursor cloud run:", `- agent: ${agentId}`, `- run: ${runId}`];
227
+ for (const branch of branches.slice(0, MAX_CLOUD_REPORT_BRANCHES)) lines.push(...formatBranchLine(branch, options.apiKey));
228
+ if (branches.length > MAX_CLOUD_REPORT_BRANCHES) lines.push(`- branches: +${branches.length - MAX_CLOUD_REPORT_BRANCHES} more`);
229
+ if (artifacts) {
230
+ if (artifacts.length === 0) {
231
+ lines.push("- artifacts: none");
232
+ } else {
233
+ lines.push("- artifacts:");
234
+ for (const artifact of artifacts.slice(0, MAX_CLOUD_REPORT_ARTIFACTS)) {
235
+ const path = remoteDisplayString(artifact.path, MAX_CLOUD_ARTIFACT_DISPLAY_LENGTH, options.apiKey);
236
+ const updatedAt = remoteDisplayString(artifact.updatedAt, MAX_CLOUD_ARTIFACT_DISPLAY_LENGTH, options.apiKey);
237
+ lines.push(` - ${path} (${artifact.sizeBytes.toLocaleString("en-US")} bytes, updated ${updatedAt})`);
238
+ }
239
+ if (artifacts.length > MAX_CLOUD_REPORT_ARTIFACTS) lines.push(` - +${artifacts.length - MAX_CLOUD_REPORT_ARTIFACTS} more`);
240
+ }
241
+ }
242
+ const normalizedUsage = normalizeUsage(report.usage);
243
+ const usage = normalizedUsage?.runUsage ?? normalizedUsage?.totalUsage;
244
+ if (usage) lines.push(`- raw usage (display only): ${formatTokenUsage(usage)}`);
245
+ return scrubSensitiveText(`${lines.join("\n")}\n`, options.apiKey);
246
+ }