pi-cursor-bridge 0.2.0 → 0.2.1
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/.codex-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/dist/cursor-bridge.mjs +466 -126
- package/dist/cursor-lifecycle-supervisor.mjs +41 -11
- package/extensions/index.ts +1 -1
- package/package.json +2 -2
- package/skills/cursor-delegate/SKILL.md +4 -4
- package/skills/cursor-delegate/references/delegation-contract.md +7 -7
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-bridge",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.1+codex.20260909075353",
|
|
4
4
|
"description": "Evidence-backed Cursor Context Engine search and bounded Cursor Agent execution, including a UI-suppressed minimal runtime.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Vanyangyang"
|
package/README.md
CHANGED
|
@@ -10,6 +10,6 @@ pi install npm:pi-cursor-bridge
|
|
|
10
10
|
|
|
11
11
|
Restart Pi after installation. Initialize the current project by asking Pi to initialize Cursor Bridge for the absolute project path, then use it normally. The package includes the `cce-routing` and `cursor-delegate` Skills and registers the native Cursor Bridge MCP tools directly in Pi.
|
|
12
12
|
|
|
13
|
-
Pi wrapper 0.2.
|
|
13
|
+
Pi wrapper 0.2.1 embeds Cursor Bridge 6.0.1, including explicit request provenance for CCE and delegated tasks. Its result contract returns a compact receipt for background `cursor_do` work: poll `cursor_status(task_id)` normally, retrieve the terminal raw reply with `cursor_status(task_id, detail="result")`, check `isError` before using its content, and use `detail="full"` only when diagnostics are needed. Cursor must be installed and signed in. Current end-to-end compatibility claims remain scoped to the environments documented in the main repository.
|
|
14
14
|
|
|
15
15
|
Full documentation: [English](https://github.com/Vanyangyang/cursor-bridge#readme) · [简体中文](https://github.com/Vanyangyang/cursor-bridge/blob/main/README.zh-CN.md)
|
package/dist/cursor-bridge.mjs
CHANGED
|
@@ -10975,7 +10975,7 @@ function powershellWindowScript(options) {
|
|
|
10975
10975
|
` Remove-Item -LiteralPath '${showFlagPath}' -Force -ErrorAction SilentlyContinue`,
|
|
10976
10976
|
"}"
|
|
10977
10977
|
].join("\n");
|
|
10978
|
-
const apply = lifetime ? lifetimeLoop : loop ? `for ($i = 0; $i -lt ${iterations}; $i++) { ${hideIfAllowed}; Start-Sleep -Milliseconds ${intervalMs} }` : `$changed = [CursorBridgeWindowControl]::Apply(${targetPid}, ${show}); [Console]::Out.Write($changed)`;
|
|
10978
|
+
const apply = lifetime ? lifetimeLoop : loop ? `for ($i = 0; $i -lt ${iterations}; $i++) { ${hideIfAllowed}; Start-Sleep -Milliseconds ${intervalMs} }` : `$changed = [CursorBridgeWindowControl]::Apply(${targetPid}, ${show}${options.scope === "agents" ? ", $true" : ""}); [Console]::Out.Write($changed)`;
|
|
10979
10979
|
return `$ErrorActionPreference = 'Stop'
|
|
10980
10980
|
Add-Type -TypeDefinition @'
|
|
10981
10981
|
${WINDOW_CONTROL_TYPE}
|
|
@@ -10989,6 +10989,8 @@ function setCursorWindowPresentation(options = {}) {
|
|
|
10989
10989
|
const platform = options.platform || process.platform;
|
|
10990
10990
|
const action = String(options.action || "").trim().toLowerCase();
|
|
10991
10991
|
if (!["hide", "show"].includes(action)) throw new Error(`unsupported Cursor window action: ${options.action}`);
|
|
10992
|
+
const scope = options.scope ?? "process";
|
|
10993
|
+
if (!["process", "agents"].includes(scope)) throw new Error(`unsupported Cursor window scope: ${scope}`);
|
|
10992
10994
|
if (platform !== "win32") {
|
|
10993
10995
|
return { supported: false, applied: false, action, reason: `window control is not implemented for ${platform}` };
|
|
10994
10996
|
}
|
|
@@ -10999,12 +11001,14 @@ function setCursorWindowPresentation(options = {}) {
|
|
|
10999
11001
|
}
|
|
11000
11002
|
const showFlagPath = resolve(options.showFlagPath || join(dirname(resolveCursorRuntimeFile()), `show-${pid}.flag`));
|
|
11001
11003
|
try {
|
|
11002
|
-
if (
|
|
11003
|
-
|
|
11004
|
-
|
|
11004
|
+
if (scope === "process") {
|
|
11005
|
+
if (action === "show") {
|
|
11006
|
+
mkdirSync(dirname(showFlagPath), { recursive: true });
|
|
11007
|
+
writeFileSync(showFlagPath, `${pid}
|
|
11005
11008
|
`, { encoding: "utf8", mode: 384 });
|
|
11006
|
-
|
|
11007
|
-
|
|
11009
|
+
} else {
|
|
11010
|
+
rmSync(showFlagPath, { force: true });
|
|
11011
|
+
}
|
|
11008
11012
|
}
|
|
11009
11013
|
} catch (error2) {
|
|
11010
11014
|
return {
|
|
@@ -11018,7 +11022,7 @@ function setCursorWindowPresentation(options = {}) {
|
|
|
11018
11022
|
}
|
|
11019
11023
|
const run = options.execFileSyncImpl || execFileSync;
|
|
11020
11024
|
try {
|
|
11021
|
-
const script = powershellWindowScript({ pid, action });
|
|
11025
|
+
const script = powershellWindowScript({ pid, action, scope });
|
|
11022
11026
|
const output = run("powershell.exe", [
|
|
11023
11027
|
"-NoLogo",
|
|
11024
11028
|
"-NoProfile",
|
|
@@ -11034,9 +11038,18 @@ function setCursorWindowPresentation(options = {}) {
|
|
|
11034
11038
|
timeout: Number(options.timeoutMs || 15e3)
|
|
11035
11039
|
});
|
|
11036
11040
|
const changedWindows = Number(String(output || "").trim() || 0);
|
|
11037
|
-
return {
|
|
11041
|
+
return {
|
|
11042
|
+
supported: true,
|
|
11043
|
+
applied: scope !== "agents" || changedWindows > 0,
|
|
11044
|
+
action,
|
|
11045
|
+
scope,
|
|
11046
|
+
port,
|
|
11047
|
+
pid,
|
|
11048
|
+
changedWindows,
|
|
11049
|
+
...scope === "process" ? { showFlagPath } : {}
|
|
11050
|
+
};
|
|
11038
11051
|
} catch (error2) {
|
|
11039
|
-
if (action === "show") rmSync(showFlagPath, { force: true });
|
|
11052
|
+
if (action === "show" && scope === "process") rmSync(showFlagPath, { force: true });
|
|
11040
11053
|
return {
|
|
11041
11054
|
supported: true,
|
|
11042
11055
|
applied: false,
|
|
@@ -11100,6 +11113,7 @@ var init_cursor_runtime = __esm({
|
|
|
11100
11113
|
using System;
|
|
11101
11114
|
using System.Runtime.InteropServices;
|
|
11102
11115
|
using System.Text;
|
|
11116
|
+
using System.Collections.Generic;
|
|
11103
11117
|
|
|
11104
11118
|
public static class CursorBridgeWindowControl {
|
|
11105
11119
|
[StructLayout(LayoutKind.Sequential)]
|
|
@@ -11119,6 +11133,7 @@ public static class CursorBridgeWindowControl {
|
|
|
11119
11133
|
[DllImport("user32.dll", EntryPoint = "IsWindowArranged")] private static extern bool IsWindowArranged(IntPtr hWnd);
|
|
11120
11134
|
[DllImport("user32.dll")] private static extern bool GetWindowRect(IntPtr hWnd, out RECT rect);
|
|
11121
11135
|
[DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern int GetWindowTextLengthW(IntPtr hWnd);
|
|
11136
|
+
[DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern int GetWindowTextW(IntPtr hWnd, StringBuilder text, int maxCount);
|
|
11122
11137
|
[DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern int GetClassNameW(IntPtr hWnd, StringBuilder className, int maxCount);
|
|
11123
11138
|
[DllImport("user32.dll")] private static extern bool ShowWindowAsync(IntPtr hWnd, int command);
|
|
11124
11139
|
[DllImport("user32.dll")] private static extern bool SetWindowPos(IntPtr hWnd, IntPtr insertAfter, int x, int y, int width, int height, uint flags);
|
|
@@ -11142,7 +11157,12 @@ public static class CursorBridgeWindowControl {
|
|
|
11142
11157
|
}
|
|
11143
11158
|
|
|
11144
11159
|
public static int Apply(int expectedProcessId, bool show) {
|
|
11160
|
+
return Apply(expectedProcessId, show, false);
|
|
11161
|
+
}
|
|
11162
|
+
|
|
11163
|
+
public static int Apply(int expectedProcessId, bool show, bool agentsOnly) {
|
|
11145
11164
|
int changed = 0;
|
|
11165
|
+
List<IntPtr> windows = new List<IntPtr>();
|
|
11146
11166
|
EnumWindows((hWnd, lParam) => {
|
|
11147
11167
|
uint processId;
|
|
11148
11168
|
GetWindowThreadProcessId(hWnd, out processId);
|
|
@@ -11150,6 +11170,17 @@ public static class CursorBridgeWindowControl {
|
|
|
11150
11170
|
StringBuilder className = new StringBuilder(256);
|
|
11151
11171
|
GetClassNameW(hWnd, className, className.Capacity);
|
|
11152
11172
|
if (!String.Equals(className.ToString(), "Chrome_WidgetWin_1", StringComparison.Ordinal)) return true;
|
|
11173
|
+
if (agentsOnly) {
|
|
11174
|
+
StringBuilder title = new StringBuilder(GetWindowTextLengthW(hWnd) + 1);
|
|
11175
|
+
GetWindowTextW(hWnd, title, title.Capacity);
|
|
11176
|
+
if (!String.Equals(title.ToString(), "Cursor Agents", StringComparison.Ordinal)) return true;
|
|
11177
|
+
}
|
|
11178
|
+
windows.Add(hWnd);
|
|
11179
|
+
return true;
|
|
11180
|
+
}, IntPtr.Zero);
|
|
11181
|
+
// Automatic recovery must never broaden an absent or ambiguous Agents match.
|
|
11182
|
+
if (agentsOnly && windows.Count != 1) return 0;
|
|
11183
|
+
foreach (IntPtr hWnd in windows) {
|
|
11153
11184
|
bool visible = IsWindowVisible(hWnd);
|
|
11154
11185
|
if (show) {
|
|
11155
11186
|
// SWP_SHOWWINDOW + SWP_NOACTIVATE preserves minimized/maximized/arranged
|
|
@@ -11177,8 +11208,7 @@ public static class CursorBridgeWindowControl {
|
|
|
11177
11208
|
if (restored || pulsed || redrawn) changed++;
|
|
11178
11209
|
}
|
|
11179
11210
|
if (!show && visible) { if (ShowWindowAsync(hWnd, 0)) changed++; }
|
|
11180
|
-
|
|
11181
|
-
}, IntPtr.Zero);
|
|
11211
|
+
}
|
|
11182
11212
|
return changed;
|
|
11183
11213
|
}
|
|
11184
11214
|
}
|
|
@@ -22356,6 +22386,272 @@ var import_subprotocol = __toESM(require_subprotocol(), 1);
|
|
|
22356
22386
|
var import_websocket = __toESM(require_websocket(), 1);
|
|
22357
22387
|
var import_websocket_server = __toESM(require_websocket_server(), 1);
|
|
22358
22388
|
|
|
22389
|
+
// cdp-client.mjs
|
|
22390
|
+
var ERROR_JSON_LIMIT = 2048;
|
|
22391
|
+
function positiveTimeout(name, value, fallback) {
|
|
22392
|
+
const timeout = value === void 0 ? fallback : Number(value);
|
|
22393
|
+
if (!Number.isFinite(timeout) || timeout <= 0) {
|
|
22394
|
+
const error2 = new TypeError(`${name} must be a positive finite number`);
|
|
22395
|
+
error2.code = "CDP_INVALID_TIMEOUT";
|
|
22396
|
+
error2.stage = "configuration";
|
|
22397
|
+
error2.cdp = { stage: "configuration", elapsedMs: 0 };
|
|
22398
|
+
throw error2;
|
|
22399
|
+
}
|
|
22400
|
+
return timeout;
|
|
22401
|
+
}
|
|
22402
|
+
function clientError(message, { code, stage, method, cause, elapsedMs = 0 } = {}) {
|
|
22403
|
+
const error2 = new Error(message, cause ? { cause } : void 0);
|
|
22404
|
+
error2.name = "CdpClientError";
|
|
22405
|
+
error2.code = code || cause?.code || "CDP_CLIENT_ERROR";
|
|
22406
|
+
error2.stage = stage || "unknown";
|
|
22407
|
+
if (method) error2.method = method;
|
|
22408
|
+
error2.cdp = {
|
|
22409
|
+
stage: error2.stage,
|
|
22410
|
+
...method ? { method } : {},
|
|
22411
|
+
elapsedMs: Math.max(0, Number(elapsedMs) || 0)
|
|
22412
|
+
};
|
|
22413
|
+
return error2;
|
|
22414
|
+
}
|
|
22415
|
+
function boundedJson(value) {
|
|
22416
|
+
let text;
|
|
22417
|
+
try {
|
|
22418
|
+
text = JSON.stringify(value);
|
|
22419
|
+
} catch {
|
|
22420
|
+
text = String(value);
|
|
22421
|
+
}
|
|
22422
|
+
if (text === void 0) text = String(value);
|
|
22423
|
+
return text.length <= ERROR_JSON_LIMIT ? text : `${text.slice(0, ERROR_JSON_LIMIT)}\u2026`;
|
|
22424
|
+
}
|
|
22425
|
+
function makeClient(wsUrl, options = {}) {
|
|
22426
|
+
const createdAt = Date.now();
|
|
22427
|
+
const connectTimeoutMs = positiveTimeout("connectTimeoutMs", options.connectTimeoutMs, 5e3);
|
|
22428
|
+
const commandTimeoutMs = positiveTimeout("commandTimeoutMs", options.commandTimeoutMs, 3e4);
|
|
22429
|
+
const WebSocketImpl = options.WebSocketImpl || import_websocket.default;
|
|
22430
|
+
const websocketOptions = { handshakeTimeout: connectTimeoutMs };
|
|
22431
|
+
if (options.origin !== void 0) websocketOptions.origin = options.origin;
|
|
22432
|
+
let ws;
|
|
22433
|
+
let state = "connecting";
|
|
22434
|
+
let nextId = 0;
|
|
22435
|
+
let connectTimer;
|
|
22436
|
+
let readyResolve;
|
|
22437
|
+
let readyReject;
|
|
22438
|
+
const pending = /* @__PURE__ */ new Map();
|
|
22439
|
+
const ready = new Promise((resolve8, reject) => {
|
|
22440
|
+
readyResolve = resolve8;
|
|
22441
|
+
readyReject = reject;
|
|
22442
|
+
});
|
|
22443
|
+
void ready.catch(() => {
|
|
22444
|
+
});
|
|
22445
|
+
const terminate = () => {
|
|
22446
|
+
if (!ws) return;
|
|
22447
|
+
try {
|
|
22448
|
+
if (typeof ws.terminate === "function") ws.terminate();
|
|
22449
|
+
else ws.close();
|
|
22450
|
+
} catch {
|
|
22451
|
+
}
|
|
22452
|
+
};
|
|
22453
|
+
const rejectPending = (error2) => {
|
|
22454
|
+
for (const entry of pending.values()) {
|
|
22455
|
+
clearTimeout(entry.timer);
|
|
22456
|
+
entry.reject(typeof error2 === "function" ? error2(entry) : error2);
|
|
22457
|
+
}
|
|
22458
|
+
pending.clear();
|
|
22459
|
+
};
|
|
22460
|
+
const rejectReady = (error2) => {
|
|
22461
|
+
if (state !== "connecting") return;
|
|
22462
|
+
state = "failed";
|
|
22463
|
+
clearTimeout(connectTimer);
|
|
22464
|
+
readyReject(error2);
|
|
22465
|
+
};
|
|
22466
|
+
const failConnection = (cause) => {
|
|
22467
|
+
const error2 = clientError(`CDP WebSocket connection failed: ${cause?.message || cause}`, {
|
|
22468
|
+
stage: "connect",
|
|
22469
|
+
cause,
|
|
22470
|
+
elapsedMs: Date.now() - createdAt
|
|
22471
|
+
});
|
|
22472
|
+
rejectReady(error2);
|
|
22473
|
+
terminate();
|
|
22474
|
+
return error2;
|
|
22475
|
+
};
|
|
22476
|
+
const failSocket = (cause) => {
|
|
22477
|
+
const error2 = clientError(`CDP WebSocket failed: ${cause?.message || cause}`, {
|
|
22478
|
+
stage: "socket",
|
|
22479
|
+
cause,
|
|
22480
|
+
elapsedMs: Date.now() - createdAt
|
|
22481
|
+
});
|
|
22482
|
+
if (state === "connecting") return failConnection(cause);
|
|
22483
|
+
if (state === "open") state = "failed";
|
|
22484
|
+
rejectPending((entry) => clientError(`CDP WebSocket failed during ${entry.method}: ${cause?.message || cause}`, {
|
|
22485
|
+
stage: "socket",
|
|
22486
|
+
method: entry.method,
|
|
22487
|
+
cause,
|
|
22488
|
+
elapsedMs: Date.now() - entry.startedAt
|
|
22489
|
+
}));
|
|
22490
|
+
terminate();
|
|
22491
|
+
return error2;
|
|
22492
|
+
};
|
|
22493
|
+
try {
|
|
22494
|
+
ws = new WebSocketImpl(wsUrl, websocketOptions);
|
|
22495
|
+
} catch (cause) {
|
|
22496
|
+
failConnection(cause);
|
|
22497
|
+
}
|
|
22498
|
+
if (ws) {
|
|
22499
|
+
connectTimer = setTimeout(() => {
|
|
22500
|
+
const error2 = clientError(`CDP WebSocket connection timed out (${connectTimeoutMs}ms)`, {
|
|
22501
|
+
code: "CDP_CONNECT_TIMEOUT",
|
|
22502
|
+
stage: "connect",
|
|
22503
|
+
elapsedMs: Date.now() - createdAt
|
|
22504
|
+
});
|
|
22505
|
+
rejectReady(error2);
|
|
22506
|
+
terminate();
|
|
22507
|
+
}, connectTimeoutMs);
|
|
22508
|
+
ws.on("open", () => {
|
|
22509
|
+
if (state !== "connecting") {
|
|
22510
|
+
terminate();
|
|
22511
|
+
return;
|
|
22512
|
+
}
|
|
22513
|
+
clearTimeout(connectTimer);
|
|
22514
|
+
state = "open";
|
|
22515
|
+
readyResolve();
|
|
22516
|
+
});
|
|
22517
|
+
ws.on("message", (data) => {
|
|
22518
|
+
let message;
|
|
22519
|
+
try {
|
|
22520
|
+
message = JSON.parse(data.toString());
|
|
22521
|
+
} catch {
|
|
22522
|
+
return;
|
|
22523
|
+
}
|
|
22524
|
+
if (!message || !pending.has(message.id)) return;
|
|
22525
|
+
const entry = pending.get(message.id);
|
|
22526
|
+
pending.delete(message.id);
|
|
22527
|
+
clearTimeout(entry.timer);
|
|
22528
|
+
if (message.error) {
|
|
22529
|
+
const detail = boundedJson(message.error);
|
|
22530
|
+
entry.reject(clientError(`CDP command failed: ${entry.method}: ${detail}`, {
|
|
22531
|
+
code: message.error.code ?? "CDP_PROTOCOL_ERROR",
|
|
22532
|
+
stage: "protocol",
|
|
22533
|
+
method: entry.method,
|
|
22534
|
+
elapsedMs: Date.now() - entry.startedAt
|
|
22535
|
+
}));
|
|
22536
|
+
} else {
|
|
22537
|
+
entry.resolve(message.result);
|
|
22538
|
+
}
|
|
22539
|
+
});
|
|
22540
|
+
ws.on("error", (cause) => {
|
|
22541
|
+
if (state === "closed" || state === "failed") return;
|
|
22542
|
+
failSocket(cause);
|
|
22543
|
+
});
|
|
22544
|
+
ws.on("close", (code, reason) => {
|
|
22545
|
+
if (state === "closed" || state === "failed") return;
|
|
22546
|
+
const suffix = code ? ` (code=${code}${reason?.length ? ` reason=${String(reason).slice(0, 256)}` : ""})` : "";
|
|
22547
|
+
const error2 = clientError(`CDP WebSocket closed${suffix}`, {
|
|
22548
|
+
code: "CDP_SOCKET_CLOSED",
|
|
22549
|
+
stage: state === "connecting" ? "connect" : "socket",
|
|
22550
|
+
elapsedMs: Date.now() - createdAt
|
|
22551
|
+
});
|
|
22552
|
+
if (state === "connecting") rejectReady(error2);
|
|
22553
|
+
else {
|
|
22554
|
+
state = "closed";
|
|
22555
|
+
rejectPending((entry) => clientError(`CDP WebSocket closed during ${entry.method}${suffix}`, {
|
|
22556
|
+
code: "CDP_SOCKET_CLOSED",
|
|
22557
|
+
stage: "socket",
|
|
22558
|
+
method: entry.method,
|
|
22559
|
+
elapsedMs: Date.now() - entry.startedAt
|
|
22560
|
+
}));
|
|
22561
|
+
}
|
|
22562
|
+
});
|
|
22563
|
+
}
|
|
22564
|
+
const send = (method, params = {}, sendOptions = {}) => {
|
|
22565
|
+
if (state !== "open" || !ws || ws.readyState !== (WebSocketImpl.OPEN ?? 1)) {
|
|
22566
|
+
return Promise.reject(clientError(`CDP command cannot be sent before the WebSocket is open: ${method}`, {
|
|
22567
|
+
code: "CDP_NOT_OPEN",
|
|
22568
|
+
stage: "send",
|
|
22569
|
+
method,
|
|
22570
|
+
elapsedMs: 0
|
|
22571
|
+
}));
|
|
22572
|
+
}
|
|
22573
|
+
let timeoutMs;
|
|
22574
|
+
try {
|
|
22575
|
+
timeoutMs = sendOptions.timeoutMs === void 0 ? commandTimeoutMs : Math.min(commandTimeoutMs, positiveTimeout("timeoutMs", sendOptions.timeoutMs));
|
|
22576
|
+
} catch (error2) {
|
|
22577
|
+
return Promise.reject(error2);
|
|
22578
|
+
}
|
|
22579
|
+
const id = ++nextId;
|
|
22580
|
+
let payload;
|
|
22581
|
+
try {
|
|
22582
|
+
payload = JSON.stringify({ id, method, params });
|
|
22583
|
+
} catch (cause) {
|
|
22584
|
+
return Promise.reject(clientError(`CDP command could not be serialized: ${method}: ${cause.message}`, {
|
|
22585
|
+
stage: "send",
|
|
22586
|
+
method,
|
|
22587
|
+
cause,
|
|
22588
|
+
elapsedMs: 0
|
|
22589
|
+
}));
|
|
22590
|
+
}
|
|
22591
|
+
return new Promise((resolve8, reject) => {
|
|
22592
|
+
const startedAt = Date.now();
|
|
22593
|
+
const timer = setTimeout(() => {
|
|
22594
|
+
if (!pending.delete(id)) return;
|
|
22595
|
+
reject(clientError(`CDP command timed out: ${method} (${timeoutMs}ms)`, {
|
|
22596
|
+
code: "CDP_COMMAND_TIMEOUT",
|
|
22597
|
+
stage: "command",
|
|
22598
|
+
method,
|
|
22599
|
+
elapsedMs: Date.now() - startedAt
|
|
22600
|
+
}));
|
|
22601
|
+
}, timeoutMs);
|
|
22602
|
+
pending.set(id, { method, resolve: resolve8, reject, timer, startedAt });
|
|
22603
|
+
try {
|
|
22604
|
+
ws.send(payload, (cause) => {
|
|
22605
|
+
if (!cause || !pending.has(id)) return;
|
|
22606
|
+
const entry = pending.get(id);
|
|
22607
|
+
pending.delete(id);
|
|
22608
|
+
clearTimeout(entry.timer);
|
|
22609
|
+
entry.reject(clientError(`CDP command send failed: ${method}: ${cause.message}`, {
|
|
22610
|
+
stage: "send",
|
|
22611
|
+
method,
|
|
22612
|
+
cause,
|
|
22613
|
+
elapsedMs: Date.now() - startedAt
|
|
22614
|
+
}));
|
|
22615
|
+
});
|
|
22616
|
+
} catch (cause) {
|
|
22617
|
+
pending.delete(id);
|
|
22618
|
+
clearTimeout(timer);
|
|
22619
|
+
reject(clientError(`CDP command send failed: ${method}: ${cause.message}`, {
|
|
22620
|
+
stage: "send",
|
|
22621
|
+
method,
|
|
22622
|
+
cause,
|
|
22623
|
+
elapsedMs: Date.now() - startedAt
|
|
22624
|
+
}));
|
|
22625
|
+
}
|
|
22626
|
+
});
|
|
22627
|
+
};
|
|
22628
|
+
const close = () => {
|
|
22629
|
+
if (state === "closed") return;
|
|
22630
|
+
const wasConnecting = state === "connecting";
|
|
22631
|
+
state = "closed";
|
|
22632
|
+
clearTimeout(connectTimer);
|
|
22633
|
+
const error2 = clientError("CDP client closed", {
|
|
22634
|
+
code: "CDP_CLIENT_CLOSED",
|
|
22635
|
+
stage: wasConnecting ? "connect" : "socket",
|
|
22636
|
+
elapsedMs: Date.now() - createdAt
|
|
22637
|
+
});
|
|
22638
|
+
if (wasConnecting) readyReject(error2);
|
|
22639
|
+
rejectPending((entry) => clientError(`CDP client closed during ${entry.method}`, {
|
|
22640
|
+
code: "CDP_CLIENT_CLOSED",
|
|
22641
|
+
stage: "socket",
|
|
22642
|
+
method: entry.method,
|
|
22643
|
+
elapsedMs: Date.now() - entry.startedAt
|
|
22644
|
+
}));
|
|
22645
|
+
if (!ws) return;
|
|
22646
|
+
try {
|
|
22647
|
+
if (wasConnecting && typeof ws.terminate === "function") ws.terminate();
|
|
22648
|
+
else ws.close();
|
|
22649
|
+
} catch {
|
|
22650
|
+
}
|
|
22651
|
+
};
|
|
22652
|
+
return { ready, send, close };
|
|
22653
|
+
}
|
|
22654
|
+
|
|
22359
22655
|
// server.mjs
|
|
22360
22656
|
init_cursor_runtime();
|
|
22361
22657
|
import http2 from "http";
|
|
@@ -22605,7 +22901,7 @@ function updateCursorSessionRegistry(filePath, mutator, options = {}) {
|
|
|
22605
22901
|
// server.mjs
|
|
22606
22902
|
init_cursor_ensure_core();
|
|
22607
22903
|
init_lifecycle_paths();
|
|
22608
|
-
var PLUGIN_VERSION = "6.0.
|
|
22904
|
+
var PLUGIN_VERSION = "6.0.1";
|
|
22609
22905
|
var CDP_PORT2 = Number(process.env.CURSOR_BRIDGE_CDP_PORT || 9223);
|
|
22610
22906
|
var ORIGIN = `http://localhost:${CDP_PORT2}`;
|
|
22611
22907
|
var QUERY_TIMEOUT = Number(process.env.CURSOR_BRIDGE_TIMEOUT || 3e5);
|
|
@@ -22786,17 +23082,11 @@ function shouldRecoverNormalAgentsPresentation({
|
|
|
22786
23082
|
return Number(now) - previousAt >= Math.max(0, Number(refreshMs) || 0);
|
|
22787
23083
|
}
|
|
22788
23084
|
async function inspectPageTarget(page) {
|
|
22789
|
-
const c = makeClient(page.webSocketDebuggerUrl);
|
|
22790
23085
|
const probeMs = Number(process.env.CURSOR_BRIDGE_PAGE_PROBE_TIMEOUT || 5e3);
|
|
23086
|
+
const c = makeClient2(page.webSocketDebuggerUrl, { connectTimeoutMs: probeMs });
|
|
22791
23087
|
try {
|
|
22792
|
-
await
|
|
22793
|
-
|
|
22794
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error("Timed out connecting to the CDP target")), probeMs))
|
|
22795
|
-
]);
|
|
22796
|
-
const raw = await Promise.race([
|
|
22797
|
-
evalJS(c, EXPR_PAGE_CAPABILITIES),
|
|
22798
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error("Timed out probing the CDP target")), probeMs))
|
|
22799
|
-
]);
|
|
23088
|
+
await c.ready;
|
|
23089
|
+
const raw = await evalJS(c, EXPR_PAGE_CAPABILITIES, { timeoutMs: probeMs });
|
|
22800
23090
|
return { ...page, capabilities: JSON.parse(raw || "{}") };
|
|
22801
23091
|
} catch (error2) {
|
|
22802
23092
|
return { ...page, capabilities: null, probeError: error2.message };
|
|
@@ -22844,73 +23134,22 @@ async function findPage(options = {}) {
|
|
|
22844
23134
|
const inspected = await Promise.all(pages.map(inspectPageTarget));
|
|
22845
23135
|
return selectPageForUiPreference(inspected, options) || pages[0];
|
|
22846
23136
|
}
|
|
22847
|
-
function
|
|
22848
|
-
|
|
22849
|
-
|
|
22850
|
-
|
|
22851
|
-
|
|
22852
|
-
|
|
22853
|
-
try {
|
|
22854
|
-
rej(new Error(msg));
|
|
22855
|
-
} catch {
|
|
22856
|
-
}
|
|
22857
|
-
}
|
|
22858
|
-
pending.clear();
|
|
22859
|
-
};
|
|
22860
|
-
const ready = new Promise((res, rej) => {
|
|
22861
|
-
ws.on("open", res);
|
|
22862
|
-
ws.once("error", rej);
|
|
22863
|
-
});
|
|
22864
|
-
ws.on("message", (data) => {
|
|
22865
|
-
let m;
|
|
22866
|
-
try {
|
|
22867
|
-
m = JSON.parse(data.toString());
|
|
22868
|
-
} catch {
|
|
22869
|
-
return;
|
|
22870
|
-
}
|
|
22871
|
-
if (m.id && pending.has(m.id)) {
|
|
22872
|
-
const { res, rej } = pending.get(m.id);
|
|
22873
|
-
pending.delete(m.id);
|
|
22874
|
-
if (m.error) rej(new Error(JSON.stringify(m.error)));
|
|
22875
|
-
else res(m.result);
|
|
22876
|
-
}
|
|
23137
|
+
function makeClient2(wsUrl, options = {}) {
|
|
23138
|
+
return makeClient(wsUrl, {
|
|
23139
|
+
origin: ORIGIN,
|
|
23140
|
+
connectTimeoutMs: Number(process.env.CURSOR_BRIDGE_CONNECT_TIMEOUT || 5e3),
|
|
23141
|
+
commandTimeoutMs: Number(process.env.CURSOR_BRIDGE_CMD_TIMEOUT || 3e4),
|
|
23142
|
+
...options
|
|
22877
23143
|
});
|
|
22878
|
-
ws.on("close", () => failAll("CDP WebSocket closed because the page or renderer disappeared"));
|
|
22879
|
-
ws.on("error", (e) => failAll("CDP WebSocket error: " + (e && e.message)));
|
|
22880
|
-
const CMD_TIMEOUT = Number(process.env.CURSOR_BRIDGE_CMD_TIMEOUT || 3e4);
|
|
22881
|
-
const send = (method, params = {}) => {
|
|
22882
|
-
const myId = ++id;
|
|
22883
|
-
return new Promise((res, rej) => {
|
|
22884
|
-
const t = setTimeout(() => {
|
|
22885
|
-
if (pending.delete(myId)) rej(new Error(`CDP command timed out: ${method} (${CMD_TIMEOUT}ms)`));
|
|
22886
|
-
}, CMD_TIMEOUT);
|
|
22887
|
-
pending.set(myId, { res: (v) => {
|
|
22888
|
-
clearTimeout(t);
|
|
22889
|
-
res(v);
|
|
22890
|
-
}, rej: (e) => {
|
|
22891
|
-
clearTimeout(t);
|
|
22892
|
-
rej(e);
|
|
22893
|
-
} });
|
|
22894
|
-
try {
|
|
22895
|
-
ws.send(JSON.stringify({ id: myId, method, params }));
|
|
22896
|
-
} catch (e) {
|
|
22897
|
-
clearTimeout(t);
|
|
22898
|
-
pending.delete(myId);
|
|
22899
|
-
rej(e);
|
|
22900
|
-
}
|
|
22901
|
-
});
|
|
22902
|
-
};
|
|
22903
|
-
return { ready, send, close: () => {
|
|
22904
|
-
try {
|
|
22905
|
-
ws.close();
|
|
22906
|
-
} catch {
|
|
22907
|
-
}
|
|
22908
|
-
} };
|
|
22909
23144
|
}
|
|
22910
23145
|
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
22911
|
-
async function evalJS(c, expr) {
|
|
22912
|
-
const r = await c.send("Runtime.evaluate", { expression: expr, returnByValue: true, includeCommandLineAPI: true, awaitPromise: true });
|
|
22913
|
-
if (r.exceptionDetails)
|
|
23146
|
+
async function evalJS(c, expr, options) {
|
|
23147
|
+
const r = await c.send("Runtime.evaluate", { expression: expr, returnByValue: true, includeCommandLineAPI: true, awaitPromise: true }, options);
|
|
23148
|
+
if (r.exceptionDetails) {
|
|
23149
|
+
const error2 = new Error("Page exception: " + (r.exceptionDetails.exception && r.exceptionDetails.exception.description || r.exceptionDetails.text));
|
|
23150
|
+
error2.code = "CDP_EVALUATE_FAILED";
|
|
23151
|
+
throw error2;
|
|
23152
|
+
}
|
|
22914
23153
|
return r.result && r.result.value;
|
|
22915
23154
|
}
|
|
22916
23155
|
async function chord(c, modifiers, key, code, vk) {
|
|
@@ -23188,6 +23427,7 @@ var EXPR_MODEL_PICKER_ROWS = `(function(){
|
|
|
23188
23427
|
kind,
|
|
23189
23428
|
selected:row.getAttribute('data-selected')==='true'||row.getAttribute('aria-checked')==='true'||!!row.querySelector('.ui-model-picker__item-check,.ui-model-picker__param-check'),
|
|
23190
23429
|
disabled:row.getAttribute('data-disabled')==='true'||row.getAttribute('aria-disabled')==='true',
|
|
23430
|
+
pointerEvents:getComputedStyle(row).pointerEvents,
|
|
23191
23431
|
hasSubmenu:row.getAttribute('aria-haspopup')==='menu',
|
|
23192
23432
|
submenu:!!row.closest('[data-submenu]'),
|
|
23193
23433
|
x:Math.round(rect.x+rect.width/2),
|
|
@@ -24029,7 +24269,7 @@ var CursorBridge = class {
|
|
|
24029
24269
|
throw cursorSessionError("SESSION_NOT_READY", `state=${session.state}; recovery=${session.recoveryState || "none"}`);
|
|
24030
24270
|
}
|
|
24031
24271
|
if (session.lastTask?.status === "completed" && !session.lastTask.resultCollectedAt && this.tasks.has(session.lastTask.taskId)) {
|
|
24032
|
-
throw cursorSessionError("SESSION_RESULT_UNCOLLECTED", `read cursor_status(task_id=${session.lastTask.taskId}, detail="
|
|
24272
|
+
throw cursorSessionError("SESSION_RESULT_UNCOLLECTED", `read cursor_status(task_id=${session.lastTask.taskId}, detail="result") before continuing`);
|
|
24033
24273
|
}
|
|
24034
24274
|
if (session.lastTask?.status === "completed" && !session.lastTask.resultCollectedAt && !this.tasks.has(session.lastTask.taskId) && session.recoveryState !== "reconciled_result_uncollected") {
|
|
24035
24275
|
throw cursorSessionError("SESSION_RECONCILE_REQUIRED", "the prior reply was not read before this adapter restarted; reconcile the exact Agent before continuing");
|
|
@@ -24406,7 +24646,7 @@ var CursorBridge = class {
|
|
|
24406
24646
|
return null;
|
|
24407
24647
|
}
|
|
24408
24648
|
if (!page || page.capabilities && page.capabilities.uiFlavor !== "agents_v2") return null;
|
|
24409
|
-
const c =
|
|
24649
|
+
const c = makeClient2(page.webSocketDebuggerUrl);
|
|
24410
24650
|
try {
|
|
24411
24651
|
await c.ready;
|
|
24412
24652
|
let repository = JSON.parse(await evalJS(c, exprInspectWorkspaceRepository(projectPath)) || "{}");
|
|
@@ -24518,14 +24758,15 @@ var CursorBridge = class {
|
|
|
24518
24758
|
lastPresentation: this._lastPresentation
|
|
24519
24759
|
};
|
|
24520
24760
|
}
|
|
24521
|
-
async applyRuntimePresentation(action) {
|
|
24761
|
+
async applyRuntimePresentation(action, { scope = "process" } = {}) {
|
|
24522
24762
|
const normalizedAction = String(action || "").trim().toLowerCase();
|
|
24523
24763
|
if (!["hide", "show"].includes(normalizedAction)) {
|
|
24524
24764
|
throw new Error("cursor_runtime action supports hide or show");
|
|
24525
24765
|
}
|
|
24526
24766
|
const result = setCursorWindowPresentation({
|
|
24527
24767
|
action: normalizedAction,
|
|
24528
|
-
port: CDP_PORT2
|
|
24768
|
+
port: CDP_PORT2,
|
|
24769
|
+
scope
|
|
24529
24770
|
});
|
|
24530
24771
|
this._lastPresentation = { ...result, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
24531
24772
|
return this._lastPresentation;
|
|
@@ -24540,7 +24781,7 @@ var CursorBridge = class {
|
|
|
24540
24781
|
})) {
|
|
24541
24782
|
return null;
|
|
24542
24783
|
}
|
|
24543
|
-
const presentation = await this.applyRuntimePresentation("show");
|
|
24784
|
+
const presentation = await this.applyRuntimePresentation("show", { scope: "agents" });
|
|
24544
24785
|
if (lifecycle && typeof lifecycle === "object") lifecycle.presentation = presentation;
|
|
24545
24786
|
return presentation;
|
|
24546
24787
|
}
|
|
@@ -25115,7 +25356,7 @@ var CursorBridge = class {
|
|
|
25115
25356
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
25116
25357
|
options.targetId = page.id;
|
|
25117
25358
|
options.targetUiFlavor = page.capabilities && page.capabilities.uiFlavor || options.targetUiFlavor || null;
|
|
25118
|
-
const c =
|
|
25359
|
+
const c = makeClient2(page.webSocketDebuggerUrl);
|
|
25119
25360
|
await c.ready;
|
|
25120
25361
|
try {
|
|
25121
25362
|
this._throwIfCancelledBeforeSend(options);
|
|
@@ -25195,20 +25436,29 @@ var CursorBridge = class {
|
|
|
25195
25436
|
error2.preSend = true;
|
|
25196
25437
|
throw error2;
|
|
25197
25438
|
}
|
|
25198
|
-
async
|
|
25439
|
+
async _readModelPickerValue(c, expression, kind, { timeoutMs = 3e3 } = {}) {
|
|
25440
|
+
const startedAt = Date.now();
|
|
25199
25441
|
try {
|
|
25200
|
-
|
|
25201
|
-
|
|
25202
|
-
|
|
25442
|
+
const value = JSON.parse(await evalJS(c, expression, { timeoutMs }));
|
|
25443
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
25444
|
+
const error2 = new Error("Cursor picker returned an invalid snapshot");
|
|
25445
|
+
error2.code = "CURSOR_PICKER_RESPONSE_INVALID";
|
|
25446
|
+
throw error2;
|
|
25447
|
+
}
|
|
25448
|
+
return value;
|
|
25449
|
+
} catch (cause) {
|
|
25450
|
+
const error2 = cause instanceof Error ? cause : new Error(String(cause));
|
|
25451
|
+
if (error2 instanceof SyntaxError) error2.code = "CURSOR_PICKER_PARSE_FAILED";
|
|
25452
|
+
error2.pickerRead = { kind, elapsedMs: Date.now() - startedAt, code: error2.code || "CDP_READ_FAILED", message: error2.message };
|
|
25453
|
+
throw error2;
|
|
25203
25454
|
}
|
|
25204
25455
|
}
|
|
25205
|
-
async
|
|
25206
|
-
|
|
25207
|
-
|
|
25208
|
-
|
|
25209
|
-
|
|
25210
|
-
|
|
25211
|
-
}
|
|
25456
|
+
async _readModelPickerTrigger(c, options) {
|
|
25457
|
+
return this._readModelPickerValue(c, EXPR_MODEL_PICKER_TRIGGER, "trigger", options);
|
|
25458
|
+
}
|
|
25459
|
+
async _readModelPickerRows(c, options) {
|
|
25460
|
+
const snapshot = await this._readModelPickerValue(c, EXPR_MODEL_PICKER_ROWS, "rows", options);
|
|
25461
|
+
return { open: snapshot.open === true, rows: Array.isArray(snapshot.rows) ? snapshot.rows : [] };
|
|
25212
25462
|
}
|
|
25213
25463
|
async _clickModelPickerPoint(c, point) {
|
|
25214
25464
|
if (!point || !Number.isFinite(Number(point.x)) || !Number.isFinite(Number(point.y))) {
|
|
@@ -25289,7 +25539,7 @@ var CursorBridge = class {
|
|
|
25289
25539
|
let last = this._inspectModelPickerRows({ open: false, rows: [] }, requested, kind);
|
|
25290
25540
|
do {
|
|
25291
25541
|
this._throwIfCancelledBeforeSend(job);
|
|
25292
|
-
last = this._inspectModelPickerRows(await this._readModelPickerRows(c), requested, kind);
|
|
25542
|
+
last = this._inspectModelPickerRows(await this._readModelPickerRows(c, { timeoutMs: Math.max(1, deadline - Date.now()) }), requested, kind);
|
|
25293
25543
|
if (last.row) return { ...last, state: "matched" };
|
|
25294
25544
|
const signature = JSON.stringify(last.available);
|
|
25295
25545
|
if (modelPickerAvailableIsDecisive(kind, last.available)) {
|
|
@@ -25310,17 +25560,41 @@ var CursorBridge = class {
|
|
|
25310
25560
|
}
|
|
25311
25561
|
async _selectedEffortRow(c, modelRow, effort, job) {
|
|
25312
25562
|
const requested = cursorEffortUiValue(effort);
|
|
25313
|
-
|
|
25563
|
+
let snapshot = await this._readModelPickerRows(c);
|
|
25314
25564
|
const immediate = this._inspectModelPickerRows(snapshot, requested, "parameter");
|
|
25315
25565
|
if (immediate.row) return { ...immediate, state: "matched", attempts: ["visible"] };
|
|
25316
25566
|
const attempts = [];
|
|
25317
|
-
|
|
25567
|
+
let effortControl = (snapshot.rows || []).find((row) => row.kind === "effort_control" && row.disabled !== true);
|
|
25318
25568
|
if (effortControl) {
|
|
25319
25569
|
this._throwIfCancelledBeforeSend(job);
|
|
25570
|
+
const modelSubmenuOpen = () => snapshot.rows.some((row) => row.kind === "model" && row.submenu === true);
|
|
25571
|
+
if (modelSubmenuOpen()) {
|
|
25572
|
+
attempts.push("close_model_submenu");
|
|
25573
|
+
for (const type of ["keyDown", "keyUp"]) {
|
|
25574
|
+
await c.send("Input.dispatchKeyEvent", { type, key: "Escape", code: "Escape", windowsVirtualKeyCode: 27, nativeVirtualKeyCode: 27 });
|
|
25575
|
+
}
|
|
25576
|
+
const deadline = Date.now() + 700;
|
|
25577
|
+
do {
|
|
25578
|
+
this._throwIfCancelledBeforeSend(job);
|
|
25579
|
+
snapshot = await this._readModelPickerRows(c, { timeoutMs: Math.max(1, deadline - Date.now()) });
|
|
25580
|
+
if (!snapshot.open) break;
|
|
25581
|
+
effortControl = snapshot.rows.find((row) => row.kind === "effort_control" && row.disabled !== true && row.pointerEvents !== "none");
|
|
25582
|
+
if (effortControl && !modelSubmenuOpen()) {
|
|
25583
|
+
attempts.push("fresh_effort_control");
|
|
25584
|
+
break;
|
|
25585
|
+
}
|
|
25586
|
+
if (Date.now() >= deadline) break;
|
|
25587
|
+
await sleep2(Math.min(50, deadline - Date.now()));
|
|
25588
|
+
} while (Date.now() <= deadline);
|
|
25589
|
+
}
|
|
25590
|
+
if (!snapshot.open || !effortControl || effortControl.pointerEvents === "none" || modelSubmenuOpen()) {
|
|
25591
|
+
return { ...this._inspectModelPickerRows(snapshot, requested, "parameter"), state: "not_rendered", attempts };
|
|
25592
|
+
}
|
|
25320
25593
|
attempts.push("effort_control");
|
|
25594
|
+
this._throwIfCancelledBeforeSend(job);
|
|
25321
25595
|
await this._clickModelPickerPoint(c, effortControl);
|
|
25322
25596
|
const result2 = await this._waitForModelPickerMatch(c, requested, "parameter", job, { timeoutMs: 700 });
|
|
25323
|
-
|
|
25597
|
+
return { ...result2, attempts };
|
|
25324
25598
|
}
|
|
25325
25599
|
this._throwIfCancelledBeforeSend(job);
|
|
25326
25600
|
attempts.push("model_hover");
|
|
@@ -25339,7 +25613,7 @@ var CursorBridge = class {
|
|
|
25339
25613
|
const deadline = Date.now() + timeoutMs;
|
|
25340
25614
|
do {
|
|
25341
25615
|
this._throwIfCancelledBeforeSend(job);
|
|
25342
|
-
const inspected = this._inspectModelPickerRows(await this._readModelPickerRows(c), requested, kind);
|
|
25616
|
+
const inspected = this._inspectModelPickerRows(await this._readModelPickerRows(c, { timeoutMs: Math.max(1, deadline - Date.now()) }), requested, kind);
|
|
25343
25617
|
if (inspected.row && inspected.row.selected) return inspected.row;
|
|
25344
25618
|
if (Date.now() >= deadline) break;
|
|
25345
25619
|
await sleep2(Math.min(100, Math.max(0, deadline - Date.now())));
|
|
@@ -25356,8 +25630,11 @@ var CursorBridge = class {
|
|
|
25356
25630
|
let modelRow = null;
|
|
25357
25631
|
let effectiveEffort = null;
|
|
25358
25632
|
let primaryError = null;
|
|
25633
|
+
const selectionStartedAt = Date.now();
|
|
25634
|
+
let stage = "open_picker";
|
|
25359
25635
|
try {
|
|
25360
25636
|
const opened = await this._openModelPicker(c);
|
|
25637
|
+
stage = "locate_model";
|
|
25361
25638
|
let located = await this._findModelPickerModel(c, opened, requestedModel);
|
|
25362
25639
|
modelRow = located.modelRow;
|
|
25363
25640
|
if (!modelRow) {
|
|
@@ -25396,10 +25673,15 @@ var CursorBridge = class {
|
|
|
25396
25673
|
message,
|
|
25397
25674
|
failureClass,
|
|
25398
25675
|
failureClass === "effort_menu_not_rendered",
|
|
25399
|
-
{
|
|
25676
|
+
{
|
|
25677
|
+
available: outcome.available || [],
|
|
25678
|
+
attempts: outcome.attempts || [],
|
|
25679
|
+
menu: outcome.snapshot ? { open: outcome.snapshot.open, rows: (outcome.snapshot.rows || []).slice(0, 20).map(({ text, kind, selected, disabled, pointerEvents, submenu }) => ({ text, kind, selected, disabled, pointerEvents, submenu })) } : null
|
|
25680
|
+
}
|
|
25400
25681
|
);
|
|
25401
25682
|
};
|
|
25402
25683
|
if (requestedEffort && modelRow.hasSubmenu) {
|
|
25684
|
+
stage = "select_effort";
|
|
25403
25685
|
const selectedEffort = await resolveEffort();
|
|
25404
25686
|
if (!selectedEffort.row) throwEffortFailure(selectedEffort);
|
|
25405
25687
|
if (!selectedEffort.row.selected || !modelRow.selected) {
|
|
@@ -25408,10 +25690,12 @@ var CursorBridge = class {
|
|
|
25408
25690
|
await sleep2(550);
|
|
25409
25691
|
}
|
|
25410
25692
|
} else if (!modelRow.selected) {
|
|
25693
|
+
stage = "select_model";
|
|
25411
25694
|
this._throwIfCancelledBeforeSend(job);
|
|
25412
25695
|
await this._clickModelPickerPoint(c, modelRow);
|
|
25413
25696
|
await sleep2(550);
|
|
25414
25697
|
}
|
|
25698
|
+
stage = "verify_model";
|
|
25415
25699
|
let trigger2 = await this._readModelPickerTrigger(c);
|
|
25416
25700
|
if (!trigger2.found || !normalizeModelPickerText(trigger2.text).includes(normalizeModelPickerText(requestedModel))) {
|
|
25417
25701
|
const reopened = await this._openModelPicker(c);
|
|
@@ -25427,6 +25711,7 @@ var CursorBridge = class {
|
|
|
25427
25711
|
modelRow = selected;
|
|
25428
25712
|
}
|
|
25429
25713
|
if (requestedEffort) {
|
|
25714
|
+
stage = "verify_effort";
|
|
25430
25715
|
const reopened = await this._openModelPicker(c);
|
|
25431
25716
|
located = await this._findModelPickerModel(c, reopened, requestedModel);
|
|
25432
25717
|
modelRow = located.modelRow;
|
|
@@ -25463,13 +25748,19 @@ var CursorBridge = class {
|
|
|
25463
25748
|
if (!failure) {
|
|
25464
25749
|
const pickerUnavailable = /model picker (?:is unavailable|did not open)/i.test(primaryError.message);
|
|
25465
25750
|
failure = {
|
|
25466
|
-
failureClass: pickerUnavailable ? "picker_did_not_open" : "probe_error",
|
|
25751
|
+
failureClass: primaryError.pickerRead ? "picker_read_failed" : pickerUnavailable ? "picker_did_not_open" : "probe_error",
|
|
25467
25752
|
retryable: true
|
|
25468
25753
|
};
|
|
25469
25754
|
}
|
|
25470
25755
|
const diagnostic = {
|
|
25471
25756
|
configured: true,
|
|
25472
25757
|
applied: false,
|
|
25758
|
+
stage,
|
|
25759
|
+
elapsedMs: Date.now() - selectionStartedAt,
|
|
25760
|
+
taskId: job?.id || null,
|
|
25761
|
+
targetId: job?.targetId || null,
|
|
25762
|
+
pickerRead: primaryError.pickerRead || null,
|
|
25763
|
+
cdp: primaryError.cdp || null,
|
|
25473
25764
|
requestedModel,
|
|
25474
25765
|
requestedEffort,
|
|
25475
25766
|
failureClass: failure.failureClass,
|
|
@@ -25477,6 +25768,7 @@ var CursorBridge = class {
|
|
|
25477
25768
|
errorCode: primaryError.code || `CURSOR_MODEL_${String(failure.failureClass).toUpperCase()}`,
|
|
25478
25769
|
available: failure.available || [],
|
|
25479
25770
|
attempts: failure.attempts || [],
|
|
25771
|
+
menu: failure.menu || null,
|
|
25480
25772
|
runtimeMode: this.runtimeMode,
|
|
25481
25773
|
lastError: primaryError.message,
|
|
25482
25774
|
failedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -25513,7 +25805,29 @@ var CursorBridge = class {
|
|
|
25513
25805
|
if (job && job.modelSelection) job.modelSelection.cleanupError = message;
|
|
25514
25806
|
}
|
|
25515
25807
|
}
|
|
25516
|
-
|
|
25808
|
+
let trigger;
|
|
25809
|
+
try {
|
|
25810
|
+
trigger = await this._readModelPickerTrigger(c);
|
|
25811
|
+
} catch (error2) {
|
|
25812
|
+
error2.modelSelection = {
|
|
25813
|
+
configured: true,
|
|
25814
|
+
applied: false,
|
|
25815
|
+
requestedModel,
|
|
25816
|
+
requestedEffort,
|
|
25817
|
+
taskId: job?.id || null,
|
|
25818
|
+
targetId: job?.targetId || null,
|
|
25819
|
+
stage: "final_trigger",
|
|
25820
|
+
failureClass: "picker_read_failed",
|
|
25821
|
+
retryable: true,
|
|
25822
|
+
errorCode: error2.code || "CDP_READ_FAILED",
|
|
25823
|
+
pickerRead: error2.pickerRead || null,
|
|
25824
|
+
cdp: error2.cdp || null,
|
|
25825
|
+
elapsedMs: Date.now() - selectionStartedAt,
|
|
25826
|
+
lastError: error2.message
|
|
25827
|
+
};
|
|
25828
|
+
if (job) job.modelSelection = error2.modelSelection;
|
|
25829
|
+
throw error2;
|
|
25830
|
+
}
|
|
25517
25831
|
const result = {
|
|
25518
25832
|
configured: true,
|
|
25519
25833
|
applied: true,
|
|
@@ -25810,7 +26124,7 @@ var CursorBridge = class {
|
|
|
25810
26124
|
});
|
|
25811
26125
|
job.targetId = page.id;
|
|
25812
26126
|
job.targetUiFlavor = page.capabilities && page.capabilities.uiFlavor || null;
|
|
25813
|
-
const c =
|
|
26127
|
+
const c = makeClient2(page.webSocketDebuggerUrl);
|
|
25814
26128
|
await c.ready;
|
|
25815
26129
|
let sent = false;
|
|
25816
26130
|
try {
|
|
@@ -25945,7 +26259,7 @@ var CursorBridge = class {
|
|
|
25945
26259
|
});
|
|
25946
26260
|
job.targetId = page.id;
|
|
25947
26261
|
job.targetUiFlavor = page.capabilities && page.capabilities.uiFlavor || null;
|
|
25948
|
-
const c =
|
|
26262
|
+
const c = makeClient2(page.webSocketDebuggerUrl);
|
|
25949
26263
|
await c.ready;
|
|
25950
26264
|
let sent = false;
|
|
25951
26265
|
try {
|
|
@@ -26001,7 +26315,7 @@ var CursorBridge = class {
|
|
|
26001
26315
|
async _readParallelEntry(job) {
|
|
26002
26316
|
return this._withUiLock(async () => {
|
|
26003
26317
|
const page = await findPage({ targetId: job.targetId, purpose: "parallel_agent" });
|
|
26004
|
-
const c =
|
|
26318
|
+
const c = makeClient2(page.webSocketDebuggerUrl);
|
|
26005
26319
|
await c.ready;
|
|
26006
26320
|
try {
|
|
26007
26321
|
let entries = await this._readAgentEntries(c);
|
|
@@ -26206,7 +26520,7 @@ var CursorBridge = class {
|
|
|
26206
26520
|
}
|
|
26207
26521
|
async _collectParallelAgent(job) {
|
|
26208
26522
|
const page = await findPage({ targetId: job.targetId, purpose: "parallel_agent" });
|
|
26209
|
-
const c =
|
|
26523
|
+
const c = makeClient2(page.webSocketDebuggerUrl);
|
|
26210
26524
|
await c.ready;
|
|
26211
26525
|
try {
|
|
26212
26526
|
return await this._withRestoredAgentSelection(c, job, async () => {
|
|
@@ -26530,7 +26844,7 @@ var CursorBridge = class {
|
|
|
26530
26844
|
if (!job.agentId) return { confirmed: false, state: "unbound_agent" };
|
|
26531
26845
|
return this._withUiLock(async () => {
|
|
26532
26846
|
const page = await findPage({ targetId: job.targetId, purpose: "parallel_agent" });
|
|
26533
|
-
const c =
|
|
26847
|
+
const c = makeClient2(page.webSocketDebuggerUrl);
|
|
26534
26848
|
await c.ready;
|
|
26535
26849
|
try {
|
|
26536
26850
|
return await this._stopBoundAgentOnClient(c, job, { restorePrevious: true });
|
|
@@ -26724,7 +27038,7 @@ var CursorBridge = class {
|
|
|
26724
27038
|
this.parallelRestoreTargetId = null;
|
|
26725
27039
|
this._withUiLock(async () => {
|
|
26726
27040
|
const page = await findPage({ targetId, purpose: "parallel_agent" });
|
|
26727
|
-
const c =
|
|
27041
|
+
const c = makeClient2(page.webSocketDebuggerUrl);
|
|
26728
27042
|
await c.ready;
|
|
26729
27043
|
try {
|
|
26730
27044
|
if (await this._ensureHistoryOpen(c)) {
|
|
@@ -26891,6 +27205,14 @@ var CursorBridge = class {
|
|
|
26891
27205
|
effectiveModel: full.modelSelection.effectiveModel,
|
|
26892
27206
|
effectiveEffort: full.modelSelection.effectiveEffort,
|
|
26893
27207
|
failureClass: full.modelSelection.failureClass,
|
|
27208
|
+
stage: full.modelSelection.stage,
|
|
27209
|
+
elapsedMs: full.modelSelection.elapsedMs,
|
|
27210
|
+
pickerRead: full.modelSelection.pickerRead,
|
|
27211
|
+
cdp: full.modelSelection.cdp,
|
|
27212
|
+
available: full.modelSelection.available,
|
|
27213
|
+
attempts: full.modelSelection.attempts,
|
|
27214
|
+
menu: full.modelSelection.menu,
|
|
27215
|
+
cleanupError: full.modelSelection.cleanupError,
|
|
26894
27216
|
retryable: full.modelSelection.retryable,
|
|
26895
27217
|
errorCode: full.modelSelection.errorCode,
|
|
26896
27218
|
lastError: full.modelSelection.lastError,
|
|
@@ -27034,7 +27356,7 @@ var CursorBridge = class {
|
|
|
27034
27356
|
_ensureTaskCapacity() {
|
|
27035
27357
|
this._trimTasks(49);
|
|
27036
27358
|
if (this.tasks.size >= 50) {
|
|
27037
|
-
throw new Error('TASK_RETENTION_FULL: 50 tasks are active or have unread replies. Read each unreadResultTaskId with cursor_status(task_id, detail="
|
|
27359
|
+
throw new Error('TASK_RETENTION_FULL: 50 tasks are active or have unread replies. Read each unreadResultTaskId with cursor_status(task_id, detail="result"), or wait for active tasks before submitting more work.');
|
|
27038
27360
|
}
|
|
27039
27361
|
}
|
|
27040
27362
|
_trimTasks(limit = 50) {
|
|
@@ -27046,6 +27368,17 @@ var CursorBridge = class {
|
|
|
27046
27368
|
}
|
|
27047
27369
|
async status(taskId = "", { detail = "compact" } = {}) {
|
|
27048
27370
|
const normalizedDetail = normalizeStatusDetail(detail);
|
|
27371
|
+
if (normalizedDetail === "result") {
|
|
27372
|
+
if (!taskId) throw new Error('RESULT_TASK_REQUIRED: detail="result" requires task_id');
|
|
27373
|
+
const job = this.tasks.get(String(taskId));
|
|
27374
|
+
if (!job) throw new Error(`RESULT_TASK_NOT_FOUND: ${taskId}`);
|
|
27375
|
+
if (!isTerminalTask(job) || job.result == null) {
|
|
27376
|
+
throw new Error(`RESULT_NOT_AVAILABLE: ${taskId}; inspect compact status before retrying`);
|
|
27377
|
+
}
|
|
27378
|
+
const text = String(job.result);
|
|
27379
|
+
this._markTaskResultCollected(job);
|
|
27380
|
+
return text;
|
|
27381
|
+
}
|
|
27049
27382
|
if (taskId) {
|
|
27050
27383
|
const job = this.tasks.get(String(taskId));
|
|
27051
27384
|
if (normalizedDetail === "full") {
|
|
@@ -27132,8 +27465,8 @@ function buildSearchInputSchema() {
|
|
|
27132
27465
|
}
|
|
27133
27466
|
function normalizeStatusDetail(detail) {
|
|
27134
27467
|
if (detail === void 0) return "compact";
|
|
27135
|
-
if (typeof detail !== "string" || !["compact", "full"].includes(detail)) {
|
|
27136
|
-
throw new Error("detail must be compact or
|
|
27468
|
+
if (typeof detail !== "string" || !["compact", "full", "result"].includes(detail)) {
|
|
27469
|
+
throw new Error("detail must be compact, full or result");
|
|
27137
27470
|
}
|
|
27138
27471
|
return detail;
|
|
27139
27472
|
}
|
|
@@ -27166,7 +27499,7 @@ function buildToolDefinitions(bridgeInstance) {
|
|
|
27166
27499
|
},
|
|
27167
27500
|
bridgeInstance.environmentDelegationMode !== "off" ? {
|
|
27168
27501
|
name: "cursor_do",
|
|
27169
|
-
description: 'Give Cursor a clearly bounded task and get back a task ID. fifo means first in, first out: Bridge runs one queued task at a time, starting it in a clean chat. parallel_agent creates a separate top-level Cursor Agent. Persistent continuity is explicit: session_mode=create starts one durable top-level Agent association, and session_mode=continue requires its exact session_id. Omission keeps the existing isolated behavior. Parallel write tasks must declare non-overlapping allowed_paths; mark read-only work with read_only=true. Collect the result with cursor_status(task_id, detail="
|
|
27502
|
+
description: 'Give Cursor a clearly bounded task and get back a task ID. fifo means first in, first out: Bridge runs one queued task at a time, starting it in a clean chat. parallel_agent creates a separate top-level Cursor Agent. Persistent continuity is explicit: session_mode=create starts one durable top-level Agent association, and session_mode=continue requires its exact session_id. Omission keeps the existing isolated behavior. Parallel write tasks must declare non-overlapping allowed_paths; mark read-only work with read_only=true. Collect the result with cursor_status(task_id, detail="result"); ordinary status calls are compact and do not acknowledge the reply. Cursor can do the work, but the main agent still owns review and final verification. A direct user opt-out always wins.',
|
|
27170
27503
|
inputSchema: {
|
|
27171
27504
|
type: "object",
|
|
27172
27505
|
properties: {
|
|
@@ -27187,7 +27520,7 @@ function buildToolDefinitions(bridgeInstance) {
|
|
|
27187
27520
|
} : null,
|
|
27188
27521
|
{
|
|
27189
27522
|
name: "cursor_task_control",
|
|
27190
|
-
description: 'Recover or terminate one exact in-memory Cursor task without resubmitting it; task records do not survive this MCP server process. Use reap for needs_attention/orphaned work only when it has a bound agentId; it explicitly rechecks that Agent and stores a stable terminal result when possible. Responses stay compact and never acknowledge the result; collect it later with cursor_status(task_id, detail="
|
|
27523
|
+
description: 'Recover or terminate one exact in-memory Cursor task without resubmitting it; task records do not survive this MCP server process. Use reap for needs_attention/orphaned work only when it has a bound agentId; it explicitly rechecks that Agent and stores a stable terminal result when possible. Responses stay compact and never acknowledge the result; collect it later with cursor_status(task_id, detail="result"). Use cancel with confirm=true and the exact expected_agent_id to target Stop safely. FIFO or unbound orphans globally block delegation and require manual verification before abandon. Use abandon only with an explicit reason and acknowledge_may_still_write=true; it releases reservations without proving the Cursor Agent stopped.',
|
|
27191
27524
|
inputSchema: {
|
|
27192
27525
|
type: "object",
|
|
27193
27526
|
properties: {
|
|
@@ -27243,14 +27576,14 @@ function buildToolDefinitions(bridgeInstance) {
|
|
|
27243
27576
|
},
|
|
27244
27577
|
{
|
|
27245
27578
|
name: "cursor_status",
|
|
27246
|
-
description: 'Read-only snapshot of Cursor connectivity, queued/running work, reservations, execution availability, persistent model/effort defaults, sessions, and normal/minimal runtime presentation. Compact is the default and never includes or acknowledges a task result. Use detail="
|
|
27579
|
+
description: 'Read-only snapshot of Cursor connectivity, queued/running work, reservations, execution availability, persistent model/effort defaults, sessions, and normal/minimal runtime presentation. Compact is the default and never includes or acknowledges a task result. Use detail="result" with task_id for only the complete reply as plain text, or detail="full" for JSON diagnostics and the reply; both record receipt and allow repeat reads while retained. Pass task_id for its configured and effective model selection or session_id for the durable association; never pass both. This tool never switches Agents, reconciles, or stops work.',
|
|
27247
27580
|
inputSchema: {
|
|
27248
27581
|
type: "object",
|
|
27249
27582
|
additionalProperties: false,
|
|
27250
27583
|
properties: {
|
|
27251
27584
|
task_id: { type: "string", description: "A task ID returned by cursor_do." },
|
|
27252
27585
|
session_id: { type: "string", description: "A persistent session ID returned by cursor_do(session_mode=create)." },
|
|
27253
|
-
detail: { type: "string", enum: ["compact", "full"], default: "compact", description: "compact returns status and
|
|
27586
|
+
detail: { type: "string", enum: ["compact", "full", "result"], default: "compact", description: "compact returns status without collecting the reply. result requires task_id and returns only its complete reply as plain text, recording receipt. full returns complete JSON details and the reply. Explicit result/full reads are repeatable while retained." }
|
|
27254
27587
|
}
|
|
27255
27588
|
}
|
|
27256
27589
|
}
|
|
@@ -27279,10 +27612,13 @@ async function ensureBridgeCursor(targetBridge, reason) {
|
|
|
27279
27612
|
}
|
|
27280
27613
|
function toolErrorResult(error2) {
|
|
27281
27614
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
27282
|
-
if (error2 && error2.uiDiagnostic) {
|
|
27615
|
+
if (error2 && (error2.uiDiagnostic || error2.modelSelection || error2.pickerRead || error2.cdp)) {
|
|
27283
27616
|
const payload = {
|
|
27284
|
-
error: { code: error2.code || error2.uiDiagnostic
|
|
27285
|
-
uiDiagnostic: error2.uiDiagnostic
|
|
27617
|
+
error: { code: error2.code || error2.uiDiagnostic?.code || "CURSOR_BRIDGE_ERROR", message },
|
|
27618
|
+
...error2.uiDiagnostic ? { uiDiagnostic: error2.uiDiagnostic } : {},
|
|
27619
|
+
...error2.modelSelection ? { modelSelection: error2.modelSelection } : {},
|
|
27620
|
+
...error2.pickerRead ? { pickerRead: error2.pickerRead } : {},
|
|
27621
|
+
...error2.cdp ? { cdp: error2.cdp } : {}
|
|
27286
27622
|
};
|
|
27287
27623
|
return {
|
|
27288
27624
|
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
@@ -27368,6 +27704,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request2) => {
|
|
|
27368
27704
|
throw cursorSessionError("STATUS_SELECTOR_AMBIGUOUS", "pass task_id or session_id, not both");
|
|
27369
27705
|
}
|
|
27370
27706
|
const detail = normalizeStatusDetail(args && args.detail);
|
|
27707
|
+
if (detail === "result") {
|
|
27708
|
+
if (!args.task_id || args.session_id) throw new Error('RESULT_TASK_REQUIRED: detail="result" requires task_id only');
|
|
27709
|
+
return { content: [{ type: "text", text: await bridge.status(args.task_id, { detail }) }] };
|
|
27710
|
+
}
|
|
27371
27711
|
const statusMs = Math.max(1e3, Number(process.env.CURSOR_BRIDGE_STATUS_TIMEOUT || 8e3));
|
|
27372
27712
|
let result;
|
|
27373
27713
|
try {
|
|
@@ -81,7 +81,7 @@ function powershellWindowScript(options) {
|
|
|
81
81
|
` Remove-Item -LiteralPath '${showFlagPath}' -Force -ErrorAction SilentlyContinue`,
|
|
82
82
|
"}"
|
|
83
83
|
].join("\n");
|
|
84
|
-
const apply = lifetime ? lifetimeLoop : loop ? `for ($i = 0; $i -lt ${iterations}; $i++) { ${hideIfAllowed}; Start-Sleep -Milliseconds ${intervalMs} }` : `$changed = [CursorBridgeWindowControl]::Apply(${targetPid}, ${show}); [Console]::Out.Write($changed)`;
|
|
84
|
+
const apply = lifetime ? lifetimeLoop : loop ? `for ($i = 0; $i -lt ${iterations}; $i++) { ${hideIfAllowed}; Start-Sleep -Milliseconds ${intervalMs} }` : `$changed = [CursorBridgeWindowControl]::Apply(${targetPid}, ${show}${options.scope === "agents" ? ", $true" : ""}); [Console]::Out.Write($changed)`;
|
|
85
85
|
return `$ErrorActionPreference = 'Stop'
|
|
86
86
|
Add-Type -TypeDefinition @'
|
|
87
87
|
${WINDOW_CONTROL_TYPE}
|
|
@@ -95,6 +95,8 @@ function setCursorWindowPresentation(options = {}) {
|
|
|
95
95
|
const platform = options.platform || process.platform;
|
|
96
96
|
const action = String(options.action || "").trim().toLowerCase();
|
|
97
97
|
if (!["hide", "show"].includes(action)) throw new Error(`unsupported Cursor window action: ${options.action}`);
|
|
98
|
+
const scope = options.scope ?? "process";
|
|
99
|
+
if (!["process", "agents"].includes(scope)) throw new Error(`unsupported Cursor window scope: ${scope}`);
|
|
98
100
|
if (platform !== "win32") {
|
|
99
101
|
return { supported: false, applied: false, action, reason: `window control is not implemented for ${platform}` };
|
|
100
102
|
}
|
|
@@ -105,12 +107,14 @@ function setCursorWindowPresentation(options = {}) {
|
|
|
105
107
|
}
|
|
106
108
|
const showFlagPath = resolve(options.showFlagPath || join2(dirname(resolveCursorRuntimeFile()), `show-${pid}.flag`));
|
|
107
109
|
try {
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
if (scope === "process") {
|
|
111
|
+
if (action === "show") {
|
|
112
|
+
mkdirSync2(dirname(showFlagPath), { recursive: true });
|
|
113
|
+
writeFileSync(showFlagPath, `${pid}
|
|
111
114
|
`, { encoding: "utf8", mode: 384 });
|
|
112
|
-
|
|
113
|
-
|
|
115
|
+
} else {
|
|
116
|
+
rmSync(showFlagPath, { force: true });
|
|
117
|
+
}
|
|
114
118
|
}
|
|
115
119
|
} catch (error) {
|
|
116
120
|
return {
|
|
@@ -124,7 +128,7 @@ function setCursorWindowPresentation(options = {}) {
|
|
|
124
128
|
}
|
|
125
129
|
const run = options.execFileSyncImpl || execFileSync;
|
|
126
130
|
try {
|
|
127
|
-
const script = powershellWindowScript({ pid, action });
|
|
131
|
+
const script = powershellWindowScript({ pid, action, scope });
|
|
128
132
|
const output = run("powershell.exe", [
|
|
129
133
|
"-NoLogo",
|
|
130
134
|
"-NoProfile",
|
|
@@ -140,9 +144,18 @@ function setCursorWindowPresentation(options = {}) {
|
|
|
140
144
|
timeout: Number(options.timeoutMs || 15e3)
|
|
141
145
|
});
|
|
142
146
|
const changedWindows = Number(String(output || "").trim() || 0);
|
|
143
|
-
return {
|
|
147
|
+
return {
|
|
148
|
+
supported: true,
|
|
149
|
+
applied: scope !== "agents" || changedWindows > 0,
|
|
150
|
+
action,
|
|
151
|
+
scope,
|
|
152
|
+
port,
|
|
153
|
+
pid,
|
|
154
|
+
changedWindows,
|
|
155
|
+
...scope === "process" ? { showFlagPath } : {}
|
|
156
|
+
};
|
|
144
157
|
} catch (error) {
|
|
145
|
-
if (action === "show") rmSync(showFlagPath, { force: true });
|
|
158
|
+
if (action === "show" && scope === "process") rmSync(showFlagPath, { force: true });
|
|
146
159
|
return {
|
|
147
160
|
supported: true,
|
|
148
161
|
applied: false,
|
|
@@ -206,6 +219,7 @@ var init_cursor_runtime = __esm({
|
|
|
206
219
|
using System;
|
|
207
220
|
using System.Runtime.InteropServices;
|
|
208
221
|
using System.Text;
|
|
222
|
+
using System.Collections.Generic;
|
|
209
223
|
|
|
210
224
|
public static class CursorBridgeWindowControl {
|
|
211
225
|
[StructLayout(LayoutKind.Sequential)]
|
|
@@ -225,6 +239,7 @@ public static class CursorBridgeWindowControl {
|
|
|
225
239
|
[DllImport("user32.dll", EntryPoint = "IsWindowArranged")] private static extern bool IsWindowArranged(IntPtr hWnd);
|
|
226
240
|
[DllImport("user32.dll")] private static extern bool GetWindowRect(IntPtr hWnd, out RECT rect);
|
|
227
241
|
[DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern int GetWindowTextLengthW(IntPtr hWnd);
|
|
242
|
+
[DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern int GetWindowTextW(IntPtr hWnd, StringBuilder text, int maxCount);
|
|
228
243
|
[DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern int GetClassNameW(IntPtr hWnd, StringBuilder className, int maxCount);
|
|
229
244
|
[DllImport("user32.dll")] private static extern bool ShowWindowAsync(IntPtr hWnd, int command);
|
|
230
245
|
[DllImport("user32.dll")] private static extern bool SetWindowPos(IntPtr hWnd, IntPtr insertAfter, int x, int y, int width, int height, uint flags);
|
|
@@ -248,7 +263,12 @@ public static class CursorBridgeWindowControl {
|
|
|
248
263
|
}
|
|
249
264
|
|
|
250
265
|
public static int Apply(int expectedProcessId, bool show) {
|
|
266
|
+
return Apply(expectedProcessId, show, false);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
public static int Apply(int expectedProcessId, bool show, bool agentsOnly) {
|
|
251
270
|
int changed = 0;
|
|
271
|
+
List<IntPtr> windows = new List<IntPtr>();
|
|
252
272
|
EnumWindows((hWnd, lParam) => {
|
|
253
273
|
uint processId;
|
|
254
274
|
GetWindowThreadProcessId(hWnd, out processId);
|
|
@@ -256,6 +276,17 @@ public static class CursorBridgeWindowControl {
|
|
|
256
276
|
StringBuilder className = new StringBuilder(256);
|
|
257
277
|
GetClassNameW(hWnd, className, className.Capacity);
|
|
258
278
|
if (!String.Equals(className.ToString(), "Chrome_WidgetWin_1", StringComparison.Ordinal)) return true;
|
|
279
|
+
if (agentsOnly) {
|
|
280
|
+
StringBuilder title = new StringBuilder(GetWindowTextLengthW(hWnd) + 1);
|
|
281
|
+
GetWindowTextW(hWnd, title, title.Capacity);
|
|
282
|
+
if (!String.Equals(title.ToString(), "Cursor Agents", StringComparison.Ordinal)) return true;
|
|
283
|
+
}
|
|
284
|
+
windows.Add(hWnd);
|
|
285
|
+
return true;
|
|
286
|
+
}, IntPtr.Zero);
|
|
287
|
+
// Automatic recovery must never broaden an absent or ambiguous Agents match.
|
|
288
|
+
if (agentsOnly && windows.Count != 1) return 0;
|
|
289
|
+
foreach (IntPtr hWnd in windows) {
|
|
259
290
|
bool visible = IsWindowVisible(hWnd);
|
|
260
291
|
if (show) {
|
|
261
292
|
// SWP_SHOWWINDOW + SWP_NOACTIVATE preserves minimized/maximized/arranged
|
|
@@ -283,8 +314,7 @@ public static class CursorBridgeWindowControl {
|
|
|
283
314
|
if (restored || pulsed || redrawn) changed++;
|
|
284
315
|
}
|
|
285
316
|
if (!show && visible) { if (ShowWindowAsync(hWnd, 0)) changed++; }
|
|
286
|
-
|
|
287
|
-
}, IntPtr.Zero);
|
|
317
|
+
}
|
|
288
318
|
return changed;
|
|
289
319
|
}
|
|
290
320
|
}
|
package/extensions/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ const hostWorkspaceId = hostCwd.replace(/\\/g, "/").toLowerCase();
|
|
|
10
10
|
export default createStdioMcpExtension({
|
|
11
11
|
label: "Cursor Bridge",
|
|
12
12
|
clientName: "pi-cursor-bridge",
|
|
13
|
-
packageVersion: "0.2.
|
|
13
|
+
packageVersion: "0.2.1",
|
|
14
14
|
serverName: "cursor-bridge",
|
|
15
15
|
serverScript: join(packageRoot, "dist", "cursor-bridge.mjs"),
|
|
16
16
|
cwd: hostCwd,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cursor-bridge",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Use Cursor Context Engine and bounded, explicitly continuous Cursor Agent execution from the Pi coding agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -47,6 +47,6 @@
|
|
|
47
47
|
},
|
|
48
48
|
"piPackage": {
|
|
49
49
|
"embeddedProduct": "Cursor Bridge",
|
|
50
|
-
"embeddedProductVersion": "6.0.
|
|
50
|
+
"embeddedProductVersion": "6.0.1"
|
|
51
51
|
}
|
|
52
52
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cursor-delegate
|
|
3
|
-
description: "Delegate bounded light-to-medium implementation, investigation, documentation, configuration, testing, and tooling work to Cursor Bridge after the primary agent owns direction and risk boundaries. Also use when the user explicitly asks to create, keep, continue, inspect, or close the same Cursor execution session, including phrases such as '持续会话', '同一个 Cursor 会话', or 'continue the Cursor session'. Generic '继续' is not enough to reuse a session. Poll each turn compactly by task_id, then
|
|
3
|
+
description: "Delegate bounded light-to-medium implementation, investigation, documentation, configuration, testing, and tooling work to Cursor Bridge after the primary agent owns direction and risk boundaries. Also use when the user explicitly asks to create, keep, continue, inspect, or close the same Cursor execution session, including phrases such as '持续会话', '同一个 Cursor 会话', or 'continue the Cursor session'. Generic '继续' is not enough to reuse a session. Poll each turn compactly by task_id, then retrieve its normal terminal reply with detail=result and verify it in the primary agent. Do not use when the user opts out, cursor_do is unavailable or administrator-disabled, or for product direction, architecture decisions, exclusive GUI operations, formal verification verdicts, governance state decisions, or unbounded investigation."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Cursor Delegate
|
|
@@ -21,7 +21,7 @@ Declare `request_context` for each call: an AI caller uses `sender="model"`; set
|
|
|
21
21
|
|
|
22
22
|
Use this responsibility chain:
|
|
23
23
|
|
|
24
|
-
`primary agent defines purpose, invariants, and risk boundaries -> form a bounded task envelope -> Cursor investigates locally and executes within the envelope -> poll compactly and retrieve the terminal
|
|
24
|
+
`primary agent defines purpose, invariants, and risk boundaries -> form a bounded task envelope -> Cursor investigates locally and executes within the envelope -> poll compactly and retrieve the normal terminal reply with detail=result -> primary agent inspects the real changes and verifies them`
|
|
25
25
|
|
|
26
26
|
- Decide what should be achieved, why it matters, what must not change, where Cursor may work, and what evidence makes the result acceptable. Do not delegate product direction, architecture boundaries, or state verdicts.
|
|
27
27
|
- Allow Cursor to locate relevant implementation, compare local approaches, and complete code, documentation, configuration, scripts, tests, and tooling inside those boundaries. Do not require the primary agent to pre-solve the task line by line.
|
|
@@ -85,7 +85,7 @@ The envelope may contain a small number of local implementation `open_questions`
|
|
|
85
85
|
|
|
86
86
|
1. Always query `cursor_status(task_id)` for the exact task. Its default compact view is for normal polling; use `detail="full"` during progress only when detailed diagnostics are needed. Do not treat the currently visible Cursor chat as task identity.
|
|
87
87
|
2. Treat `submitting`, `running`, and `collecting` as normal in-progress states. More than two minutes is not itself a failure; wait for an explicit terminal state.
|
|
88
|
-
3. After a terminal state, call `cursor_status(task_id, detail="
|
|
88
|
+
3. After a terminal state, call `cursor_status(task_id, detail="result")`. It returns the raw complete retained reply without a JSON wrapper and records explicit receipt; check `isError` before treating content as a reply. Repeat result reads remain allowed while the task is retained. Use `detail="full"` when the diagnostic task detail is needed.
|
|
89
89
|
4. Compare Cursor's claimed work with the real diff, `allowed_paths`, and acceptance contract.
|
|
90
90
|
5. When `cursor_status` reports a configured model default, confirm `modelSelection.applied=true` and preserve its configured/effective model and effort fields in any failure report.
|
|
91
91
|
6. Run risk-proportionate verification in the primary agent. Cursor's response alone cannot support a formal pass, verified state, or governance transition.
|
|
@@ -98,7 +98,7 @@ Read [delegation-contract.md](references/delegation-contract.md) for state inter
|
|
|
98
98
|
## Handle abnormal states
|
|
99
99
|
|
|
100
100
|
- For `needs_attention`, `orphaned`, ambiguous state, or an unbound session, assume the real Cursor Agent may still be running. Preserve path ownership and never resubmit automatically.
|
|
101
|
-
- For a parallel orphan with a bound `agent_id`, first call `cursor_task_control(action=reap)`. This explicitly rechecks and, when possible, resumes monitoring or recovers that task's terminal state. It returns only an action/state summary; after a terminal state, retrieve
|
|
101
|
+
- For a parallel orphan with a bound `agent_id`, first call `cursor_task_control(action=reap)`. This explicitly rechecks and, when possible, resumes monitoring or recovers that task's terminal state. It returns only an action/state summary; after a terminal state, retrieve the normal reply with `cursor_status(task_id, detail="result")` or use `detail="full"` for diagnostics.
|
|
102
102
|
- For an unbound FIFO or any orphan without an `agent_id`, do not call `reap` as if an identity existed. It globally blocks delegation; manually verify Cursor has stopped, then use the explicitly acknowledged `abandon` path.
|
|
103
103
|
- To stop a bound task, use `cursor_task_control(action=cancel, confirm=true, expected_agent_id=<exact id>)`. This includes FIFO tasks that have published an Agent ID. If Stop cannot be confirmed, the reservation remains held.
|
|
104
104
|
- Use `action=abandon` only after manual verification and an explicit user decision to accept the risk. It requires `confirm=true`, a non-empty reason, `acknowledge_may_still_write=true`, and the exact `expected_agent_id` when one is already bound; report that the underlying Agent may still run or write.
|
|
@@ -69,15 +69,15 @@ Compact collection flow:
|
|
|
69
69
|
|
|
70
70
|
1. After `cursor_do(background=true)`, save the compact receipt's `task_id`.
|
|
71
71
|
2. Poll `cursor_status(task_id)` with its default compact view while the task is active.
|
|
72
|
-
3. Once terminal, call `cursor_status(task_id, detail="
|
|
72
|
+
3. Once terminal, call `cursor_status(task_id, detail="result")` to retrieve the raw retained reply and record receipt. It has no JSON wrapper, so check `isError` before treating content as a reply. Use `detail="full"` when task diagnostics are needed; repeat either explicit read is allowed while the task record remains retained.
|
|
73
73
|
|
|
74
74
|
## Identity and collection contract
|
|
75
75
|
|
|
76
76
|
- `task_id` is the stable identity used by the primary agent to query and summarize a task. Save it immediately after dispatch.
|
|
77
77
|
- `agent_id` binds a task to one specific Agents Window session when Bridge publishes it. `parallel_agent` always needs this identity. FIFO may also publish one; if it does not, do not assume a safe Stop target.
|
|
78
78
|
- Determine task state only through `cursor_status(task_id)`, not the currently selected chat or latest visible response. Its default compact view never returns a result body or records receipt.
|
|
79
|
-
- After a terminal state, use `cursor_status(task_id, detail="
|
|
80
|
-
- `cursor_task_control` returns an action and compact task-state summary without a result body or implicit receipt. Retrieve a terminal
|
|
79
|
+
- After a terminal state, use `cursor_status(task_id, detail="result")` to retrieve the plain complete retained reply and record explicit receipt. Use `detail="full"` when task diagnostics are needed. A collected result should include at least task state, summary, changed files, validation performed, failures or blockers, and the raw Cursor response.
|
|
80
|
+
- `cursor_task_control` returns an action and compact task-state summary without a result body or implicit receipt. Retrieve a terminal reply afterward with `detail="result"`, or use `detail="full"` for diagnostics.
|
|
81
81
|
- `cursor_session_control(action=collect_result)` always returns the full session reply.
|
|
82
82
|
- Do not require a unique completion marker or minimum response length. Bridge determines completion from Agent state, stopped generation, and response stability.
|
|
83
83
|
|
|
@@ -86,15 +86,15 @@ Compact collection flow:
|
|
|
86
86
|
| State or phase | Primary-agent action |
|
|
87
87
|
|---|---|
|
|
88
88
|
| `queued/submitting/running/collecting` | Keep the original task and continue polling by `task_id`. More than two minutes is not a failure. |
|
|
89
|
-
| `completed` | Call `cursor_status(task_id, detail="
|
|
89
|
+
| `completed` | Call `cursor_status(task_id, detail="result")` to read the raw response, then inspect the real diff, allowed paths, and completion contract. Use `detail="full"` when diagnostics are needed. |
|
|
90
90
|
| `failed` | Read the explicit error and determine whether the Cursor Agent actually failed before deciding to rework. |
|
|
91
91
|
| `needs_attention/orphaned` with bound `agent_id` | Preserve path ownership and explicitly call `cursor_task_control(action=reap)` for the same in-memory task. Do not resubmit automatically. |
|
|
92
92
|
| FIFO or unbound orphan | A global reservation blocks all new delegation. If an `agent_id` was published, use targeted `cancel`. Otherwise manually verify Cursor has stopped, then use explicitly acknowledged `abandon`; there is no safe `reap` target. |
|
|
93
|
-
| `terminal_uncollected` | Agent History is stably terminal but the final response extraction failed. Keep the reservation and retry explicit `reap`; after recovery, use
|
|
93
|
+
| `terminal_uncollected` | Agent History is stably terminal but the final response extraction failed. Keep the reservation and retry explicit `reap`; after recovery, use `detail="result"` to retrieve the reply or `detail="full"` for diagnostics. Do not release on one DOM failure. |
|
|
94
94
|
| `cancelled` | The exact Agent Stop action or an unsent queued cancellation was confirmed; the reservation is released. |
|
|
95
95
|
| `abandoned` | The reservation was explicitly released without proof that the underlying Agent stopped. Treat the warning as live risk and inspect workspace changes before any overlapping write. |
|
|
96
96
|
|
|
97
|
-
For an R6-style false negative, continue compact polling of the original `task_id` when Agent History already contains a complete final response but automatic collection has not finished. Bridge should retry extraction against the original `agent_id`; once terminal, use
|
|
97
|
+
For an R6-style false negative, continue compact polling of the original `task_id` when Agent History already contains a complete final response but automatic collection has not finished. Bridge should retry extraction against the original `agent_id`; once terminal, use `detail="result"` or `detail="full"` for diagnostics. Do not work around collection by requiring a longer reply, injecting a completion marker, or submitting the same task again.
|
|
98
98
|
|
|
99
99
|
## Primary-agent acceptance contract
|
|
100
100
|
|
|
@@ -114,5 +114,5 @@ Cursor's completion statement means only that delegated execution ended; it is n
|
|
|
114
114
|
- Stop automatic integration when parallel tasks conflict and return the batch to primary-agent review.
|
|
115
115
|
- If Agent History or the response DOM is temporarily unreadable, let Bridge wait and retry against the same `agent_id`. Enter `needs_attention` after persistent failure; do not incorrectly mark the task complete or create a duplicate Agent.
|
|
116
116
|
- For a bound orphan, use `reap` before `cancel`. `cancel` requires the exact `expected_agent_id` and only releases after stable Stop evidence. `abandon` requires explicit confirmation, a reason, acknowledgement that the Agent may still write, and the exact `expected_agent_id` when one is bound.
|
|
117
|
-
- Default compact `cursor_status` is a pure snapshot. `cursor_status(task_id, detail="
|
|
117
|
+
- Default compact `cursor_status` is a pure snapshot. `cursor_status(task_id, detail="result")` is the normal explicit result-receipt operation; `detail="full"` also records receipt and retains diagnostic detail. Reconciliation still happens only through explicit `cursor_task_control`.
|
|
118
118
|
- Task records and reservations live only for the current Bridge MCP process. After restart, inspect Cursor Agent History and the workspace manually; persistent cross-process task leases are outside the current contract.
|