pi-cursor-bridge 0.1.12 → 0.1.14

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": "5.8.2+codex.20260904092304",
3
+ "version": "5.9.1+codex.20260905095629",
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
- This Pi package embeds Cursor Bridge 5.8.2. Cursor must be installed and signed in. Current end-to-end compatibility claims remain scoped to the environments documented in the main repository.
13
+ This Pi package embeds Cursor Bridge 5.9.1. 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)
@@ -22604,7 +22604,7 @@ function updateCursorSessionRegistry(filePath, mutator, options = {}) {
22604
22604
  // server.mjs
22605
22605
  init_cursor_ensure_core();
22606
22606
  init_lifecycle_paths();
22607
- var PLUGIN_VERSION = "5.8.2";
22607
+ var PLUGIN_VERSION = "5.9.1";
22608
22608
  var CDP_PORT2 = Number(process.env.CURSOR_BRIDGE_CDP_PORT || 9223);
22609
22609
  var ORIGIN = `http://localhost:${CDP_PORT2}`;
22610
22610
  var QUERY_TIMEOUT = Number(process.env.CURSOR_BRIDGE_TIMEOUT || 3e5);
@@ -23116,8 +23116,22 @@ var EXPR_MODEL_PICKER_TRIGGER = `(function(){
23116
23116
  const detail=String(trigger.querySelector('.ui-model-picker__trigger-variant-suffix,.vscode-model-picker__trigger-variant-suffix')?.innerText||'').replace(/\\s+/g,' ').trim();
23117
23117
  return JSON.stringify({found:true,state:'ready',text,detail,x:Math.round(rect.x+rect.width/2),y:Math.round(rect.y+rect.height/2)});
23118
23118
  })()`;
23119
+ function classifyModelPickerRowKind(text, hasItemName, menuTestId, submenuTestId, ariaHaspopup, hasParamCheck, inSubmenu) {
23120
+ const menu = String(menuTestId || "").toLowerCase();
23121
+ const submenu = String(submenuTestId || "").toLowerCase();
23122
+ const haystack = menu + " " + submenu;
23123
+ if (hasItemName) return "model";
23124
+ if (/^model(?:\s|$)/i.test(String(text || "")) && ariaHaspopup === "menu") return "model_control";
23125
+ if (/^effort(?:\s|$)/i.test(String(text || "")) && ariaHaspopup === "menu") return "effort_control";
23126
+ if (haystack.includes("parameter-submenu") || hasParamCheck) return "parameter";
23127
+ if (/^add models$/i.test(String(text || "").trim())) return "control";
23128
+ if (haystack.includes("model-list") || haystack.includes("model-picker-menu") || haystack.includes("model-selection")) return "model";
23129
+ if (inSubmenu) return "parameter";
23130
+ return "control";
23131
+ }
23119
23132
  var EXPR_MODEL_PICKER_ROWS = `(function(){
23120
23133
  ${MODEL_PICKER_VISIBLE_BODY}
23134
+ ${classifyModelPickerRowKind.toString()}
23121
23135
  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);
23122
23136
  const rows=[];
23123
23137
  const seen=new Set();
@@ -23128,13 +23142,18 @@ var EXPR_MODEL_PICKER_ROWS = `(function(){
23128
23142
  const rect=row.getBoundingClientRect();
23129
23143
  const text=String(row.innerText||row.textContent||'').replace(/\\s+/g,' ').trim();
23130
23144
  if(!text)continue;
23131
- const menuTestId=String(menu.getAttribute('data-testid')||'').toLowerCase();
23132
- let kind='control';
23133
- if(row.querySelector('.ui-model-picker__item-content-name,.vscode-model-picker__item-content-name'))kind='model';
23134
- else if(/^model(?:\\s|$)/i.test(text)&&row.getAttribute('aria-haspopup')==='menu')kind='model_control';
23135
- else if(/^effort(?:\\s|$)/i.test(text)&&row.getAttribute('aria-haspopup')==='menu')kind='effort_control';
23136
- else if(menuTestId.includes('parameter-submenu')||row.closest('[data-submenu]'))kind='parameter';
23137
- else if(menuTestId.includes('model-picker-menu')||menuTestId.includes('model-selection'))kind='model';
23145
+ const menuTestId=String(menu.getAttribute('data-testid')||'');
23146
+ const submenuRoot=row.closest('[data-submenu],[data-testid*="parameter-submenu"],[data-testid*="model-list"]');
23147
+ const submenuTestId=String((submenuRoot&&submenuRoot.getAttribute('data-testid'))||'');
23148
+ const kind=classifyModelPickerRowKind(
23149
+ text,
23150
+ !!row.querySelector('.ui-model-picker__item-content-name,.vscode-model-picker__item-content-name'),
23151
+ menuTestId,
23152
+ submenuTestId,
23153
+ row.getAttribute('aria-haspopup'),
23154
+ !!row.querySelector('.ui-model-picker__param-check,.vscode-model-picker__param-check'),
23155
+ !!row.closest('[data-submenu]')
23156
+ );
23138
23157
  rows.push({
23139
23158
  text,
23140
23159
  kind,
@@ -23152,6 +23171,14 @@ var EXPR_MODEL_PICKER_ROWS = `(function(){
23152
23171
  function normalizeModelPickerText(value) {
23153
23172
  return String(value || "").trim().toLowerCase().replace(/extra[\s_-]*high/g, "xhigh").replace(/[^a-z0-9]+/g, "");
23154
23173
  }
23174
+ function isCursorEffortOptionText(text) {
23175
+ return CURSOR_MODEL_EFFORTS.includes(normalizeModelPickerText(text));
23176
+ }
23177
+ function modelPickerAvailableIsDecisive(kind, available) {
23178
+ if (!Array.isArray(available) || available.length === 0) return false;
23179
+ if (kind !== "parameter") return true;
23180
+ return available.some((text) => isCursorEffortOptionText(text));
23181
+ }
23155
23182
  function selectModelPickerRow(rows, requested, kind = "model") {
23156
23183
  const wanted = normalizeModelPickerText(requested);
23157
23184
  if (!wanted) return null;
@@ -23690,6 +23717,7 @@ var CursorBridge = class {
23690
23717
  this.projectPath = options.projectPath !== void 0 ? resolve7(String(options.projectPath)) : persistedWorkspace && persistedWorkspace.projectPath || null;
23691
23718
  this.workspaceSource = options.projectPath !== void 0 ? "constructor" : persistedWorkspace ? "persistent_init" : "auto_detect";
23692
23719
  this.workspaceUpdatedAt = persistedWorkspace && persistedWorkspace.updatedAt || null;
23720
+ this.workspaceConfirmationRequired = this.workspaceKey === "default" && !!persistedWorkspace;
23693
23721
  this.modelPreferencesFile = options.modelPreferencesFile === null ? null : resolve7(options.modelPreferencesFile || resolveCursorModelPreferencesFile());
23694
23722
  this.modelPreferences = readCursorModelPreferences(this.modelPreferencesFile);
23695
23723
  this.sessionFile = options.sessionFile === null ? null : resolve7(options.sessionFile || resolveCursorSessionRegistryFile());
@@ -23712,6 +23740,8 @@ var CursorBridge = class {
23712
23740
  const resolvedProjectPath = this.projectPath || this._lastLifecycle && this._lastLifecycle.projectPath || null;
23713
23741
  return {
23714
23742
  workspaceKey: this.workspaceKey,
23743
+ workspaceConfirmationRequired: this.workspaceConfirmationRequired,
23744
+ workspaceBindingWarning: this.workspaceConfirmationRequired ? "This adapter has no host workspace identity. Run cursor_init with the intended project before using its shared saved binding." : null,
23715
23745
  projectPath: resolvedProjectPath,
23716
23746
  workspaceSource: this.projectPath ? this.workspaceSource : resolvedProjectPath ? "host_auto_detect" : this.workspaceSource,
23717
23747
  workspaceUpdatedAt: this.workspaceUpdatedAt,
@@ -23746,7 +23776,10 @@ var CursorBridge = class {
23746
23776
  agentLabel: session.agentLabel || null,
23747
23777
  turnIndex: Number(session.turnIndex || 0),
23748
23778
  activeTaskId: session.activeTaskId || null,
23749
- lastTask: session.lastTask || null,
23779
+ lastTask: session.lastTask ? {
23780
+ ...session.lastTask,
23781
+ resultUnavailable: this.tasks.get(session.lastTask.taskId)?.result == null
23782
+ } : null,
23750
23783
  modelPreference: session.modelPreference || null,
23751
23784
  readOnly: session.scopeEnvelope && session.scopeEnvelope.readOnly === true,
23752
23785
  allowedPaths: session.scopeEnvelope && Array.isArray(session.scopeEnvelope.allowedPaths) ? session.scopeEnvelope.allowedPaths : [],
@@ -23758,6 +23791,7 @@ var CursorBridge = class {
23758
23791
  };
23759
23792
  }
23760
23793
  _claimNewSession({ taskId, projectPath, readOnly, allowedPaths, modelPreference, requestId, timeoutMs }) {
23794
+ this._ensureTaskCapacity();
23761
23795
  if (!this.sessionFile) throw cursorSessionError("SESSION_STORAGE_DISABLED", "persistent session storage is disabled");
23762
23796
  const sessionId = createCursorSessionId();
23763
23797
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -23803,6 +23837,7 @@ var CursorBridge = class {
23803
23837
  if (requestId && session.lastRequestId === requestId) {
23804
23838
  return { duplicate: true, session: { ...session } };
23805
23839
  }
23840
+ this._ensureTaskCapacity();
23806
23841
  if (!sameSessionProject(session.projectPath, projectPath)) {
23807
23842
  throw cursorSessionError("SESSION_WORKSPACE_MISMATCH", `session is bound to ${session.projectPath}`);
23808
23843
  }
@@ -23822,6 +23857,15 @@ var CursorBridge = class {
23822
23857
  if (session.state !== "ready") {
23823
23858
  throw cursorSessionError("SESSION_NOT_READY", `state=${session.state}; recovery=${session.recoveryState || "none"}`);
23824
23859
  }
23860
+ if (session.lastTask?.status === "completed" && !session.lastTask.resultCollectedAt && this.tasks.has(session.lastTask.taskId)) {
23861
+ throw cursorSessionError("SESSION_RESULT_UNCOLLECTED", `read cursor_status(task_id=${session.lastTask.taskId}) before continuing`);
23862
+ }
23863
+ if (session.lastTask?.status === "completed" && !session.lastTask.resultCollectedAt && !this.tasks.has(session.lastTask.taskId) && session.recoveryState !== "reconciled_result_uncollected") {
23864
+ throw cursorSessionError("SESSION_RECONCILE_REQUIRED", "the prior reply was not read before this adapter restarted; reconcile the exact Agent before continuing");
23865
+ }
23866
+ if (session.recoveryState === "reconciled_result_uncollected" && (Number(session.turnIndex) <= 1 || session.responseBaseline)) {
23867
+ throw cursorSessionError("SESSION_RESULT_UNCOLLECTED", "collect the interrupted reply with cursor_session_control(action=collect_result) before continuing");
23868
+ }
23825
23869
  if (!session.agentId) throw cursorSessionError("SESSION_AGENT_NOT_BOUND", sessionId);
23826
23870
  const envelope = session.scopeEnvelope || { readOnly: false, allowedPaths: [] };
23827
23871
  if (envelope.readOnly === true) {
@@ -23841,6 +23885,7 @@ var CursorBridge = class {
23841
23885
  session.epoch = epoch;
23842
23886
  session.activeTaskId = taskId;
23843
23887
  session.lastRequestId = requestId || null;
23888
+ delete session.responseBaseline;
23844
23889
  session.lease = {
23845
23890
  taskId,
23846
23891
  instanceId: this.sessionInstanceId,
@@ -23869,6 +23914,20 @@ var CursorBridge = class {
23869
23914
  });
23870
23915
  job.sessionState = "busy";
23871
23916
  }
23917
+ _persistSessionResponseBaseline(job) {
23918
+ if (!job.sessionId) return;
23919
+ const baseline = job.responseBaseline;
23920
+ if (!baseline || !["messageCount", "replyLength", "replyHash"].every((key) => Number.isFinite(baseline[key]))) {
23921
+ throw cursorSessionError("SESSION_BASELINE_UNAVAILABLE", "a numeric response baseline is required before sending");
23922
+ }
23923
+ updateCursorSessionRegistry(this.sessionFile, (registry2) => {
23924
+ const session = registry2.sessions[job.sessionId];
23925
+ if (!session || session.activeTaskId !== job.id || Number(session.epoch) !== Number(job.sessionEpoch)) {
23926
+ throw cursorSessionError("SESSION_STALE_SENDER", "the sender no longer owns this session turn");
23927
+ }
23928
+ session.responseBaseline = { messageCount: baseline.messageCount, replyLength: baseline.replyLength, replyHash: baseline.replyHash };
23929
+ });
23930
+ }
23872
23931
  _settleSessionJob(job, outcome, options = {}) {
23873
23932
  if (!job || !job.sessionId || !this.sessionFile) return;
23874
23933
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -23896,6 +23955,7 @@ var CursorBridge = class {
23896
23955
  }
23897
23956
  session.activeTaskId = null;
23898
23957
  session.lease = null;
23958
+ if (outcome !== "completed") delete session.responseBaseline;
23899
23959
  session.recoveryState = null;
23900
23960
  session.attention = null;
23901
23961
  session.state = session.agentId ? "ready" : "failed";
@@ -23910,6 +23970,19 @@ var CursorBridge = class {
23910
23970
  job.sessionError = error2 instanceof Error ? error2.message : String(error2);
23911
23971
  }
23912
23972
  }
23973
+ _markTaskResultCollected(job) {
23974
+ if (!isTerminalTask(job) || job.result == null || job.resultCollectedAt) return;
23975
+ const now = (/* @__PURE__ */ new Date()).toISOString();
23976
+ if (job.sessionId && this.sessionFile) {
23977
+ updateCursorSessionRegistry(this.sessionFile, (registry2) => {
23978
+ const session = registry2.sessions[job.sessionId];
23979
+ if (!session || session.lastTask?.taskId !== job.id || Number(session.epoch) !== Number(job.sessionEpoch)) return;
23980
+ session.lastTask.resultCollectedAt = now;
23981
+ delete session.responseBaseline;
23982
+ });
23983
+ }
23984
+ job.resultCollectedAt = now;
23985
+ }
23913
23986
  sessionStatus(sessionId) {
23914
23987
  const session = this._readSession(sessionId);
23915
23988
  return {
@@ -23918,17 +23991,28 @@ var CursorBridge = class {
23918
23991
  ...session ? this._sessionView(session) : { sessionId: String(sessionId || "") }
23919
23992
  };
23920
23993
  }
23921
- async _reconcileSession(sessionId) {
23994
+ async _reconcileSession(sessionId, { collectResult = false } = {}) {
23995
+ const action = collectResult ? "collect_result" : "reconcile";
23922
23996
  const initial = this._readSession(sessionId);
23923
- if (!initial) return { found: false, action: "reconcile", sessionId };
23924
- if (initial.state === "ready" || initial.state === "closed" || initial.state === "failed") {
23925
- return { found: true, changed: false, action: "reconcile", ...this._sessionView(initial) };
23997
+ if (!initial) return { found: false, action, sessionId };
23998
+ if (collectResult && initial.recoveryState === "reconciled_result_collected") {
23999
+ return { found: true, changed: false, action, ...this._sessionView(initial), state: "already_collected", resultPersisted: false };
24000
+ }
24001
+ const unreadAfterRestart = initial.lastTask?.status === "completed" && !initial.lastTask.resultCollectedAt && !this.tasks.has(initial.lastTask.taskId);
24002
+ if (!collectResult && initial.state === "ready" && !unreadAfterRestart || initial.state === "closed" || initial.state === "failed") {
24003
+ return { found: true, changed: false, action, ...this._sessionView(initial) };
24004
+ }
24005
+ if (collectResult && (initial.state !== "ready" || initial.activeTaskId || initial.lastTask?.status !== "completed" || initial.recoveryState !== "reconciled_result_uncollected")) {
24006
+ throw cursorSessionError("SESSION_RESULT_NOT_READY", "reconcile a completed turn before collecting its reply");
24007
+ }
24008
+ if (collectResult && Number(initial.turnIndex) > 1 && !initial.responseBaseline) {
24009
+ throw cursorSessionError("SESSION_RESULT_BASELINE_UNAVAILABLE", "this older interrupted turn has no reply signature; inspect the original Agent manually before continuing");
23926
24010
  }
23927
24011
  if (!initial.agentId) {
23928
24012
  return {
23929
24013
  found: true,
23930
24014
  changed: false,
23931
- action: "reconcile",
24015
+ action,
23932
24016
  state: "agent_missing",
23933
24017
  attention: "No exact agentId is available. Confirm the Cursor task state manually before explicit abandon.",
23934
24018
  ...this._sessionView(initial)
@@ -23946,21 +24030,44 @@ var CursorBridge = class {
23946
24030
  effectiveExecution: "parallel_agent",
23947
24031
  lastRecoveryAt: null,
23948
24032
  recoveryState: null,
23949
- error: null
24033
+ error: null,
24034
+ responseBaseline: initial.responseBaseline || null
23950
24035
  };
23951
24036
  const observed = await this._readStableParallelEntry(probe);
24037
+ let collectedReply;
24038
+ if (collectResult) {
24039
+ if (!observed.stable || !observed.entry || observed.entry.showSpinner || classifyParallelTerminalIcon(observed.entry.icon) !== "completed") {
24040
+ throw cursorSessionError("SESSION_RESULT_NOT_READY", "the exact Agent must have a stable completed state");
24041
+ }
24042
+ try {
24043
+ collectedReply = await this._withUiLock(() => this._collectParallelAgent(probe));
24044
+ } catch (error2) {
24045
+ if (probe.uiDiagnostic && error2 && typeof error2 === "object") error2.uiDiagnostic = probe.uiDiagnostic;
24046
+ throw error2;
24047
+ }
24048
+ if (!String(collectedReply || "").trim()) throw cursorSessionError("SESSION_RESULT_UNAVAILABLE", "no final reply was collected");
24049
+ }
23952
24050
  let result;
23953
24051
  updateCursorSessionRegistry(this.sessionFile, (registry2) => {
23954
24052
  const session = registry2.sessions[sessionId];
23955
24053
  if (!session) {
23956
- result = { found: false, action: "reconcile", sessionId };
24054
+ result = { found: false, action, sessionId };
23957
24055
  return;
23958
24056
  }
23959
- if (Number(session.epoch || 0) !== Number(initial.epoch || 0) || session.activeTaskId !== initial.activeTaskId) {
23960
- result = { found: true, changed: false, action: "reconcile", state: "stale_observation", ...this._sessionView(session) };
24057
+ if (Number(session.epoch || 0) !== Number(initial.epoch || 0) || session.activeTaskId !== initial.activeTaskId || session.state !== initial.state) {
24058
+ result = { found: true, changed: false, action, state: "stale_observation", ...this._sessionView(session) };
23961
24059
  return;
23962
24060
  }
23963
24061
  const now = (/* @__PURE__ */ new Date()).toISOString();
24062
+ if (collectResult) {
24063
+ session.lastTask.resultCollectedAt = now;
24064
+ delete session.responseBaseline;
24065
+ session.recoveryState = "reconciled_result_collected";
24066
+ session.attention = null;
24067
+ session.updatedAt = now;
24068
+ result = { found: true, changed: true, action, ...this._sessionView(session), result: collectedReply, resultPersisted: false, uiDiagnostic: probe.uiDiagnostic || null };
24069
+ return;
24070
+ }
23964
24071
  if (!observed.stable || !observed.entry) {
23965
24072
  session.state = "needs_attention";
23966
24073
  session.recoveryState = probe.recoveryState || "history_unavailable";
@@ -24000,7 +24107,8 @@ var CursorBridge = class {
24000
24107
  session.lease = null;
24001
24108
  session.state = "ready";
24002
24109
  session.recoveryState = terminalClass === "completed" ? "reconciled_result_uncollected" : null;
24003
- session.attention = terminalClass === "completed" ? "The interrupted turn completed in Cursor, but its reply was not persisted. Inspect that Agent before the next turn." : null;
24110
+ session.attention = terminalClass === "completed" ? Number(session.turnIndex) > 1 && !session.responseBaseline ? "This older interrupted turn has no saved reply signature. Inspect the original Agent manually before continuing; automatic collection cannot identify its reply safely." : "The interrupted turn completed in Cursor, but its reply was not persisted. Use cursor_session_control(action=collect_result) before the next turn." : null;
24111
+ if (terminalClass !== "completed") delete session.responseBaseline;
24004
24112
  session.updatedAt = now;
24005
24113
  result = { found: true, changed: true, action: "reconcile", state: terminalClass, ...this._sessionView(session) };
24006
24114
  });
@@ -24010,10 +24118,11 @@ var CursorBridge = class {
24010
24118
  const id = String(sessionId || "").trim();
24011
24119
  if (!id) throw cursorSessionError("SESSION_REQUIRED", "session_id must not be empty");
24012
24120
  const action = String(options.action || "").trim().toLowerCase();
24013
- if (!["reconcile", "close", "forget", "abandon"].includes(action)) {
24014
- throw cursorSessionError("SESSION_ACTION_UNSUPPORTED", "expected reconcile, close, forget, or abandon");
24121
+ if (!["reconcile", "collect_result", "close", "forget", "abandon"].includes(action)) {
24122
+ throw cursorSessionError("SESSION_ACTION_UNSUPPORTED", "expected reconcile, collect_result, close, forget, or abandon");
24015
24123
  }
24016
24124
  if (action === "reconcile") return this._reconcileSession(id);
24125
+ if (action === "collect_result") return this._reconcileSession(id, { collectResult: true });
24017
24126
  let result;
24018
24127
  updateCursorSessionRegistry(this.sessionFile, (registry2) => {
24019
24128
  const session = registry2.sessions[id];
@@ -24071,6 +24180,7 @@ var CursorBridge = class {
24071
24180
  }
24072
24181
  const previousProjectPath = this.projectPath || this._lastLifecycle && this._lastLifecycle.projectPath || null;
24073
24182
  const saved = writeWorkspaceBinding(this.workspaceFile, this.workspaceKey, projectPath);
24183
+ this.workspaceConfirmationRequired = false;
24074
24184
  this.projectPath = saved.projectPath;
24075
24185
  this.workspaceSource = "persistent_init";
24076
24186
  this.workspaceUpdatedAt = saved.updatedAt;
@@ -24265,6 +24375,7 @@ var CursorBridge = class {
24265
24375
  const text = String(query || "").trim();
24266
24376
  if (!text) throw new Error("query must not be empty");
24267
24377
  if (text.length > 2e4) throw new Error("query exceeds the 20,000-character limit");
24378
+ this._assertWorkspaceConfirmed();
24268
24379
  if (this._hasGlobalReservation()) {
24269
24380
  throw new Error("A global Cursor reservation has an unconfirmed Stop state; resolve blockingTaskIds from cursor_status first");
24270
24381
  }
@@ -24277,7 +24388,9 @@ var CursorBridge = class {
24277
24388
  allowedPaths: [],
24278
24389
  modelPreference: this._modelPreferenceFor("cce")
24279
24390
  });
24280
- return normalizeCceSearchResult(await job.promise);
24391
+ const result = normalizeCceSearchResult(await job.promise);
24392
+ this._markTaskResultCollected(job);
24393
+ return result;
24281
24394
  }
24282
24395
  // Unlisted compatibility aliases for clients that cached the pre-3.0 tool surface.
24283
24396
  async search(query) {
@@ -24293,6 +24406,7 @@ var CursorBridge = class {
24293
24406
  const text = String(prompt || "").trim();
24294
24407
  if (!text) throw new Error("prompt must not be empty");
24295
24408
  if (text.length > 1e5) throw new Error("prompt exceeds the 100,000-character limit");
24409
+ this._assertWorkspaceConfirmed();
24296
24410
  if (this._hasGlobalReservation()) {
24297
24411
  throw new Error("A global Cursor reservation has an unconfirmed Stop state; no new task may be submitted until it is explicitly recovered or released");
24298
24412
  }
@@ -24428,6 +24542,7 @@ var CursorBridge = class {
24428
24542
  return `cursor-${Date.now().toString(36)}-${this.nextTaskId++}`;
24429
24543
  }
24430
24544
  _enqueue(kind, prompt, options) {
24545
+ this._ensureTaskCapacity();
24431
24546
  const id = options.taskId || this._nextTaskId();
24432
24547
  let resolvePromise;
24433
24548
  let rejectPromise;
@@ -24471,6 +24586,8 @@ var CursorBridge = class {
24471
24586
  targetUiFlavor: null,
24472
24587
  fallbackReason: null,
24473
24588
  result: null,
24589
+ resultCollectedAt: null,
24590
+ waitDeadlineAt: null,
24474
24591
  error: null,
24475
24592
  cancelRequested: false,
24476
24593
  cancelReason: null,
@@ -24973,7 +25090,7 @@ var CursorBridge = class {
24973
25090
  last = this._inspectModelPickerRows(await this._readModelPickerRows(c), requested, kind);
24974
25091
  if (last.row) return { ...last, state: "matched" };
24975
25092
  const signature = JSON.stringify(last.available);
24976
- if (last.available.length > 0) {
25093
+ if (modelPickerAvailableIsDecisive(kind, last.available)) {
24977
25094
  if (signature !== stableSignature) {
24978
25095
  stableSignature = signature;
24979
25096
  stableSince = Date.now();
@@ -25613,6 +25730,7 @@ var CursorBridge = class {
25613
25730
  await this._applyModelPreference(c, job.modelPreference, job);
25614
25731
  this._throwIfCancelledBeforeSend(job);
25615
25732
  job.responseBaseline = await this._captureSessionResponseBaseline(c);
25733
+ this._persistSessionResponseBaseline(job);
25616
25734
  const providerErrorBaseline = providerErrorSignature(await this._readProviderError(c));
25617
25735
  const filled = await this._fillPrompt(c, job.prompt, job);
25618
25736
  if (filled === "NO_INPUT") await this._throwChatPanelUnavailableAfterNoInput(c);
@@ -25687,8 +25805,16 @@ var CursorBridge = class {
25687
25805
  job.monitorPromise = monitorPromise;
25688
25806
  return true;
25689
25807
  }
25808
+ _taskWaitDeadline(job, timeoutMs = job?.timeoutMs) {
25809
+ if (!job) return Date.now() + timeoutMs;
25810
+ if (!Number.isFinite(job.waitDeadlineAt)) {
25811
+ const sentAt = Date.parse(job.sentAt || "");
25812
+ job.waitDeadlineAt = (Number.isFinite(sentAt) ? sentAt : Date.now()) + timeoutMs;
25813
+ }
25814
+ return job.waitDeadlineAt;
25815
+ }
25690
25816
  async _monitorParallelAgent(job, generation) {
25691
- const started = Date.now();
25817
+ const deadline = this._taskWaitDeadline(job);
25692
25818
  let sawGenerating = false;
25693
25819
  let completedStable = 0;
25694
25820
  let missingPolls = 0;
@@ -25697,7 +25823,7 @@ var CursorBridge = class {
25697
25823
  let lastTerminalErrorSignature = "";
25698
25824
  let collectionAttempts = 0;
25699
25825
  let lastCollectionError = "";
25700
- while (Date.now() - started < job.timeoutMs) {
25826
+ while (Date.now() < deadline) {
25701
25827
  if (!this._monitorOwns(job, generation)) return;
25702
25828
  await sleep2(1400);
25703
25829
  if (!this._monitorOwns(job, generation)) return;
@@ -25815,51 +25941,63 @@ var CursorBridge = class {
25815
25941
  }
25816
25942
  throw new Error(`After opening ${agentId}, it could not be confirmed as the selected Agent`);
25817
25943
  }
25944
+ async _withRestoredAgentSelection(c, job, operation) {
25945
+ const entries = await this._readAgentEntries(c, true);
25946
+ const previousSelectedId = (entries.find((entry) => entry.isSelected) || {}).id || null;
25947
+ try {
25948
+ return await operation();
25949
+ } finally {
25950
+ if (previousSelectedId && previousSelectedId !== job.agentId) {
25951
+ try {
25952
+ const opened = await this._requestExactAgentSelection(c, previousSelectedId);
25953
+ if (opened !== "OPENED") throw new Error(`Unable to restore ${previousSelectedId}: ${opened}`);
25954
+ await this._waitForSelectedAgent(c, previousSelectedId);
25955
+ } catch (error2) {
25956
+ job.uiDiagnostic = { ...job.uiDiagnostic, selectionRestore: { state: "failed", agentId: previousSelectedId, detail: String(error2.message).slice(0, 500) } };
25957
+ }
25958
+ }
25959
+ }
25960
+ }
25818
25961
  async _collectParallelAgent(job) {
25819
25962
  const page = await findPage({ targetId: job.targetId, purpose: "parallel_agent" });
25820
25963
  const c = makeClient(page.webSocketDebuggerUrl);
25821
25964
  await c.ready;
25822
- let previousSelectedId = null;
25823
25965
  try {
25824
- const entries = await this._readAgentEntries(c, true);
25825
- previousSelectedId = (entries.find((e) => e.isSelected) || {}).id || null;
25826
- const opened = await evalJS(c, exprOpenAgent(job.agentId));
25827
- if (opened !== "OPENED") throw new Error(`Unable to open ${job.agentId}: ${opened}`);
25828
- await this._closeHistory(c);
25829
- await this._waitForSelectedAgent(c, job.agentId);
25830
- let answer = "";
25831
- let lastKey = "";
25832
- let stable = 0;
25833
- for (let i = 0; i < 80; i++) {
25834
- const snap = await this._readResponseSnapshot(c);
25835
- const candidate = String(await evalJS(c, EXPR_EXTRACT) || "").trim();
25836
- const hasNewTurn = isSessionTurnReplyReady(job.responseBaseline, snap);
25837
- if (candidate && hasNewTurn && Number(snap.stop || 0) === 0) {
25838
- const key = `${snap.replyLength}:${snap.replyHash}`;
25839
- if (key === lastKey) stable++;
25840
- else {
25841
- lastKey = key;
25842
- stable = 0;
25843
- }
25844
- if (stable >= 1) {
25845
- answer = candidate;
25846
- break;
25966
+ return await this._withRestoredAgentSelection(c, job, async () => {
25967
+ const opened = await evalJS(c, exprOpenAgent(job.agentId));
25968
+ if (opened !== "OPENED") throw new Error(`Unable to open ${job.agentId}: ${opened}`);
25969
+ await this._closeHistory(c);
25970
+ await this._waitForSelectedAgent(c, job.agentId);
25971
+ let answer = "";
25972
+ let lastKey = "";
25973
+ let stable = 0;
25974
+ for (let i = 0; i < 80; i++) {
25975
+ const snap = await this._readResponseSnapshot(c);
25976
+ const candidate = String(await evalJS(c, EXPR_EXTRACT) || "").trim();
25977
+ const hasNewTurn = isSessionTurnReplyReady(job.responseBaseline, snap);
25978
+ if (candidate && hasNewTurn && Number(snap.stop || 0) === 0) {
25979
+ const key = `${snap.replyLength}:${snap.replyHash}`;
25980
+ if (key === lastKey) stable++;
25981
+ else {
25982
+ lastKey = key;
25983
+ stable = 0;
25984
+ }
25985
+ if (stable >= 1) {
25986
+ answer = candidate;
25987
+ break;
25988
+ }
25847
25989
  }
25990
+ await sleep2(300);
25848
25991
  }
25849
- await sleep2(300);
25850
- }
25851
- if (!answer) throw new Error(`${job.agentId} was opened, but no final assistant reply was found`);
25852
- if (previousSelectedId && previousSelectedId !== job.agentId) {
25853
- if (await this._ensureHistoryOpen(c)) {
25854
- await evalJS(c, exprOpenAgent(previousSelectedId));
25855
- await this._closeHistory(c);
25856
- await this._waitForSelectedAgent(c, previousSelectedId);
25857
- }
25858
- }
25859
- return answer;
25992
+ if (!answer) throw new Error(`${job.agentId} was opened, but no final assistant reply was found`);
25993
+ return answer;
25994
+ });
25860
25995
  } finally {
25861
- await this._closeHistory(c);
25862
- c.close();
25996
+ try {
25997
+ await this._closeHistory(c);
25998
+ } finally {
25999
+ c.close();
26000
+ }
25863
26001
  }
25864
26002
  }
25865
26003
  _reapWithoutResult(job, error2, evidence) {
@@ -25874,7 +26012,6 @@ var CursorBridge = class {
25874
26012
  job.terminalEvidence = evidence || "stable_completed_history_icon";
25875
26013
  job.recoveryState = "terminal_result_uncollected";
25876
26014
  job.reservationScope = uncertainSubmissionReservationScope(job, error2);
25877
- job.recoveryState = error2.recoveryState || "monitoring_uncertain_submission";
25878
26015
  this._safeSettleSessionJob(job, "terminal_result_uncollected", { needsAttention: true });
25879
26016
  }
25880
26017
  _abandonJob(job, reason) {
@@ -25951,6 +26088,7 @@ var CursorBridge = class {
25951
26088
  job.phase = "running";
25952
26089
  job.error = null;
25953
26090
  job.recoveryState = "monitoring";
26091
+ if (options.reattach !== false && !job.cancelRequested) job.waitDeadlineAt = Date.now() + job.timeoutMs;
25954
26092
  const attached = options.reattach !== false && !job.cancelRequested && this._startParallelMonitor(job);
25955
26093
  return { changed: attached, state: "running", monitorReattached: attached, task: this._taskView(job, true) };
25956
26094
  }
@@ -26349,12 +26487,12 @@ var CursorBridge = class {
26349
26487
  }).catch((e) => console.error("\u26A0\uFE0F Failed to restore the original Cursor Agent: " + e.message));
26350
26488
  }
26351
26489
  async _waitComplete(c, timeoutMs = QUERY_TIMEOUT, baselineCount = 0, job = null, providerErrorBaseline = "") {
26352
- const start = Date.now();
26490
+ const deadline = this._taskWaitDeadline(job, timeoutMs);
26353
26491
  const INTERVAL = 1e3;
26354
26492
  let sawStop = false;
26355
26493
  let lastReplyKey = "", stableReply = 0;
26356
26494
  await sleep2(1200);
26357
- while (Date.now() - start < timeoutMs) {
26495
+ while (Date.now() < deadline) {
26358
26496
  await this._throwIfNewProviderError(c, providerErrorBaseline);
26359
26497
  if (job && job.cancelRequested) {
26360
26498
  if (job.agentId) {
@@ -26419,6 +26557,7 @@ var CursorBridge = class {
26419
26557
  throw new Error(`Cursor task timed out (${timeoutMs}ms) before generation was confirmed stopped with a complete assistant reply${taskHint}`);
26420
26558
  }
26421
26559
  _taskView(job, includeResult = false) {
26560
+ if (includeResult) this._markTaskResultCollected(job);
26422
26561
  const view = {
26423
26562
  taskId: job.id,
26424
26563
  kind: job.kind,
@@ -26457,6 +26596,8 @@ var CursorBridge = class {
26457
26596
  monitorAttached: job.monitorAttached,
26458
26597
  monitorGeneration: job.monitorGeneration,
26459
26598
  resultUnavailable: job.resultUnavailable,
26599
+ resultCollectedAt: job.resultCollectedAt || null,
26600
+ waitDeadlineAt: Number.isFinite(job.waitDeadlineAt) ? new Date(job.waitDeadlineAt).toISOString() : null,
26460
26601
  terminalEvidence: job.terminalEvidence,
26461
26602
  providerError: job.providerError,
26462
26603
  uiDiagnostic: job.uiDiagnostic,
@@ -26466,7 +26607,7 @@ var CursorBridge = class {
26466
26607
  blocksFifo: this.activeParallel.has(job.id) && !isTerminalTask(job),
26467
26608
  blocksAll: this.activeParallel.has(job.id) && !isTerminalTask(job) && job.reservationScope === "global"
26468
26609
  };
26469
- if (includeResult || isTerminalTask(job)) view.result = job.result;
26610
+ if (includeResult) view.result = job.result;
26470
26611
  if (job.phase === "orphaned") {
26471
26612
  if (job.recoveryState === "terminal_result_uncollected") {
26472
26613
  view.attention = "Agent History proves that the underlying task ended, but its final reply has not been collected. Retry with reap, or explicitly abandon only if a missing reply is acceptable.";
@@ -26476,11 +26617,22 @@ var CursorBridge = class {
26476
26617
  }
26477
26618
  return view;
26478
26619
  }
26479
- _trimTasks() {
26480
- if (this.tasks.size <= 50) return;
26620
+ _assertWorkspaceConfirmed() {
26621
+ if (this.workspaceConfirmationRequired) {
26622
+ throw new Error("WORKSPACE_CONFIRMATION_REQUIRED: the saved default binding has no host workspace identity. Run cursor_init with the intended project before submitting work.");
26623
+ }
26624
+ }
26625
+ _ensureTaskCapacity() {
26626
+ this._trimTasks(49);
26627
+ if (this.tasks.size >= 50) {
26628
+ throw new Error("TASK_RETENTION_FULL: 50 tasks are active or have unread replies. Read unreadResultTaskIds with cursor_status(task_id), or wait for active tasks before submitting more work.");
26629
+ }
26630
+ }
26631
+ _trimTasks(limit = 50) {
26632
+ if (this.tasks.size <= limit) return;
26481
26633
  for (const [id, job] of this.tasks) {
26482
- if (isTerminalTask(job)) this.tasks.delete(id);
26483
- if (this.tasks.size <= 50) break;
26634
+ if (isTerminalTask(job) && (job.result == null || job.resultCollectedAt)) this.tasks.delete(id);
26635
+ if (this.tasks.size <= limit) break;
26484
26636
  }
26485
26637
  }
26486
26638
  async status(taskId = "") {
@@ -26510,6 +26662,8 @@ var CursorBridge = class {
26510
26662
  blockedQueuedCount: this.activeParallel.size > 0 ? this.queue.filter((job) => globalBlocked || job.effectiveExecution !== "parallel_agent").length : 0,
26511
26663
  activeParallel: [...this.activeParallel.values()].map((job) => this._taskView(job)),
26512
26664
  recentTasks: [...this.tasks.values()].slice(-10).map((job) => this._taskView(job)),
26665
+ unreadResultTaskIds: [...this.tasks.values()].filter((job) => isTerminalTask(job) && job.result != null && !job.resultCollectedAt).map((job) => job.id),
26666
+ taskRetentionLimit: 50,
26513
26667
  cdpPort: CDP_PORT2,
26514
26668
  lifecycle: this._lastLifecycle || {
26515
26669
  adapterPid: process.pid,
@@ -26575,7 +26729,7 @@ function buildToolDefinitions(bridgeInstance) {
26575
26729
  background: { type: "boolean", default: true, description: "When true, return the task ID immediately. When false, wait for the task to finish or need attention." },
26576
26730
  execution: { type: "string", enum: ["fifo", "parallel_agent"], default: "fifo", description: "fifo is the first-in, first-out serial queue and runs one task at a time in a clean chat. parallel_agent creates a separate top-level Cursor Agent." },
26577
26731
  read_only: { type: "boolean", default: false, description: "Set true when Cursor must not change the workspace." },
26578
- timeout_ms: { type: "integer", minimum: 3e4, maximum: 9e5, default: 6e5, description: "How long to wait before timing out, in milliseconds. The default is 10 minutes." },
26732
+ timeout_ms: { type: "integer", minimum: 3e4, maximum: 9e5, default: 6e5, description: "One monitoring budget after submission, shared by FIFO and automatic Agent recovery. Expiry needs attention; it does not cancel work. Explicit reap may start a new budget. The default is 10 minutes." },
26579
26733
  allowed_paths: { type: "array", items: { type: "string" }, description: "Workspace-relative paths Cursor may write. Parallel write tasks require non-overlapping paths. This declaration is not a filesystem sandbox." },
26580
26734
  completion_contract: { type: "string", description: "Optional acceptance checks or a required final-report format." },
26581
26735
  session_mode: { type: "string", enum: [...CURSOR_SESSION_MODES], default: "isolated", description: "isolated preserves the current clean-task behavior. create starts an update-safe persistent session. continue sends one new turn to the exact session_id." },
@@ -26603,12 +26757,12 @@ function buildToolDefinitions(bridgeInstance) {
26603
26757
  },
26604
26758
  {
26605
26759
  name: "cursor_session_control",
26606
- description: "Recover, close, or forget one exact persistent cursor_do session. reconcile checks the exact Agent twice and never resends. close prevents future sends but does not stop or delete the Cursor Agent. An active or uncertain session cannot be closed. abandon is the explicit last resort when exact stop evidence is unavailable. forget requires confirm=true and is allowed only after close; it deletes only the Bridge mapping.",
26760
+ description: "Recover, collect a reply, close, or forget one exact persistent cursor_do session. reconcile checks the exact Agent twice and never resends. collect_result reads the completed reconciled turn from that Agent without sending a prompt; it may temporarily select that Agent. close prevents future sends but does not stop or delete the Cursor Agent. An active or uncertain session cannot be closed. abandon is the explicit last resort when exact stop evidence is unavailable. forget requires confirm=true and is allowed only after close; it deletes only the Bridge mapping.",
26607
26761
  inputSchema: {
26608
26762
  type: "object",
26609
26763
  properties: {
26610
26764
  session_id: { type: "string", description: "The exact session ID returned by cursor_do(session_mode=create)." },
26611
- action: { type: "string", enum: ["reconcile", "close", "forget", "abandon"], description: "reconcile reads the exact Agent state; close ends safe continuity; forget removes a closed mapping; abandon releases an uncertain mapping without stop proof." },
26765
+ action: { type: "string", enum: ["reconcile", "collect_result", "close", "forget", "abandon"], description: "reconcile reads the exact Agent state; collect_result retrieves its completed latest turn before continuing; close ends safe continuity; forget removes a closed mapping; abandon releases an uncertain mapping without stop proof." },
26612
26766
  confirm: { type: "boolean", default: false, description: "Required for forget and abandon." },
26613
26767
  reason: { type: "string", description: "Required and non-empty for abandon." },
26614
26768
  acknowledge_may_still_write: { type: "boolean", default: false, description: "Required for abandon; acknowledges that the underlying Cursor Agent may still run or write." }
@@ -26840,6 +26994,7 @@ export {
26840
26994
  buildContextEnginePrompt,
26841
26995
  buildToolDefinitions,
26842
26996
  classifyChatPanelDiagnostic,
26997
+ classifyModelPickerRowKind,
26843
26998
  classifyParallelTerminalIcon,
26844
26999
  createChatPanelUnavailableError,
26845
27000
  createProviderError,
@@ -26850,10 +27005,12 @@ export {
26850
27005
  exprInspectWorkspaceRepository,
26851
27006
  exprOpenAgent,
26852
27007
  isConfirmedCompletedReply,
27008
+ isCursorEffortOptionText,
26853
27009
  isDurablyRegisteredParallelEntry,
26854
27010
  isSessionTurnReplyReady,
26855
27011
  isTargetedStopConfirmed,
26856
27012
  lifecycleFailureSummary,
27013
+ modelPickerAvailableIsDecisive,
26857
27014
  normalizeAllowedPath,
26858
27015
  normalizeCceSearchResult,
26859
27016
  normalizeCursorModelEffort,
@@ -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.1.12",
13
+ packageVersion: "0.1.14",
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.1.12",
3
+ "version": "0.1.14",
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": "5.8.2"
50
+ "embeddedProductVersion": "5.9.1"
51
51
  }
52
52
  }