pi-cursor-bridge 0.2.0 → 0.2.2

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursor-bridge",
3
- "version": "6.0.0+codex.20260908194916",
3
+ "version": "6.0.2+codex.20260913042930",
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.0 embeds Cursor Bridge 6.0.0, including explicit request provenance for CCE and delegated tasks. Its breaking result contract returns a compact receipt for background `cursor_do` work: poll `cursor_status(task_id)` normally, then retrieve a terminal result with `cursor_status(task_id, detail="full")`. Cursor must be installed and signed in. Current end-to-end compatibility claims remain scoped to the environments documented in the main repository.
13
+ Pi wrapper 0.2.2 embeds Cursor Bridge 6.0.2, 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)
@@ -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 (action === "show") {
11003
- mkdirSync(dirname(showFlagPath), { recursive: true });
11004
- writeFileSync(showFlagPath, `${pid}
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
- } else {
11007
- rmSync(showFlagPath, { force: true });
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 { supported: true, applied: true, action, port, pid, changedWindows, showFlagPath };
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
- return true;
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.0";
22904
+ var PLUGIN_VERSION = "6.0.2";
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 Promise.race([
22793
- c.ready,
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 makeClient(wsUrl) {
22848
- const ws = new import_websocket.default(wsUrl, { origin: ORIGIN });
22849
- let id = 0;
22850
- const pending = /* @__PURE__ */ new Map();
22851
- const failAll = (msg) => {
22852
- for (const { rej } of pending.values()) {
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) throw new Error("Page exception: " + (r.exceptionDetails.exception && r.exceptionDetails.exception.description || r.exceptionDetails.text));
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) {
@@ -23161,7 +23400,7 @@ function classifyModelPickerRowKind(text, hasItemName, menuTestId, submenuTestId
23161
23400
  var EXPR_MODEL_PICKER_ROWS = `(function(){
23162
23401
  ${MODEL_PICKER_VISIBLE_BODY}
23163
23402
  ${classifyModelPickerRowKind.toString()}
23164
- const menus=[...document.querySelectorAll('[data-testid="model-picker-menu"],[data-testid*="model-parameters"],[data-testid*="parameter-submenu"],[data-component="menu-popup"][data-submenu]')].filter(visible);
23403
+ const menus=[...document.querySelectorAll('[data-testid="model-picker-menu"],[data-testid="selected-auto-menu"],[data-testid*="model-parameters"],[data-testid*="parameter-submenu"],[data-component="menu-popup"][data-submenu]')].filter(visible);
23165
23404
  const rows=[];
23166
23405
  const seen=new Set();
23167
23406
  for(const menu of menus){
@@ -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="full") before continuing`);
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 = makeClient(page.webSocketDebuggerUrl);
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 = makeClient(page.webSocketDebuggerUrl);
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 _readModelPickerTrigger(c) {
25439
+ async _readModelPickerValue(c, expression, kind, { timeoutMs = 3e3 } = {}) {
25440
+ const startedAt = Date.now();
25199
25441
  try {
25200
- return JSON.parse(await evalJS(c, EXPR_MODEL_PICKER_TRIGGER) || "{}");
25201
- } catch {
25202
- return { found: false, state: "trigger_unreadable" };
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 _readModelPickerRows(c) {
25206
- try {
25207
- const snapshot = JSON.parse(await evalJS(c, EXPR_MODEL_PICKER_ROWS) || "{}");
25208
- return { open: snapshot.open === true, rows: Array.isArray(snapshot.rows) ? snapshot.rows : [] };
25209
- } catch {
25210
- return { open: false, rows: [] };
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
- const snapshot = await this._readModelPickerRows(c);
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
- const effortControl = (snapshot.rows || []).find((row) => row.kind === "effort_control" && row.disabled !== true);
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
- if (result2.state !== "not_rendered") return { ...result2, attempts };
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
- { available: outcome.available || [], attempts: outcome.attempts || [] }
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
- const trigger = await this._readModelPickerTrigger(c);
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 = makeClient(page.webSocketDebuggerUrl);
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 = makeClient(page.webSocketDebuggerUrl);
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 = makeClient(page.webSocketDebuggerUrl);
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 = makeClient(page.webSocketDebuggerUrl);
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 = makeClient(page.webSocketDebuggerUrl);
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 = makeClient(page.webSocketDebuggerUrl);
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="full"), or wait for active tasks before submitting more work.');
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 full");
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="full"); 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.',
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="full"). 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.',
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="full" with task_id to receive the complete legacy task details and result; this marks the result collected while keeping repeat full reads available. 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.',
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 safety metadata without the result body or receipt side effect. full returns legacy complete details and explicitly collects a task result." }
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.code || "CURSOR_BRIDGE_ERROR", message },
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 (action === "show") {
109
- mkdirSync2(dirname(showFlagPath), { recursive: true });
110
- writeFileSync(showFlagPath, `${pid}
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
- } else {
113
- rmSync(showFlagPath, { force: true });
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 { supported: true, applied: true, action, port, pid, changedWindows, showFlagPath };
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
- return true;
287
- }, IntPtr.Zero);
317
+ }
288
318
  return changed;
289
319
  }
290
320
  }
@@ -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.0",
13
+ packageVersion: "0.2.2",
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.0",
3
+ "version": "0.2.2",
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.0"
50
+ "embeddedProductVersion": "6.0.2"
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 explicitly retrieve a terminal result by task_id 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."
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 result by task_id -> primary agent inspects the real changes and verifies them`
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="full")`. It returns the complete retained result and records explicit receipt; repeat full reads remain allowed while the task is retained.
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 any result with `cursor_status(task_id, detail="full")`.
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="full")` to retrieve the full retained result and record receipt. Repeat explicit full reads are allowed while the task record remains retained.
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="full")` to retrieve the complete retained result and record explicit receipt. 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 result afterward through explicit full task status.
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="full")` to read the raw response, then inspect the real diff, allowed paths, and completion contract. |
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 full task status to retrieve the result. Do not release on one DOM failure. |
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 explicit full task status. Do not work around collection by requiring a longer reply, injecting a completion marker, or submitting the same task again.
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="full")` is the explicit result-receipt operation; reconciliation still happens only through explicit `cursor_task_control`.
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.