pi-cursor-sdk 0.1.59 → 0.1.61
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.
- package/CHANGELOG.md +33 -0
- package/README.md +124 -10
- package/docs/cursor-model-ux-spec.md +19 -11
- package/docs/cursor-native-tool-replay.md +5 -3
- package/docs/cursor-testing-lessons.md +2 -2
- package/docs/cursor-tool-surfaces.md +7 -1
- package/docs/platform-smoke.md +51 -20
- package/package.json +13 -2
- package/platform-smoke.config.mjs +1 -0
- package/scripts/cloud-runtime-smoke.d.mts +40 -1
- package/scripts/cloud-runtime-smoke.mjs +444 -325
- package/scripts/lib/cloud-smoke-artifacts.d.mts +16 -0
- package/scripts/lib/cloud-smoke-artifacts.mjs +94 -0
- package/scripts/lib/cloud-smoke-cleanup-evidence.d.mts +209 -0
- package/scripts/lib/cloud-smoke-cleanup-evidence.mjs +585 -0
- package/scripts/lib/cloud-smoke-github.d.mts +78 -0
- package/scripts/lib/cloud-smoke-github.mjs +331 -0
- package/scripts/lib/cloud-smoke-pi-runner.d.mts +42 -0
- package/scripts/lib/cloud-smoke-pi-runner.mjs +214 -0
- package/scripts/lib/cloud-smoke-shutdown.d.mts +67 -0
- package/scripts/lib/cloud-smoke-shutdown.mjs +133 -0
- package/scripts/lib/local-resume-smoke-harness.mjs +2 -0
- package/scripts/local-resume-cleanup-smoke.mjs +11 -2
- package/scripts/platform-smoke/card-detect.mjs +1 -0
- package/scripts/platform-smoke/scenarios.d.mts +22 -0
- package/scripts/platform-smoke/scenarios.mjs +17 -0
- package/scripts/platform-smoke/targets.mjs +1 -0
- package/src/cursor-agent-message-web-tools.ts +14 -6
- package/src/cursor-cloud-local-state.ts +536 -0
- package/src/cursor-cloud-options.ts +46 -70
- package/src/cursor-config.ts +160 -14
- package/src/cursor-http1.ts +65 -0
- package/src/cursor-live-run-coordinator.ts +8 -1
- package/src/cursor-pi-tool-bridge-abort.ts +10 -3
- package/src/cursor-pi-tool-bridge-constants.ts +6 -0
- package/src/cursor-pi-tool-bridge-env.ts +9 -0
- package/src/cursor-pi-tool-bridge-run.ts +27 -5
- package/src/cursor-pi-tool-bridge.ts +11 -2
- package/src/cursor-provider-errors.ts +72 -2
- package/src/cursor-provider-live-run-drain.ts +2 -1
- package/src/cursor-provider-run-finalizer.ts +8 -2
- package/src/cursor-provider-run-outcome.ts +7 -1
- package/src/cursor-provider-turn-finalize.ts +23 -4
- package/src/cursor-provider-turn-prepare.ts +17 -3
- package/src/cursor-provider-turn-runner.ts +15 -1
- package/src/cursor-provider-turn-send.ts +1 -1
- package/src/cursor-question-tool.ts +8 -0
- package/src/cursor-ripgrep-path.ts +32 -0
- package/src/cursor-runtime-state.ts +119 -100
- package/src/cursor-sdk-process-error-guard.ts +62 -0
- package/src/cursor-session-agent-cleanup.ts +66 -15
- package/src/cursor-session-agent-lifecycle.ts +9 -4
- package/src/cursor-session-agent-resume.ts +50 -7
- package/src/cursor-session-agent.ts +131 -71
- package/src/cursor-session-scope.ts +22 -2
- package/src/cursor-session-store.ts +155 -0
- package/src/cursor-state.ts +162 -21
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
1
|
import type { AgentModeOption, AgentOptions, ModelSelection } from "@cursor/sdk";
|
|
5
2
|
import { isCursorCloudEnvironmentType, type CursorResolvedSdkConfig } from "./cursor-config.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
3
|
+
import {
|
|
4
|
+
normalizeCursorCloudStartingRef,
|
|
5
|
+
parseCursorCloudRepositoryUrl,
|
|
6
|
+
type CursorCloudLocalState,
|
|
7
|
+
type CursorCloudLocalStateUnknownReason,
|
|
8
|
+
} from "./cursor-cloud-local-state.js";
|
|
12
9
|
|
|
13
10
|
export interface CursorCloudPreflightIssue {
|
|
14
11
|
code:
|
|
@@ -20,6 +17,7 @@ export interface CursorCloudPreflightIssue {
|
|
|
20
17
|
| "cloud_environment_type_required"
|
|
21
18
|
| "cloud_environment_repo_conflict"
|
|
22
19
|
| "cloud_branch_repo_required"
|
|
20
|
+
| "cloud_branch_invalid"
|
|
23
21
|
| "cloud_repo_invalid";
|
|
24
22
|
message: string;
|
|
25
23
|
}
|
|
@@ -29,63 +27,21 @@ export interface CursorCloudPreflightResult {
|
|
|
29
27
|
issues: CursorCloudPreflightIssue[];
|
|
30
28
|
}
|
|
31
29
|
|
|
32
|
-
type GitRunner = (cwd: string, args: string[]) => string | undefined;
|
|
33
|
-
|
|
34
30
|
const CLOUD_REPO_URL_MESSAGE = "Cursor cloud repository must be an HTTPS repository URL without embedded credentials, query parameters, or fragments.";
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
}
|
|
31
|
+
const CLOUD_STARTING_REF_MESSAGE = "Cursor cloud branch/ref must be a valid Git branch name, refs/heads/<branch>, or full commit SHA; invalid branch names and other refs/* are unsupported.";
|
|
32
|
+
const CLOUD_LOCAL_STATE_REASON_LABELS: Record<CursorCloudLocalStateUnknownReason["code"], string> = {
|
|
33
|
+
bare_repo: "the repository is bare",
|
|
34
|
+
repository_detection_failed: "Git could not determine whether the working directory is a repository",
|
|
35
|
+
status_failed: "Git could not determine whether the worktree or index is clean",
|
|
36
|
+
index_failed: "Git index inspection failed",
|
|
37
|
+
hidden_index_state: "the index hides worktree changes",
|
|
38
|
+
history_probe_failed: "Git could not inspect replacement or graft history overrides",
|
|
39
|
+
history_overrides: "replacement refs or graft metadata are present",
|
|
40
|
+
head_unavailable: "local HEAD is unavailable",
|
|
41
|
+
unverified_target: "the cloud target has no locally verified tracking ref",
|
|
42
|
+
target_probe_failed: "Git failed while resolving the cloud target",
|
|
43
|
+
comparison_failed: "the local HEAD comparison failed",
|
|
44
|
+
};
|
|
89
45
|
|
|
90
46
|
export function buildCursorCloudAgentOptions(options: {
|
|
91
47
|
apiKey: string;
|
|
@@ -101,6 +57,8 @@ export function buildCursorCloudAgentOptions(options: {
|
|
|
101
57
|
const configuredRepo = resolvedConfig.cloud.repo.value;
|
|
102
58
|
const repo = parseCursorCloudRepositoryUrl(configuredRepo);
|
|
103
59
|
if (configuredRepo && !repo) throw new Error(CLOUD_REPO_URL_MESSAGE);
|
|
60
|
+
const startingRef = normalizeCursorCloudStartingRef(resolvedConfig.cloud.branch.value);
|
|
61
|
+
if (startingRef.kind === "unsupported") throw new Error(CLOUD_STARTING_REF_MESSAGE);
|
|
104
62
|
const cloud = {
|
|
105
63
|
...(isCursorCloudEnvironmentType(environmentType)
|
|
106
64
|
? {
|
|
@@ -115,12 +73,18 @@ export function buildCursorCloudAgentOptions(options: {
|
|
|
115
73
|
repos: [
|
|
116
74
|
{
|
|
117
75
|
url: repo,
|
|
118
|
-
...(
|
|
76
|
+
...(startingRef.kind === "branch" || startingRef.kind === "commit" ? { startingRef: startingRef.value } : {}),
|
|
119
77
|
},
|
|
120
78
|
],
|
|
121
79
|
}
|
|
122
80
|
: {}),
|
|
123
81
|
...(resolvedConfig.cloud.directPush.value ? { workOnCurrentBranch: true } : {}),
|
|
82
|
+
...(resolvedConfig.cloud.autoCreatePR.source !== "builtin"
|
|
83
|
+
? { autoCreatePR: resolvedConfig.cloud.autoCreatePR.value }
|
|
84
|
+
: {}),
|
|
85
|
+
...(resolvedConfig.cloud.skipReviewerRequest.source !== "builtin"
|
|
86
|
+
? { skipReviewerRequest: resolvedConfig.cloud.skipReviewerRequest.value }
|
|
87
|
+
: {}),
|
|
124
88
|
};
|
|
125
89
|
return {
|
|
126
90
|
apiKey: options.apiKey,
|
|
@@ -133,7 +97,7 @@ export function buildCursorCloudAgentOptions(options: {
|
|
|
133
97
|
|
|
134
98
|
export function preflightCursorCloudRuntime(options: {
|
|
135
99
|
resolvedConfig: CursorResolvedSdkConfig;
|
|
136
|
-
localState
|
|
100
|
+
localState: CursorCloudLocalState;
|
|
137
101
|
hasPriorContext?: boolean;
|
|
138
102
|
}): CursorCloudPreflightResult {
|
|
139
103
|
const { resolvedConfig } = options;
|
|
@@ -151,13 +115,22 @@ export function preflightCursorCloudRuntime(options: {
|
|
|
151
115
|
});
|
|
152
116
|
}
|
|
153
117
|
if (
|
|
154
|
-
options.localState
|
|
155
|
-
(options.localState.dirty || options.localState.
|
|
118
|
+
options.localState.insideGitRepo !== false &&
|
|
119
|
+
(options.localState.dirty !== false || options.localState.comparison !== "contains_head") &&
|
|
156
120
|
!resolvedConfig.cloud.allowLocalState.value
|
|
157
121
|
) {
|
|
122
|
+
const causes: string[] = [];
|
|
123
|
+
if (options.localState.dirty === true) causes.push("the worktree or index is dirty");
|
|
124
|
+
if (options.localState.dirty === "unknown") causes.push("Git could not determine whether the worktree or index is clean");
|
|
125
|
+
if (options.localState.comparison === "unpushed") causes.push("local HEAD contains unpushed commits");
|
|
126
|
+
if (options.localState.comparison === "unknown") {
|
|
127
|
+
causes.push(...options.localState.reasons
|
|
128
|
+
.filter((reason) => reason.code !== "status_failed")
|
|
129
|
+
.map((reason) => CLOUD_LOCAL_STATE_REASON_LABELS[reason.code]));
|
|
130
|
+
}
|
|
158
131
|
issues.push({
|
|
159
132
|
code: "local_state_not_allowed",
|
|
160
|
-
message:
|
|
133
|
+
message: `Cursor cloud runtime cannot safely omit local state because ${causes.join("; ")}. Configure an explicit repository branch/ref with current local tracking evidence, or pass --cursor-cloud-allow-local-state only after accepting that risk.`,
|
|
161
134
|
});
|
|
162
135
|
}
|
|
163
136
|
if (resolvedConfig.cloud.envNames.value.length > 0 || resolvedConfig.cloud.envFromFiles.value) {
|
|
@@ -178,6 +151,9 @@ export function preflightCursorCloudRuntime(options: {
|
|
|
178
151
|
message: "Cursor cloud branch/ref requires --cursor-cloud-repo because the installed Cursor SDK supports startingRef only on cloud.repos entries.",
|
|
179
152
|
});
|
|
180
153
|
}
|
|
154
|
+
if (normalizeCursorCloudStartingRef(resolvedConfig.cloud.branch.value).kind === "unsupported") {
|
|
155
|
+
issues.push({ code: "cloud_branch_invalid", message: CLOUD_STARTING_REF_MESSAGE });
|
|
156
|
+
}
|
|
181
157
|
const environment = resolvedConfig.cloud.environment.value;
|
|
182
158
|
if (environment?.type && !isCursorCloudEnvironmentType(environment.type)) {
|
|
183
159
|
issues.push({
|
package/src/cursor-config.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
chmodSync,
|
|
4
|
+
closeSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
openSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
renameSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
statSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
} from "node:fs";
|
|
3
14
|
import { dirname, join } from "node:path";
|
|
4
15
|
import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
16
|
import { parseOptionalEnvBoolean } from "./cursor-env-boolean.js";
|
|
@@ -12,6 +23,8 @@ export const CURSOR_CLOUD_REPO_ENV = "PI_CURSOR_CLOUD_REPO";
|
|
|
12
23
|
export const CURSOR_CLOUD_BRANCH_ENV = "PI_CURSOR_CLOUD_BRANCH";
|
|
13
24
|
export const CURSOR_CLOUD_CONTEXT_ENV = "PI_CURSOR_CLOUD_CONTEXT";
|
|
14
25
|
export const CURSOR_CLOUD_DIRECT_PUSH_ENV = "PI_CURSOR_CLOUD_DIRECT_PUSH";
|
|
26
|
+
export const CURSOR_CLOUD_AUTO_CREATE_PR_ENV = "PI_CURSOR_CLOUD_AUTO_CREATE_PR";
|
|
27
|
+
export const CURSOR_CLOUD_SKIP_REVIEWER_REQUEST_ENV = "PI_CURSOR_CLOUD_SKIP_REVIEWER_REQUEST";
|
|
15
28
|
export const CURSOR_CLOUD_ALLOW_LOCAL_STATE_ENV = "PI_CURSOR_CLOUD_ALLOW_LOCAL_STATE";
|
|
16
29
|
export const CURSOR_CLOUD_ENV_ENV = "PI_CURSOR_CLOUD_ENV";
|
|
17
30
|
export const CURSOR_CLOUD_ENV_FROM_FILES_ENV = "PI_CURSOR_CLOUD_ENV_FROM_FILES";
|
|
@@ -22,6 +35,7 @@ export const CURSOR_AUTO_REVIEW_ENV = "PI_CURSOR_AUTO_REVIEW";
|
|
|
22
35
|
export const CURSOR_SANDBOX_ENV = "PI_CURSOR_SANDBOX";
|
|
23
36
|
export const CURSOR_LOCAL_FORCE_ENV = "PI_CURSOR_LOCAL_FORCE";
|
|
24
37
|
export const CURSOR_LOCAL_RESUME_ENV = "PI_CURSOR_LOCAL_RESUME";
|
|
38
|
+
export const CURSOR_HTTP1_ENV = "PI_CURSOR_HTTP_1_1";
|
|
25
39
|
|
|
26
40
|
export type CursorConfigSource = "cli" | "environment" | "project" | "user" | "session" | "model-alias" | "builtin";
|
|
27
41
|
export type CursorConfigTrustLevel = "one-shot" | "environment" | "trusted-project" | "user" | "session" | "model-catalog" | "builtin";
|
|
@@ -42,6 +56,8 @@ export interface CursorSdkConfig {
|
|
|
42
56
|
branch?: string;
|
|
43
57
|
contextHandoff?: CursorCloudContextHandoff;
|
|
44
58
|
directPush?: boolean;
|
|
59
|
+
autoCreatePR?: boolean;
|
|
60
|
+
skipReviewerRequest?: boolean;
|
|
45
61
|
allowLocalState?: boolean;
|
|
46
62
|
envNames?: string[];
|
|
47
63
|
envFromFiles?: boolean;
|
|
@@ -56,6 +72,7 @@ export interface CursorSdkConfig {
|
|
|
56
72
|
};
|
|
57
73
|
force?: boolean;
|
|
58
74
|
resume?: boolean;
|
|
75
|
+
useHttp1ForAgent?: boolean;
|
|
59
76
|
};
|
|
60
77
|
}
|
|
61
78
|
|
|
@@ -82,6 +99,8 @@ export interface CursorResolvedSdkConfig {
|
|
|
82
99
|
branch: CursorResolvedSetting<string | undefined>;
|
|
83
100
|
contextHandoff: CursorResolvedSetting<CursorCloudContextHandoff>;
|
|
84
101
|
directPush: CursorResolvedSetting<boolean>;
|
|
102
|
+
autoCreatePR: CursorResolvedSetting<boolean>;
|
|
103
|
+
skipReviewerRequest: CursorResolvedSetting<boolean>;
|
|
85
104
|
allowLocalState: CursorResolvedSetting<boolean>;
|
|
86
105
|
envNames: CursorResolvedSetting<string[]>;
|
|
87
106
|
envFromFiles: CursorResolvedSetting<boolean>;
|
|
@@ -93,6 +112,7 @@ export interface CursorResolvedSdkConfig {
|
|
|
93
112
|
sandboxEnabled: CursorResolvedSetting<boolean>;
|
|
94
113
|
force: CursorResolvedSetting<boolean>;
|
|
95
114
|
resume: CursorResolvedSetting<boolean>;
|
|
115
|
+
useHttp1ForAgent: CursorResolvedSetting<boolean>;
|
|
96
116
|
};
|
|
97
117
|
}
|
|
98
118
|
|
|
@@ -143,6 +163,8 @@ const BUILT_IN_CURSOR_CONFIG: Required<Pick<CursorSdkConfig, "runtime">> & {
|
|
|
143
163
|
branch: "",
|
|
144
164
|
contextHandoff: "fresh",
|
|
145
165
|
directPush: false,
|
|
166
|
+
autoCreatePR: false,
|
|
167
|
+
skipReviewerRequest: false,
|
|
146
168
|
allowLocalState: false,
|
|
147
169
|
envNames: [],
|
|
148
170
|
envFromFiles: false,
|
|
@@ -240,6 +262,8 @@ export function parseCursorSdkConfig(value: unknown): CursorSdkConfig | undefine
|
|
|
240
262
|
if (branch) parsedCloud.branch = branch;
|
|
241
263
|
if (isCursorCloudContextHandoff(cloud.contextHandoff)) parsedCloud.contextHandoff = cloud.contextHandoff;
|
|
242
264
|
if (typeof cloud.directPush === "boolean") parsedCloud.directPush = cloud.directPush;
|
|
265
|
+
if (typeof cloud.autoCreatePR === "boolean") parsedCloud.autoCreatePR = cloud.autoCreatePR;
|
|
266
|
+
if (typeof cloud.skipReviewerRequest === "boolean") parsedCloud.skipReviewerRequest = cloud.skipReviewerRequest;
|
|
243
267
|
if (typeof cloud.allowLocalState === "boolean") parsedCloud.allowLocalState = cloud.allowLocalState;
|
|
244
268
|
if (envNames) parsedCloud.envNames = envNames;
|
|
245
269
|
if (typeof cloud.envFromFiles === "boolean") parsedCloud.envFromFiles = cloud.envFromFiles;
|
|
@@ -255,6 +279,7 @@ export function parseCursorSdkConfig(value: unknown): CursorSdkConfig | undefine
|
|
|
255
279
|
if (typeof local.sandbox === "boolean") parsedLocal.sandbox = local.sandbox;
|
|
256
280
|
if (typeof local.force === "boolean") parsedLocal.force = local.force;
|
|
257
281
|
if (typeof local.resume === "boolean") parsedLocal.resume = local.resume;
|
|
282
|
+
if (typeof local.useHttp1ForAgent === "boolean") parsedLocal.useHttp1ForAgent = local.useHttp1ForAgent;
|
|
258
283
|
const sandboxOptions = asRecord(local.sandboxOptions);
|
|
259
284
|
if (typeof sandboxOptions?.enabled === "boolean") parsedLocal.sandboxOptions = { enabled: sandboxOptions.enabled };
|
|
260
285
|
if (Object.keys(parsedLocal).length > 0) config.local = parsedLocal;
|
|
@@ -280,6 +305,25 @@ function readCursorSdkConfigFile(path: string): CursorSdkConfig {
|
|
|
280
305
|
}
|
|
281
306
|
}
|
|
282
307
|
|
|
308
|
+
export function loadCursorSdkConfigForUpdate(path: string): Record<string, unknown> {
|
|
309
|
+
if (!existsSync(path)) return {};
|
|
310
|
+
let source: string;
|
|
311
|
+
try {
|
|
312
|
+
source = readFileSync(path, "utf-8");
|
|
313
|
+
} catch (error) {
|
|
314
|
+
throw new Error(`Failed to read Cursor SDK config ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
315
|
+
}
|
|
316
|
+
let parsed: unknown;
|
|
317
|
+
try {
|
|
318
|
+
parsed = JSON.parse(source);
|
|
319
|
+
} catch {
|
|
320
|
+
throw new Error(`Invalid JSON in Cursor SDK config ${path}`);
|
|
321
|
+
}
|
|
322
|
+
const record = asRecord(parsed);
|
|
323
|
+
if (!record) throw new Error(`Invalid Cursor SDK config ${path}: expected a JSON object`);
|
|
324
|
+
return record;
|
|
325
|
+
}
|
|
326
|
+
|
|
283
327
|
export function loadCursorSdkUserConfig(path = getCursorSdkUserConfigPath()): CursorSdkConfig {
|
|
284
328
|
return readCursorSdkConfigFile(path);
|
|
285
329
|
}
|
|
@@ -296,7 +340,39 @@ export function loadCursorSdkConfig(options: LoadCursorSdkConfigOptions = {}): {
|
|
|
296
340
|
return project ? { user, project } : { user };
|
|
297
341
|
}
|
|
298
342
|
|
|
299
|
-
|
|
343
|
+
const CONFIG_LOCK_RETRY_MS = 20;
|
|
344
|
+
const CONFIG_LOCK_TIMEOUT_MS = 5_000;
|
|
345
|
+
const configLockWaitBuffer = new Int32Array(new SharedArrayBuffer(4));
|
|
346
|
+
|
|
347
|
+
function sleepForConfigLock(): void {
|
|
348
|
+
Atomics.wait(configLockWaitBuffer, 0, 0, CONFIG_LOCK_RETRY_MS);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function acquireCursorSdkConfigLock(path: string): () => void {
|
|
352
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
353
|
+
const lockPath = `${path}.lock`;
|
|
354
|
+
const deadline = Date.now() + CONFIG_LOCK_TIMEOUT_MS;
|
|
355
|
+
while (true) {
|
|
356
|
+
try {
|
|
357
|
+
const descriptor = openSync(lockPath, "wx", 0o600);
|
|
358
|
+
return () => {
|
|
359
|
+
try {
|
|
360
|
+
closeSync(descriptor);
|
|
361
|
+
} finally {
|
|
362
|
+
rmSync(lockPath, { force: true });
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
} catch (error) {
|
|
366
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
367
|
+
if (Date.now() >= deadline) {
|
|
368
|
+
throw new Error(`Timed out waiting for Cursor SDK config lock ${lockPath}; remove it only if no pi process is writing this config`);
|
|
369
|
+
}
|
|
370
|
+
sleepForConfigLock();
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function replaceJsonFile(path: string, config: object, mode?: number): void {
|
|
300
376
|
mkdirSync(dirname(path), { recursive: true });
|
|
301
377
|
const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
302
378
|
let replaced = false;
|
|
@@ -320,31 +396,61 @@ function replaceJsonFile(path: string, config: CursorSdkConfig, mode?: number):
|
|
|
320
396
|
}
|
|
321
397
|
}
|
|
322
398
|
|
|
399
|
+
function withCursorSdkConfigLock<T>(path: string, operation: () => T): T {
|
|
400
|
+
const release = acquireCursorSdkConfigLock(path);
|
|
401
|
+
try {
|
|
402
|
+
return operation();
|
|
403
|
+
} finally {
|
|
404
|
+
release();
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function updateCursorSdkConfig(
|
|
409
|
+
path: string,
|
|
410
|
+
update: (current: Record<string, unknown>) => Record<string, unknown>,
|
|
411
|
+
options: { newFileMode?: number } = {},
|
|
412
|
+
): Record<string, unknown> {
|
|
413
|
+
return withCursorSdkConfigLock(path, () => {
|
|
414
|
+
const updated = update(loadCursorSdkConfigForUpdate(path));
|
|
415
|
+
const mode = existsSync(path) ? statSync(path).mode & 0o777 : options.newFileMode;
|
|
416
|
+
replaceJsonFile(path, updated, mode);
|
|
417
|
+
return updated;
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
323
421
|
export function saveCursorSdkUserConfig(config: CursorSdkConfig, path = getCursorSdkUserConfigPath()): void {
|
|
324
|
-
|
|
325
|
-
replaceJsonFile(path, config, mode);
|
|
422
|
+
updateCursorSdkConfig(path, () => ({ ...config }), { newFileMode: 0o600 });
|
|
326
423
|
}
|
|
327
424
|
|
|
328
425
|
export function saveCursorSdkProjectConfig(cwd: string, config: CursorSdkConfig, configDirName = CONFIG_DIR_NAME): void {
|
|
329
426
|
const path = getCursorSdkProjectConfigPath(cwd, configDirName);
|
|
330
|
-
|
|
331
|
-
replaceJsonFile(path, config, mode);
|
|
427
|
+
updateCursorSdkConfig(path, () => ({ ...config }));
|
|
332
428
|
}
|
|
333
429
|
|
|
334
430
|
export function mergeCursorSdkConfig(base: CursorSdkConfig, patch: CursorSdkConfig): CursorSdkConfig {
|
|
431
|
+
return mergeCursorSdkConfigForUpdate({ ...base }, patch) as CursorSdkConfig;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export function mergeCursorSdkConfigForUpdate(
|
|
435
|
+
base: Record<string, unknown>,
|
|
436
|
+
patch: CursorSdkConfig,
|
|
437
|
+
): Record<string, unknown> {
|
|
438
|
+
const baseCloud = asRecord(base.cloud);
|
|
439
|
+
const baseLocal = asRecord(base.local);
|
|
440
|
+
const baseSandboxOptions = asRecord(baseLocal?.sandboxOptions);
|
|
335
441
|
return {
|
|
336
442
|
...base,
|
|
337
443
|
...patch,
|
|
338
|
-
...(
|
|
339
|
-
? { cloud: { ...
|
|
444
|
+
...(baseCloud || patch.cloud
|
|
445
|
+
? { cloud: { ...baseCloud, ...patch.cloud } }
|
|
340
446
|
: {}),
|
|
341
|
-
...(
|
|
447
|
+
...(baseLocal || patch.local
|
|
342
448
|
? {
|
|
343
449
|
local: {
|
|
344
|
-
...
|
|
450
|
+
...baseLocal,
|
|
345
451
|
...patch.local,
|
|
346
|
-
...(
|
|
347
|
-
? { sandboxOptions: { ...
|
|
452
|
+
...(baseSandboxOptions || patch.local?.sandboxOptions
|
|
453
|
+
? { sandboxOptions: { ...baseSandboxOptions, ...patch.local?.sandboxOptions } }
|
|
348
454
|
: {}),
|
|
349
455
|
},
|
|
350
456
|
}
|
|
@@ -356,7 +462,10 @@ export function cursorFastDefaultsFromConfig(config: CursorSdkConfig | undefined
|
|
|
356
462
|
return new Map(Object.entries(config?.fastDefaults ?? {}));
|
|
357
463
|
}
|
|
358
464
|
|
|
359
|
-
export function withCursorFastDefaults
|
|
465
|
+
export function withCursorFastDefaults<T extends object>(
|
|
466
|
+
config: T,
|
|
467
|
+
fastDefaults: Map<string, boolean>,
|
|
468
|
+
): T & { fastDefaults: Record<string, boolean> } {
|
|
360
469
|
return {
|
|
361
470
|
...config,
|
|
362
471
|
fastDefaults: Object.fromEntries([...fastDefaults.entries()].sort(([a], [b]) => a.localeCompare(b))),
|
|
@@ -423,6 +532,7 @@ const RUNTIME_ORDER: CursorFieldSource[] = ["cli", "environment", "session", "pr
|
|
|
423
532
|
const CLOUD_ORDER: CursorFieldSource[] = ["cli", "environment", "session", "user", "builtin"];
|
|
424
533
|
const LOCAL_ORDER: CursorFieldSource[] = ["cli", "environment", "project", "user", "builtin"];
|
|
425
534
|
const LOCAL_FORCE_ORDER: CursorFieldSource[] = ["cli", "environment", "builtin"];
|
|
535
|
+
const HTTP1_ORDER: CursorFieldSource[] = ["session", "environment", "user", "builtin"];
|
|
426
536
|
|
|
427
537
|
function buildFieldLayers<T>(order: CursorFieldSource[], values: CursorFieldValues<T>): Array<CursorResolvedSetting<T> | undefined> {
|
|
428
538
|
return order.map((source) => (source === "builtin" ? resolved("builtin", values.builtin as T) : valueFrom(source, values[source])));
|
|
@@ -453,6 +563,8 @@ export function cursorSdkConfigFromEnv(env: Record<string, string | undefined> =
|
|
|
453
563
|
'"never", "fresh", or "bootstrap"',
|
|
454
564
|
);
|
|
455
565
|
const directPush = parseOptionalEnvBoolean(env[CURSOR_CLOUD_DIRECT_PUSH_ENV]);
|
|
566
|
+
const autoCreatePR = parseOptionalEnvBoolean(env[CURSOR_CLOUD_AUTO_CREATE_PR_ENV]);
|
|
567
|
+
const skipReviewerRequest = parseOptionalEnvBoolean(env[CURSOR_CLOUD_SKIP_REVIEWER_REQUEST_ENV]);
|
|
456
568
|
const allowLocalState = parseOptionalEnvBoolean(env[CURSOR_CLOUD_ALLOW_LOCAL_STATE_ENV]);
|
|
457
569
|
const envNames = parseExplicitCursorCloudEnvNames(env[CURSOR_CLOUD_ENV_ENV], CURSOR_CLOUD_ENV_ENV);
|
|
458
570
|
const envFromFiles = parseOptionalEnvBoolean(env[CURSOR_CLOUD_ENV_FROM_FILES_ENV]);
|
|
@@ -466,6 +578,8 @@ export function cursorSdkConfigFromEnv(env: Record<string, string | undefined> =
|
|
|
466
578
|
branch !== undefined ||
|
|
467
579
|
isCursorCloudContextHandoff(contextHandoff) ||
|
|
468
580
|
directPush !== undefined ||
|
|
581
|
+
autoCreatePR !== undefined ||
|
|
582
|
+
skipReviewerRequest !== undefined ||
|
|
469
583
|
allowLocalState !== undefined ||
|
|
470
584
|
envNames !== undefined ||
|
|
471
585
|
envFromFiles !== undefined ||
|
|
@@ -477,6 +591,8 @@ export function cursorSdkConfigFromEnv(env: Record<string, string | undefined> =
|
|
|
477
591
|
...(branch !== undefined ? { branch } : {}),
|
|
478
592
|
...(isCursorCloudContextHandoff(contextHandoff) ? { contextHandoff } : {}),
|
|
479
593
|
...(directPush !== undefined ? { directPush } : {}),
|
|
594
|
+
...(autoCreatePR !== undefined ? { autoCreatePR } : {}),
|
|
595
|
+
...(skipReviewerRequest !== undefined ? { skipReviewerRequest } : {}),
|
|
480
596
|
...(allowLocalState !== undefined ? { allowLocalState } : {}),
|
|
481
597
|
...(envNames !== undefined ? { envNames } : {}),
|
|
482
598
|
...(envFromFiles !== undefined ? { envFromFiles } : {}),
|
|
@@ -488,12 +604,14 @@ export function cursorSdkConfigFromEnv(env: Record<string, string | undefined> =
|
|
|
488
604
|
const sandbox = parseOptionalEnvBoolean(env[CURSOR_SANDBOX_ENV]);
|
|
489
605
|
const force = parseOptionalEnvBoolean(env[CURSOR_LOCAL_FORCE_ENV]);
|
|
490
606
|
const resume = parseOptionalEnvBoolean(env[CURSOR_LOCAL_RESUME_ENV]);
|
|
491
|
-
|
|
607
|
+
const useHttp1ForAgent = parseOptionalEnvBoolean(env[CURSOR_HTTP1_ENV]);
|
|
608
|
+
if (autoReview !== undefined || sandbox !== undefined || force !== undefined || resume !== undefined || useHttp1ForAgent !== undefined) {
|
|
492
609
|
config.local = {
|
|
493
610
|
...(autoReview !== undefined ? { autoReview } : {}),
|
|
494
611
|
...(sandbox !== undefined ? { sandboxOptions: { enabled: sandbox } } : {}),
|
|
495
612
|
...(force !== undefined ? { force } : {}),
|
|
496
613
|
...(resume !== undefined ? { resume } : {}),
|
|
614
|
+
...(useHttp1ForAgent !== undefined ? { useHttp1ForAgent } : {}),
|
|
497
615
|
};
|
|
498
616
|
}
|
|
499
617
|
return config;
|
|
@@ -567,6 +685,28 @@ export function resolveCursorSdkConfig(options: ResolveCursorSdkConfigOptions =
|
|
|
567
685
|
},
|
|
568
686
|
(value) => (value ? 1 : 0),
|
|
569
687
|
),
|
|
688
|
+
autoCreatePR: resolveSafetyField(
|
|
689
|
+
CLOUD_ORDER,
|
|
690
|
+
{
|
|
691
|
+
cli: cli?.cloud?.autoCreatePR,
|
|
692
|
+
environment: env.cloud?.autoCreatePR,
|
|
693
|
+
session: session?.cloud?.autoCreatePR,
|
|
694
|
+
user: user?.cloud?.autoCreatePR,
|
|
695
|
+
builtin: builtIn.cloud.autoCreatePR,
|
|
696
|
+
},
|
|
697
|
+
(value) => (value ? 1 : 0),
|
|
698
|
+
),
|
|
699
|
+
skipReviewerRequest: resolveSafetyField(
|
|
700
|
+
CLOUD_ORDER,
|
|
701
|
+
{
|
|
702
|
+
cli: cli?.cloud?.skipReviewerRequest,
|
|
703
|
+
environment: env.cloud?.skipReviewerRequest,
|
|
704
|
+
session: session?.cloud?.skipReviewerRequest,
|
|
705
|
+
user: user?.cloud?.skipReviewerRequest,
|
|
706
|
+
builtin: builtIn.cloud.skipReviewerRequest,
|
|
707
|
+
},
|
|
708
|
+
(value) => (value ? 1 : 0),
|
|
709
|
+
),
|
|
570
710
|
allowLocalState: resolveSafetyField(
|
|
571
711
|
CLOUD_ORDER,
|
|
572
712
|
{
|
|
@@ -638,6 +778,12 @@ export function resolveCursorSdkConfig(options: ResolveCursorSdkConfigOptions =
|
|
|
638
778
|
user: user?.local?.resume,
|
|
639
779
|
builtin: true,
|
|
640
780
|
}),
|
|
781
|
+
useHttp1ForAgent: resolveOrdinaryField(HTTP1_ORDER, {
|
|
782
|
+
session: session?.local?.useHttp1ForAgent,
|
|
783
|
+
environment: env.local?.useHttp1ForAgent,
|
|
784
|
+
user: user?.local?.useHttp1ForAgent,
|
|
785
|
+
builtin: false,
|
|
786
|
+
}),
|
|
641
787
|
},
|
|
642
788
|
};
|
|
643
789
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { CursorSdkModule } from "./cursor-sdk-runtime.js";
|
|
2
|
+
import type { CursorResolvedSetting } from "./cursor-config.js";
|
|
3
|
+
import { asRecord } from "./cursor-record-utils.js";
|
|
4
|
+
|
|
5
|
+
export const CURSOR_HTTP1_ENTRY_TYPE = "cursor-http1-state";
|
|
6
|
+
|
|
7
|
+
export interface CursorHttp1EntryData {
|
|
8
|
+
enabled: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type CursorHttp1Sdk = {
|
|
12
|
+
Cursor: Pick<CursorSdkModule["Cursor"], "configure">;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
let sessionCursorHttp1Enabled: boolean | undefined;
|
|
16
|
+
let globalPreferenceAuthoritative = false;
|
|
17
|
+
let configuredCursor: CursorHttp1Sdk["Cursor"] | undefined;
|
|
18
|
+
|
|
19
|
+
export function isCursorHttp1EntryData(value: unknown): value is CursorHttp1EntryData {
|
|
20
|
+
return typeof asRecord(value)?.enabled === "boolean";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getStoredCursorHttp1Enabled(): boolean | undefined {
|
|
24
|
+
return sessionCursorHttp1Enabled;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function setStoredCursorHttp1Enabled(enabled: boolean | undefined): void {
|
|
28
|
+
sessionCursorHttp1Enabled = enabled;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getResolvedSessionCursorHttp1Enabled(): boolean | undefined {
|
|
32
|
+
return globalPreferenceAuthoritative ? undefined : sessionCursorHttp1Enabled;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function setCursorHttp1GlobalPreferenceAuthoritative(authoritative: boolean): void {
|
|
36
|
+
globalPreferenceAuthoritative = authoritative;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function clearCursorSdkHttp1(): void {
|
|
40
|
+
if (configuredCursor === undefined) return;
|
|
41
|
+
configuredCursor.configure({ local: { useHttp1ForAgent: null } });
|
|
42
|
+
configuredCursor = undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function configureCursorSdkHttp1(
|
|
46
|
+
sdk: CursorHttp1Sdk,
|
|
47
|
+
setting: CursorResolvedSetting<boolean>,
|
|
48
|
+
): boolean | undefined {
|
|
49
|
+
if (setting.source !== "builtin") {
|
|
50
|
+
sdk.Cursor.configure({ local: { useHttp1ForAgent: setting.value } });
|
|
51
|
+
configuredCursor = sdk.Cursor;
|
|
52
|
+
return setting.value;
|
|
53
|
+
}
|
|
54
|
+
if (configuredCursor === sdk.Cursor) clearCursorSdkHttp1();
|
|
55
|
+
else configuredCursor = undefined;
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const __testUtils = {
|
|
60
|
+
reset(): void {
|
|
61
|
+
sessionCursorHttp1Enabled = undefined;
|
|
62
|
+
globalPreferenceAuthoritative = false;
|
|
63
|
+
configuredCursor = undefined;
|
|
64
|
+
},
|
|
65
|
+
};
|
|
@@ -105,6 +105,11 @@ export interface CursorLiveRunCoordinator {
|
|
|
105
105
|
|
|
106
106
|
type CursorLiveBridgeMatcher = Pick<CursorPiToolBridgeRun, "hasPendingPiToolCallId">;
|
|
107
107
|
|
|
108
|
+
function isPendingBridgeToolRequest(run: CursorLiveRun, request: CursorPiBridgeToolRequest): boolean {
|
|
109
|
+
const bridgeRun = [run.bridgeRun, run.sessionBridgeRun].find((candidate) => candidate?.id === request.runId);
|
|
110
|
+
return bridgeRun?.hasPendingPiToolCallId(request.piToolCallId) === true;
|
|
111
|
+
}
|
|
112
|
+
|
|
108
113
|
export interface CursorLiveRunRecord {
|
|
109
114
|
id: string;
|
|
110
115
|
disposed: boolean;
|
|
@@ -384,7 +389,9 @@ export function createCursorLiveRunCoordinator(deps: CursorLiveRunCoordinatorDep
|
|
|
384
389
|
const requests: CursorPiBridgeToolRequest[] = [];
|
|
385
390
|
while (run.pendingEvents[0]?.type === "bridge-tool") {
|
|
386
391
|
const event = run.pendingEvents.shift();
|
|
387
|
-
if (event?.type === "bridge-tool"
|
|
392
|
+
if (event?.type === "bridge-tool" && isPendingBridgeToolRequest(run, event.request)) {
|
|
393
|
+
requests.push(event.request);
|
|
394
|
+
}
|
|
388
395
|
}
|
|
389
396
|
return requests;
|
|
390
397
|
},
|
|
@@ -54,11 +54,18 @@ class CursorPiToolBridgeToolExecutionAbortTracker {
|
|
|
54
54
|
for (const toolCallId of [...this.activeExecutions.keys()]) this.finish(toolCallId);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
abort(toolCallId: string, reason: string): boolean {
|
|
58
|
+
const execution = this.activeExecutions.get(toolCallId);
|
|
59
|
+
if (!execution) return false;
|
|
60
|
+
this.cancelExecution(execution, reason);
|
|
61
|
+
this.abortExecution(execution);
|
|
62
|
+
this.finish(toolCallId);
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
57
66
|
abortAll(reason: string): void {
|
|
58
67
|
for (const execution of [...this.activeExecutions.values()]) {
|
|
59
|
-
this.
|
|
60
|
-
this.abortExecution(execution);
|
|
61
|
-
this.finish(execution.toolCallId);
|
|
68
|
+
this.abort(execution.toolCallId, reason);
|
|
62
69
|
}
|
|
63
70
|
}
|
|
64
71
|
|
|
@@ -1,2 +1,8 @@
|
|
|
1
1
|
export const MCP_SERVER_NAME = "pi_tools";
|
|
2
2
|
export const MCP_ENDPOINT_ROOT = "/cursor-pi-tool-bridge";
|
|
3
|
+
|
|
4
|
+
const CURSOR_PI_BRIDGE_TOOL_CALL_ID_PATTERN = /^cursor-pi-bridge-run-[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}-tool-\d+$/i;
|
|
5
|
+
|
|
6
|
+
export function isCursorPiBridgeToolCallId(toolCallId: string): boolean {
|
|
7
|
+
return CURSOR_PI_BRIDGE_TOOL_CALL_ID_PATTERN.test(toolCallId);
|
|
8
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { parseEnvBoolean } from "./cursor-env-boolean.js";
|
|
2
|
+
import { resolveCursorMcpToolTimeoutMs } from "./cursor-mcp-timeout-override.js";
|
|
2
3
|
|
|
3
4
|
export const CURSOR_PI_TOOL_BRIDGE_ENV = "PI_CURSOR_PI_TOOL_BRIDGE";
|
|
4
5
|
export const CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV = "PI_CURSOR_EXPOSE_BUILTIN_TOOLS";
|
|
6
|
+
export const CURSOR_PI_TOOL_BRIDGE_CALL_TIMEOUT_MS_ENV = "PI_CURSOR_PI_BRIDGE_CALL_TIMEOUT_MS";
|
|
5
7
|
|
|
6
8
|
export function resolveCursorPiToolBridgeEnabled(env: Record<string, string | undefined> = process.env): boolean {
|
|
7
9
|
return parseEnvBoolean(env[CURSOR_PI_TOOL_BRIDGE_ENV], true);
|
|
@@ -10,3 +12,10 @@ export function resolveCursorPiToolBridgeEnabled(env: Record<string, string | un
|
|
|
10
12
|
export function resolveCursorPiToolBridgeBuiltinsEnabled(env: Record<string, string | undefined> = process.env): boolean {
|
|
11
13
|
return parseEnvBoolean(env[CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV], false);
|
|
12
14
|
}
|
|
15
|
+
|
|
16
|
+
export function resolveCursorPiToolBridgeCallTimeoutMs(env: Record<string, string | undefined> = process.env): number {
|
|
17
|
+
const mcpToolTimeoutMs = resolveCursorMcpToolTimeoutMs(env);
|
|
18
|
+
const parsed = Number(env[CURSOR_PI_TOOL_BRIDGE_CALL_TIMEOUT_MS_ENV]?.trim());
|
|
19
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return mcpToolTimeoutMs;
|
|
20
|
+
return Math.min(Math.max(Math.trunc(parsed), 1), mcpToolTimeoutMs);
|
|
21
|
+
}
|