pi-cursor-bridge 0.1.16 → 0.2.0

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.10.0+codex.20260908042457",
3
+ "version": "6.0.0+codex.20260908194916",
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.10.0, including explicit request provenance for CCE and delegated tasks. 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.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.
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)
@@ -22344,6 +22344,7 @@ var StdioServerTransport = class {
22344
22344
 
22345
22345
  // server.mjs
22346
22346
  import { basename as basename6, dirname as dirname6, join as join8, resolve as resolve7 } from "node:path";
22347
+ import { statSync as statSync3 } from "node:fs";
22347
22348
 
22348
22349
  // node_modules/ws/wrapper.mjs
22349
22350
  var import_stream = __toESM(require_stream(), 1);
@@ -22604,7 +22605,7 @@ function updateCursorSessionRegistry(filePath, mutator, options = {}) {
22604
22605
  // server.mjs
22605
22606
  init_cursor_ensure_core();
22606
22607
  init_lifecycle_paths();
22607
- var PLUGIN_VERSION = "5.10.0";
22608
+ var PLUGIN_VERSION = "6.0.0";
22608
22609
  var CDP_PORT2 = Number(process.env.CURSOR_BRIDGE_CDP_PORT || 9223);
22609
22610
  var ORIGIN = `http://localhost:${CDP_PORT2}`;
22610
22611
  var QUERY_TIMEOUT = Number(process.env.CURSOR_BRIDGE_TIMEOUT || 3e5);
@@ -23344,6 +23345,60 @@ function exprInspectWorkspaceRepository(projectPath) {
23344
23345
  return JSON.stringify(inspectWorkspace(${JSON.stringify(String(projectPath || ""))}).diagnostic);
23345
23346
  })()`;
23346
23347
  }
23348
+ function exprRegisterAgentsWorkspace(projectPath, workspaceFile = false) {
23349
+ return `(async function(){
23350
+ ${WORKSPACE_SECTION_BODY}
23351
+ const path=${JSON.stringify(String(projectPath || ""))};
23352
+ const before=inspectWorkspace(path).diagnostic;
23353
+ if(before.state!=='workspace_registration_required')return JSON.stringify(before);
23354
+ const fail=(state,error)=>JSON.stringify({...before,state,error});
23355
+ const services=new Set();
23356
+ for(const node of document.querySelectorAll('button[aria-label="Open Workspace"],.ui-sidebar-section-head')){
23357
+ const key=Object.keys(node).find(k=>k.startsWith('__reactFiber$'));
23358
+ for(let f=key&&node[key],i=0;f&&i<48;i++,f=f.return){
23359
+ for(let d=f.dependencies&&f.dependencies.firstContext;d;d=d.next){
23360
+ const service=d.memoizedValue&&d.memoizedValue.workspace&&d.memoizedValue.workspace.instantiationService;
23361
+ if(service)services.add(service);
23362
+ }
23363
+ }
23364
+ }
23365
+ const candidates=new Map();
23366
+ for(const service of services){
23367
+ const entries=new Map();
23368
+ for(let s=service,i=0;s&&i<8;i++,s=s._parent){
23369
+ for(const [id,value] of s._services&&s._services._entries||[]){
23370
+ if(!entries.has(String(id)))entries.set(String(id),value);
23371
+ }
23372
+ }
23373
+ const projects=entries.get('glassWorkspacesService');
23374
+ const workspaces=entries.get('workspacesService');
23375
+ const URI=entries.get('environmentService')?.userHome?.constructor;
23376
+ if(typeof projects?.replaceWorkspaceProject==='function'&&typeof URI?.file==='function'
23377
+ &&typeof workspaces?.getSingleFolderWorkspaceIdentifier==='function'
23378
+ &&typeof workspaces?.getWorkspaceIdentifier==='function'
23379
+ &&typeof projects?.refresh==='function'){
23380
+ candidates.set(projects,{projects,workspaces,URI});
23381
+ }
23382
+ }
23383
+ if(candidates.size!==1)return fail('workspace_registration_unavailable','Expected one Cursor workspace registration service');
23384
+ try{
23385
+ const {projects,workspaces,URI}=[...candidates.values()][0];
23386
+ const uri=URI.file(path);
23387
+ const identifier=await workspaces[${JSON.stringify(workspaceFile ? "getWorkspaceIdentifier" : "getSingleFolderWorkspaceIdentifier")}](uri);
23388
+ if(!identifier?.id||localWorkspacePath(identifier)!==normalizeWorkspacePath(path)){
23389
+ return fail('workspace_registration_identity_mismatch','Cursor returned a different workspace identity');
23390
+ }
23391
+ const current=inspectWorkspace(path).diagnostic;
23392
+ if(current.state!=='workspace_registration_required')return JSON.stringify(current);
23393
+ // addProject can skip folders already represented by Agent history.
23394
+ // With no 'replaces', this public service method upserts only this exact ID.
23395
+ await projects.replaceWorkspaceProject({project:{type:'workspace',workspaceIdentifier:identifier}});
23396
+ await projects.refresh();
23397
+ return JSON.stringify({ok:false,state:'workspace_registration_requested',wanted:before.wanted,
23398
+ workspaceId:identifier.id,registrationSource:'cursor_glass_workspaces_service'});
23399
+ }catch(error){return fail('workspace_registration_failed',String(error.message||error));}
23400
+ })()`;
23401
+ }
23347
23402
  function exprInspectAgentWorkspace(projectPath, options = {}) {
23348
23403
  return `(function(){
23349
23404
  ${WORKSPACE_SECTION_BODY}
@@ -23974,7 +24029,7 @@ var CursorBridge = class {
23974
24029
  throw cursorSessionError("SESSION_NOT_READY", `state=${session.state}; recovery=${session.recoveryState || "none"}`);
23975
24030
  }
23976
24031
  if (session.lastTask?.status === "completed" && !session.lastTask.resultCollectedAt && this.tasks.has(session.lastTask.taskId)) {
23977
- throw cursorSessionError("SESSION_RESULT_UNCOLLECTED", `read cursor_status(task_id=${session.lastTask.taskId}) before continuing`);
24032
+ throw cursorSessionError("SESSION_RESULT_UNCOLLECTED", `read cursor_status(task_id=${session.lastTask.taskId}, detail="full") before continuing`);
23978
24033
  }
23979
24034
  if (session.lastTask?.status === "completed" && !session.lastTask.resultCollectedAt && !this.tasks.has(session.lastTask.taskId) && session.recoveryState !== "reconciled_result_uncollected") {
23980
24035
  throw cursorSessionError("SESSION_RECONCILE_REQUIRED", "the prior reply was not read before this adapter restarted; reconcile the exact Agent before continuing");
@@ -24291,6 +24346,8 @@ var CursorBridge = class {
24291
24346
  return result;
24292
24347
  }
24293
24348
  async initializeWorkspace(projectPath) {
24349
+ if (this._healing) await this._healing.catch(() => {
24350
+ });
24294
24351
  if (this.busy || this.activeParallel.size > 0 || this.queue.length > 0) {
24295
24352
  throw new Error("cursor_init cannot change workspace while Cursor tasks are queued or running");
24296
24353
  }
@@ -24303,7 +24360,7 @@ var CursorBridge = class {
24303
24360
  this._lastLifecycle = null;
24304
24361
  this._lastWorkspaceBinding = null;
24305
24362
  try {
24306
- await this._ensureCursor();
24363
+ await this._ensureCursor({ registerWorkspace: true });
24307
24364
  } catch (error2) {
24308
24365
  const lifecycle = this._lastLifecycle;
24309
24366
  const recoverableStatuses = /* @__PURE__ */ new Set([
@@ -24339,7 +24396,7 @@ var CursorBridge = class {
24339
24396
  lifecycle: this._lastLifecycle
24340
24397
  };
24341
24398
  }
24342
- async _findAgentsWorkspace(projectPath) {
24399
+ async _findAgentsWorkspace(projectPath, { registerWorkspace = false } = {}) {
24343
24400
  if (!projectPath) return null;
24344
24401
  this._lastWorkspaceBinding = null;
24345
24402
  let page;
@@ -24352,7 +24409,24 @@ var CursorBridge = class {
24352
24409
  const c = makeClient(page.webSocketDebuggerUrl);
24353
24410
  try {
24354
24411
  await c.ready;
24355
- const repository = JSON.parse(await evalJS(c, exprInspectWorkspaceRepository(projectPath)) || "{}");
24412
+ let repository = JSON.parse(await evalJS(c, exprInspectWorkspaceRepository(projectPath)) || "{}");
24413
+ if (registerWorkspace && repository.state === "workspace_registration_required") {
24414
+ const registration = await this._withUiLock(async () => {
24415
+ if (this.busy || this.activeParallel.size > 0 || this.queue.length > 0) {
24416
+ return { ok: false, state: "workspace_registration_busy", error: "Cursor tasks became queued or running before registration" };
24417
+ }
24418
+ return JSON.parse(await evalJS(c, exprRegisterAgentsWorkspace(projectPath, statSync3(projectPath).isFile())) || "{}");
24419
+ });
24420
+ repository = registration;
24421
+ if (registration.state === "workspace_registration_requested") {
24422
+ for (let attempt = 0; attempt < 20; attempt++) {
24423
+ repository = JSON.parse(await evalJS(c, exprInspectWorkspaceRepository(projectPath)) || "{}");
24424
+ if (repository.state !== "workspace_registration_required") break;
24425
+ await sleep2(250);
24426
+ }
24427
+ repository = { ...repository, registration };
24428
+ }
24429
+ }
24356
24430
  this._lastWorkspaceBinding = { ...repository, checkedAt: (/* @__PURE__ */ new Date()).toISOString() };
24357
24431
  if (!repository.ok) return null;
24358
24432
  return {
@@ -24556,7 +24630,9 @@ var CursorBridge = class {
24556
24630
  throw cursorSessionError("SESSION_EXECUTION_INVALID", "persistent sessions require execution=parallel_agent and never fall back to FIFO");
24557
24631
  }
24558
24632
  const readOnly = options.readOnly === true;
24559
- const timeoutMs = Math.max(3e4, Math.min(9e5, Number(options.timeoutMs || 6e5)));
24633
+ const requestedTimeoutMs = Number(options.timeoutMs ?? 6e5);
24634
+ if (!Number.isFinite(requestedTimeoutMs)) throw new Error("timeout_ms must be finite");
24635
+ const timeoutMs = Math.max(3e4, Math.min(9e5, requestedTimeoutMs));
24560
24636
  const allowedPaths = Array.isArray(options.allowedPaths) ? options.allowedPaths.map((x) => String(x).trim()).filter(Boolean) : [];
24561
24637
  if (readOnly && allowedPaths.length > 0) {
24562
24638
  throw new Error("read_only=true cannot be combined with allowed_paths because a read-only task cannot declare a write scope");
@@ -24612,13 +24688,14 @@ var CursorBridge = class {
24612
24688
  modelPreference = session.modelPreference || null;
24613
24689
  if (duplicate) {
24614
24690
  const existing = this.tasks.get(session.activeTaskId || session.lastTask && session.lastTask.taskId || "");
24615
- return existing ? { duplicate: true, ...this._taskView(existing, true) } : { duplicate: true, ...this._sessionView(session) };
24691
+ return existing ? { duplicate: true, ...options.background === false ? this._collectedTaskView(existing) : this._compactTaskView(existing) } : { duplicate: true, ...this._sessionView(session) };
24616
24692
  }
24617
24693
  }
24618
24694
  const job = this._enqueue("do", fullPrompt, {
24619
24695
  requestContext,
24620
24696
  taskId,
24621
24697
  timeoutMs,
24698
+ requestedTimeoutMs,
24622
24699
  newChat: sessionMode !== "continue",
24623
24700
  execution,
24624
24701
  readOnly,
@@ -24635,9 +24712,9 @@ var CursorBridge = class {
24635
24712
  agentId: sessionMode === "continue" ? session && session.agentId : null,
24636
24713
  agentLabel: sessionMode === "continue" ? session && session.agentLabel : null
24637
24714
  });
24638
- if (options.background !== false) return this._taskView(job);
24715
+ if (options.background !== false) return this._compactTaskView(job);
24639
24716
  await job.promise;
24640
- return this._taskView(job, true);
24717
+ return this._collectedTaskView(job);
24641
24718
  }
24642
24719
  _assertNoParallelPathConflict(allowedPaths) {
24643
24720
  if (this._hasGlobalReservation()) {
@@ -24682,6 +24759,7 @@ var CursorBridge = class {
24682
24759
  prompt,
24683
24760
  requestContext: options.requestContext || normalizeRequestContext(),
24684
24761
  timeoutMs: options.timeoutMs,
24762
+ requestedTimeoutMs: options.requestedTimeoutMs ?? options.timeoutMs,
24685
24763
  newChat: options.newChat,
24686
24764
  preferLegacyUi: options.preferLegacyUi === true,
24687
24765
  execution: options.execution || "fifo",
@@ -24847,7 +24925,7 @@ var CursorBridge = class {
24847
24925
  // 自愈:每次查询前委托 ensureCursorRunning。并发去重。失败静默降级(_run 报清晰错)。
24848
24926
  // 统一走 ensureCursorRunning 复用其【单一身份校验来源】(cdpUp + cdpIsCursor)——避免热路径裸 /json/version 检查
24849
24927
  // 绕过身份校验、在别的 IDE 占 9223 时驱动错应用(2026-06-08 review #6)。
24850
- _ensureCursor() {
24928
+ _ensureCursor({ registerWorkspace = false } = {}) {
24851
24929
  this._refreshPersistedRuntimeMode();
24852
24930
  if (this._healing) return this._healing;
24853
24931
  this._healing = (async () => {
@@ -24860,17 +24938,11 @@ var CursorBridge = class {
24860
24938
  ...this.projectPath ? { projectPath: this.projectPath } : {}
24861
24939
  });
24862
24940
  this._lastLifecycle = lifecycleFromEnsureResult(rr, this.runtimeMode);
24863
- if (!rr.ok && rr.status === "workspace-not-ready" && rr.projectPath) {
24864
- const agentsWorkspace = await this._findAgentsWorkspace(rr.projectPath);
24865
- if (agentsWorkspace) {
24866
- this._lastLifecycle = promoteAgentsWorkspaceLifecycle(this._lastLifecycle, agentsWorkspace);
24867
- }
24868
- }
24869
- if (rr.ok && rr.workspaceAction === "reused-agents-window" && rr.projectPath) {
24870
- const agentsWorkspace = await this._findAgentsWorkspace(rr.projectPath);
24941
+ if ((rr.ok || rr.status === "workspace-not-ready") && rr.projectPath) {
24942
+ const agentsWorkspace = await this._findAgentsWorkspace(rr.projectPath, { registerWorkspace });
24871
24943
  if (agentsWorkspace) {
24872
24944
  this._lastLifecycle = promoteAgentsWorkspaceLifecycle(this._lastLifecycle, agentsWorkspace);
24873
- } else {
24945
+ } else if (rr.ok && (rr.workspaceAction === "reused-agents-window" || this._lastWorkspaceBinding)) {
24874
24946
  this._lastLifecycle = {
24875
24947
  ...this._lastLifecycle,
24876
24948
  status: "workspace-not-ready",
@@ -25011,10 +25083,11 @@ var CursorBridge = class {
25011
25083
  this.activeParallel.delete(job.id);
25012
25084
  this._failJob(job, error2);
25013
25085
  } else if (error2 && error2.sent) {
25086
+ job.firstWaitError ||= error2.message;
25014
25087
  job.error = `Submission state is uncertain; monitoring continues by agentId: ${error2.message}`;
25015
25088
  if (job.agentId) {
25016
25089
  job.phase = "running";
25017
- job.reservationScope = job.readOnly ? "agent" : "paths";
25090
+ job.reservationScope = job.effectiveExecution === "fifo" ? "global" : job.readOnly ? "agent" : "paths";
25018
25091
  this.activeParallel.set(job.id, job);
25019
25092
  this._startParallelMonitor(job);
25020
25093
  } else {
@@ -25083,6 +25156,7 @@ var CursorBridge = class {
25083
25156
  baseline = JSON.parse(await evalJS(c, EXPR_SNAP));
25084
25157
  } catch {
25085
25158
  }
25159
+ options.responseBaseline = baseline;
25086
25160
  const providerErrorBaseline = providerErrorSignature(await this._readProviderError(c));
25087
25161
  await this._verifyAgentsWorkspace(c, options, "before_send");
25088
25162
  options.sendState = "dispatching";
@@ -25671,6 +25745,7 @@ var CursorBridge = class {
25671
25745
  if (!diagnostic.ok) throw createWorkspaceBindingError(diagnostic);
25672
25746
  }
25673
25747
  async _confirmSubmission(c, baselineCount = 0, providerErrorBaseline = "", job = null) {
25748
+ let lastSnapshot = {};
25674
25749
  const accepted = async () => {
25675
25750
  await this._throwIfNewProviderError(c, providerErrorBaseline);
25676
25751
  let snap = {};
@@ -25678,6 +25753,7 @@ var CursorBridge = class {
25678
25753
  snap = JSON.parse(await evalJS(c, EXPR_SNAP));
25679
25754
  } catch {
25680
25755
  }
25756
+ lastSnapshot = snap;
25681
25757
  const inputTextLength = Number(snap.inputTextLength);
25682
25758
  return Number(snap.stop || 0) > 0 || Number(snap.messageCount || 0) > Number(baselineCount || 0) || Number.isFinite(inputTextLength) && inputTextLength === 0;
25683
25759
  };
@@ -25693,14 +25769,22 @@ var CursorBridge = class {
25693
25769
  throw error3;
25694
25770
  }
25695
25771
  const clicked = await evalJS(c, EXPR_CLICK_SEND);
25696
- if (clicked === "CLICKED") {
25772
+ if (clicked === "CLICKED" || clicked === "NO_SEND") {
25697
25773
  for (let i = 0; i < 20; i++) {
25698
25774
  await sleep2(250);
25699
- if (await accepted()) return "button";
25775
+ if (await accepted()) return clicked === "CLICKED" ? "button" : "enter";
25700
25776
  }
25701
25777
  }
25702
- const error2 = new Error(`Cursor did not accept the submission (submit_not_accepted: ${clicked || "unknown"}); the prompt remains in the input and no orphan task was created`);
25703
- error2.confirmedNotSent = true;
25778
+ const error2 = new Error(`Cursor submission could not be confirmed (submission_uncertain: ${clicked || "unknown"}); Enter was dispatched, so inspect the bound Agent before retrying`);
25779
+ error2.confirmedNotSent = false;
25780
+ error2.sent = true;
25781
+ error2.composerDiagnostic = {
25782
+ inputTextLength: Number.isFinite(Number(lastSnapshot.inputTextLength)) ? Number(lastSnapshot.inputTextLength) : null,
25783
+ sendReady: lastSnapshot.sendReady === true,
25784
+ stop: Number(lastSnapshot.stop || 0),
25785
+ messageCount: Number(lastSnapshot.messageCount || 0),
25786
+ fallbackResult: clicked || "unknown"
25787
+ };
25704
25788
  throw error2;
25705
25789
  }
25706
25790
  async _readProviderError(c) {
@@ -26072,7 +26156,12 @@ var CursorBridge = class {
26072
26156
  }
26073
26157
  if (!this._monitorOwns(job, generation)) return;
26074
26158
  const detail = lastCollectionError ? `; last collection error: ${lastCollectionError}` : "";
26075
- throw new Error(`Cursor parallel_agent task timed out (${job.timeoutMs}ms)${detail}`);
26159
+ const recovered = await this._withJobLock(job, async () => {
26160
+ if (!this._monitorOwns(job, generation)) return true;
26161
+ const result = await this._reapParallelJobLocked(job, { reattach: false, preserveMonitor: true });
26162
+ return isTerminalTask(job) || result.state === "terminal_uncollected";
26163
+ });
26164
+ if (!recovered) throw new Error(`Cursor ${job.effectiveExecution || job.execution || "Agent"} task timed out (${job.timeoutMs}ms)${detail}`);
26076
26165
  }
26077
26166
  async _requestExactAgentSelection(c, agentId) {
26078
26167
  if (!await this._ensureHistoryOpen(c)) return "REACT_ADAPTER_UNAVAILABLE";
@@ -26168,7 +26257,7 @@ var CursorBridge = class {
26168
26257
  job.resultUnavailable = true;
26169
26258
  job.terminalEvidence = evidence || "stable_completed_history_icon";
26170
26259
  job.recoveryState = "terminal_result_uncollected";
26171
- job.reservationScope = uncertainSubmissionReservationScope(job, error2);
26260
+ job.reservationScope = job.effectiveExecution === "fifo" ? "global" : uncertainSubmissionReservationScope(job, error2);
26172
26261
  this._safeSettleSessionJob(job, "terminal_result_uncollected", { needsAttention: true });
26173
26262
  }
26174
26263
  _abandonJob(job, reason) {
@@ -26216,7 +26305,7 @@ var CursorBridge = class {
26216
26305
  job.recoveryState = "agent_missing";
26217
26306
  return { stable: false, error: "The bound agentId was not found in Agent History", entry: second || first || null };
26218
26307
  }
26219
- const stable = first.id === second.id && first.showSpinner === second.showSpinner && String(first.icon || "") === String(second.icon || "");
26308
+ const stable = first.id === second.id && second.id === job.agentId && first.showSpinner === second.showSpinner && String(first.icon || "") === String(second.icon || "");
26220
26309
  return { stable, entry: second, error: stable ? null : "Agent History state is not yet stable" };
26221
26310
  }
26222
26311
  async _reapParallelJob(job, options = {}) {
@@ -26225,15 +26314,15 @@ var CursorBridge = class {
26225
26314
  }
26226
26315
  async _reapParallelJobLocked(job, options = {}) {
26227
26316
  if (!job || isTerminalTask(job)) return { changed: false, state: "terminal", task: this._taskView(job, true) };
26228
- if (job.execution !== "parallel_agent" || !this.activeParallel.has(job.id)) {
26317
+ if (!this.activeParallel.has(job.id)) {
26229
26318
  return { changed: false, state: "not_parallel_reservation", task: this._taskView(job, true) };
26230
26319
  }
26231
26320
  if (!job.agentId) {
26232
26321
  job.lastRecoveryAt = (/* @__PURE__ */ new Date()).toISOString();
26233
26322
  job.recoveryState = "unbound_agent";
26234
- return { changed: false, state: "unbound_agent", task: this._taskView(job, true) };
26323
+ return { changed: false, state: "unbound_agent", next: "The FIFO orphan has no agentId that can be safely rebound. Confirm that it stopped in the Cursor UI before explicitly abandoning it.", task: this._taskView(job, true) };
26235
26324
  }
26236
- this._invalidateParallelMonitor(job);
26325
+ if (!options.preserveMonitor) this._invalidateParallelMonitor(job);
26237
26326
  const observed = await this._readStableParallelEntry(job);
26238
26327
  if (!observed.stable || !observed.entry) {
26239
26328
  return { changed: false, state: job.recoveryState || "unstable", error: observed.error, task: this._taskView(job, true) };
@@ -26471,17 +26560,19 @@ var CursorBridge = class {
26471
26560
  job.cancelReason = reason || (job.execution === "parallel_agent" ? "User requested Cursor Agent cancellation" : "User requested FIFO task cancellation");
26472
26561
  job.cancelRequestSeq = Number(job.cancelRequestSeq || 0) + 1;
26473
26562
  }
26474
- return this._withJobLock(job, () => this._taskControlLocked(job, action, {
26563
+ const result = await this._withJobLock(job, () => this._taskControlLocked(job, action, {
26475
26564
  ...options,
26476
26565
  reason,
26477
26566
  expectedAgentId
26478
26567
  }));
26568
+ if (result && result.task) result.task = this._compactTaskView(job);
26569
+ return result;
26479
26570
  }
26480
26571
  async _taskControlLocked(job, action, options) {
26481
26572
  if (action === "reap") {
26482
26573
  const result = await this._reapParallelJobLocked(job, { reattach: true });
26483
26574
  if (result.state === "not_parallel_reservation" && job.phase === "orphaned") {
26484
- result.next = job.agentId ? "FIFO is bound to an agentId, but reap applies only to parallel_agent. Use cancel for a targeted stop, or abandon after confirmation." : "The FIFO orphan has no agentId that can be safely rebound. Confirm that it stopped in the Cursor UI before explicitly abandoning it.";
26575
+ result.next = job.agentId ? "The bound task has no recoverable reservation. Inspect its current state before cancelling." : "The FIFO orphan has no agentId that can be safely rebound. Confirm that it stopped in the Cursor UI before explicitly abandoning it.";
26485
26576
  }
26486
26577
  return { found: true, action, ...result };
26487
26578
  }
@@ -26539,10 +26630,13 @@ var CursorBridge = class {
26539
26630
  }
26540
26631
  const canTargetStop = job.execution === "parallel_agent" || !!job.agentId;
26541
26632
  if (canTargetStop && (job.execution === "parallel_agent" || job.phase === "orphaned")) {
26542
- if (job.execution === "parallel_agent") {
26633
+ if (this.activeParallel.has(job.id)) {
26543
26634
  this._invalidateParallelMonitor(job);
26544
26635
  const reaped = await this._reapParallelJobLocked(job, { reattach: false });
26545
- if (isTerminalTask(job)) return { found: true, action, ...reaped };
26636
+ if (isTerminalTask(job) || reaped.state === "terminal_uncollected") {
26637
+ job.cancelRequested = false;
26638
+ return { found: true, action, ...reaped };
26639
+ }
26546
26640
  }
26547
26641
  job.phase = "cancelling";
26548
26642
  job.recoveryState = "stopping";
@@ -26677,6 +26771,7 @@ var CursorBridge = class {
26677
26771
  } catch {
26678
26772
  s = { stop: 0, messageCount: 0, replyLength: 0, replyHash: 0 };
26679
26773
  }
26774
+ if (job) job.lastWaitObservation = { at: (/* @__PURE__ */ new Date()).toISOString(), stop: s.stop, messageCount: s.messageCount, replyLength: s.replyLength, sawStop: sawStop || s.stop > 0 };
26680
26775
  if (s.stop > 0) sawStop = true;
26681
26776
  if (sawStop && s.stop === 0 && s.replyLength > 0) {
26682
26777
  await sleep2(800);
@@ -26714,9 +26809,12 @@ var CursorBridge = class {
26714
26809
  throw new Error(`Cursor task timed out (${timeoutMs}ms) before generation was confirmed stopped with a complete assistant reply${taskHint}`);
26715
26810
  }
26716
26811
  _taskView(job, includeResult = false) {
26717
- if (includeResult) this._markTaskResultCollected(job);
26718
26812
  const view = {
26719
26813
  taskId: job.id,
26814
+ requestedTimeoutMs: job.requestedTimeoutMs ?? job.timeoutMs,
26815
+ effectiveTimeoutMs: job.timeoutMs,
26816
+ firstWaitError: job.firstWaitError || null,
26817
+ lastWaitObservation: job.lastWaitObservation || null,
26720
26818
  kind: job.kind,
26721
26819
  status: job.status,
26722
26820
  phase: job.phase,
@@ -26772,11 +26870,162 @@ var CursorBridge = class {
26772
26870
  if (job.recoveryState === "terminal_result_uncollected") {
26773
26871
  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.";
26774
26872
  } else {
26775
- view.attention = job.agentId ? job.execution === "parallel_agent" ? "Use cursor_task_control(action=reap) to recheck the original agentId. Use cancel when a stop is needed, and abandon only after manual confirmation while accepting residual write risk." : "FIFO is bound to an agentId. Use cursor_task_control(action=cancel) with the exact expected_agent_id for a targeted stop." : "This orphan has no agentId that can be safely rebound and globally blocks new delegation. Confirm that it stopped in the Cursor UI, then explicitly release it with cursor_task_control(action=abandon).";
26873
+ view.attention = job.agentId ? job.execution === "parallel_agent" ? "Use cursor_task_control(action=reap) to recheck the original agentId. Use cancel when a stop is needed, and abandon only after manual confirmation while accepting residual write risk." : "FIFO is bound to an agentId. Use reap to collect or resume monitoring; use cancel with the exact expected_agent_id to stop running work." : "This orphan has no agentId that can be safely rebound and globally blocks new delegation. Confirm that it stopped in the Cursor UI, then explicitly release it with cursor_task_control(action=abandon).";
26776
26874
  }
26777
26875
  }
26778
26876
  return view;
26779
26877
  }
26878
+ _collectedTaskView(job) {
26879
+ this._markTaskResultCollected(job);
26880
+ return this._taskView(job, true);
26881
+ }
26882
+ _compactTaskView(job, { summary = false } = {}) {
26883
+ const full = this._taskView(job);
26884
+ const selection = full.modelSelection && {
26885
+ configured: full.modelSelection.configured,
26886
+ applied: full.modelSelection.applied,
26887
+ model: full.modelSelection.model,
26888
+ effort: full.modelSelection.effort,
26889
+ requestedModel: full.modelSelection.requestedModel ?? full.modelSelection.model ?? full.modelPreference?.model,
26890
+ requestedEffort: full.modelSelection.requestedEffort ?? full.modelSelection.effort ?? full.modelPreference?.effort,
26891
+ effectiveModel: full.modelSelection.effectiveModel,
26892
+ effectiveEffort: full.modelSelection.effectiveEffort,
26893
+ failureClass: full.modelSelection.failureClass,
26894
+ retryable: full.modelSelection.retryable,
26895
+ errorCode: full.modelSelection.errorCode,
26896
+ lastError: full.modelSelection.lastError,
26897
+ failedAt: full.modelSelection.failedAt,
26898
+ verifiedAt: full.modelSelection.verifiedAt
26899
+ };
26900
+ const view = {
26901
+ taskId: full.taskId,
26902
+ kind: full.kind,
26903
+ status: full.status,
26904
+ phase: full.phase,
26905
+ execution: full.execution,
26906
+ effectiveExecution: full.effectiveExecution,
26907
+ projectPath: full.projectPath,
26908
+ sessionId: full.sessionId,
26909
+ agentId: full.agentId,
26910
+ sendState: full.sendState,
26911
+ modelSelection: selection,
26912
+ reservationHeld: full.reservationHeld,
26913
+ reservationScope: full.reservationScope,
26914
+ blocksFifo: full.blocksFifo,
26915
+ blocksAll: full.blocksAll,
26916
+ recoveryState: full.recoveryState,
26917
+ attention: full.attention,
26918
+ cancelRequested: full.cancelRequested,
26919
+ cancelReason: full.cancelReason,
26920
+ underlyingStopConfirmed: full.underlyingStopConfirmed,
26921
+ lastRecoveryAt: full.lastRecoveryAt,
26922
+ terminalEvidence: full.terminalEvidence,
26923
+ resultAvailable: job.result != null,
26924
+ resultLength: job.result == null ? 0 : String(job.result).length,
26925
+ resultUnread: job.result != null && !full.resultCollectedAt,
26926
+ resultUnavailable: full.resultUnavailable,
26927
+ resultCollectedAt: full.resultCollectedAt,
26928
+ error: full.error
26929
+ };
26930
+ if (selection && Object.values(selection).every((value) => value === void 0)) view.modelSelection = null;
26931
+ if (full.sessionError) view.sessionError = full.sessionError;
26932
+ if (full.firstWaitError) view.firstWaitError = full.firstWaitError;
26933
+ if (full.providerError) view.providerError = full.providerError;
26934
+ if (full.uiDiagnostic) view.uiDiagnostic = full.uiDiagnostic;
26935
+ if (full.workspaceBinding && full.workspaceBinding.ok === false) view.workspaceBinding = full.workspaceBinding;
26936
+ if ((full.error || full.status === "needs_attention") && full.workspaceBindingChecks) {
26937
+ view.workspaceBindingChecks = full.workspaceBindingChecks;
26938
+ }
26939
+ if (full.status === "needs_attention" && full.lastWaitObservation) view.lastWaitObservation = full.lastWaitObservation;
26940
+ if (summary) {
26941
+ return {
26942
+ taskId: view.taskId,
26943
+ status: view.status,
26944
+ phase: view.phase,
26945
+ agentId: view.agentId,
26946
+ sessionId: view.sessionId,
26947
+ resultAvailable: view.resultAvailable,
26948
+ resultLength: view.resultLength,
26949
+ resultUnread: view.resultUnread,
26950
+ resultCollectedAt: view.resultCollectedAt,
26951
+ ...view.sendState ? { sendState: view.sendState } : {},
26952
+ ...view.reservationHeld ? { reservationHeld: true, reservationScope: view.reservationScope } : {},
26953
+ ...view.blocksFifo ? { blocksFifo: true } : {},
26954
+ ...view.blocksAll ? { blocksAll: true } : {},
26955
+ ...view.recoveryState ? { recoveryState: view.recoveryState } : {},
26956
+ ...view.attention ? { attention: view.attention } : {},
26957
+ ...view.cancelRequested ? { cancelRequested: true } : {},
26958
+ ...view.cancelReason ? { cancelReason: view.cancelReason } : {},
26959
+ ...view.underlyingStopConfirmed != null ? { underlyingStopConfirmed: view.underlyingStopConfirmed } : {},
26960
+ ...view.lastRecoveryAt ? { lastRecoveryAt: view.lastRecoveryAt } : {},
26961
+ ...view.terminalEvidence ? { terminalEvidence: view.terminalEvidence } : {},
26962
+ ...view.resultUnavailable ? { resultUnavailable: true } : {},
26963
+ ...view.error ? { error: view.error } : {},
26964
+ ...view.providerError ? { providerError: view.providerError } : {},
26965
+ ...view.uiDiagnostic ? { uiDiagnostic: view.uiDiagnostic } : {},
26966
+ ...view.workspaceBinding ? { workspaceBinding: view.workspaceBinding } : {},
26967
+ ...view.workspaceBindingChecks ? { workspaceBindingChecks: view.workspaceBindingChecks } : {}
26968
+ };
26969
+ }
26970
+ return {
26971
+ ...view,
26972
+ requestedTimeoutMs: full.requestedTimeoutMs,
26973
+ effectiveTimeoutMs: full.effectiveTimeoutMs,
26974
+ readOnly: full.readOnly,
26975
+ allowedPaths: full.allowedPaths,
26976
+ modelPreference: full.modelPreference,
26977
+ requestContext: full.requestContext,
26978
+ sessionMode: full.sessionMode,
26979
+ sessionTurn: full.sessionTurn,
26980
+ sessionState: full.sessionState,
26981
+ requestId: full.requestId,
26982
+ provisionalAgentId: full.provisionalAgentId,
26983
+ agentLabel: full.agentLabel,
26984
+ createdAt: full.createdAt,
26985
+ startedAt: full.startedAt,
26986
+ sentAt: full.sentAt,
26987
+ finishedAt: full.finishedAt
26988
+ };
26989
+ }
26990
+ _compactStatusCommon() {
26991
+ const workspace = this.workspaceView();
26992
+ const runtime = this.runtimeModeView();
26993
+ const models = this.modelPreferencesView();
26994
+ return {
26995
+ pluginVersion: PLUGIN_VERSION,
26996
+ statusPath: "json-list",
26997
+ workspaceKey: workspace.workspaceKey,
26998
+ workspaceConfirmationRequired: workspace.workspaceConfirmationRequired,
26999
+ workspaceBindingWarning: workspace.workspaceBindingWarning,
27000
+ projectPath: workspace.projectPath,
27001
+ initialized: workspace.initialized,
27002
+ ...workspace.workspaceBinding && workspace.workspaceBinding.ok === false ? { workspaceBinding: workspace.workspaceBinding } : {},
27003
+ ...this.delegationView(),
27004
+ runtimeMode: runtime.runtimeMode,
27005
+ minimalModeWarning: runtime.minimalModeWarning,
27006
+ modelPreferences: models.modelPreferences,
27007
+ modelPreferencesUpdatedAt: models.modelPreferencesUpdatedAt,
27008
+ sessionStoragePersistent: !!this.sessionFile
27009
+ };
27010
+ }
27011
+ _compactLifecycleView() {
27012
+ const lifecycle = this._lastLifecycle;
27013
+ if (!lifecycle) {
27014
+ return { adapterPid: process.pid, status: null, lifecycleMode: null, persistent: null, degradedReason: null };
27015
+ }
27016
+ if (lifecycle.degradedReason || lifecycle.spawnErrorCode || lifecycle.error || lifecycle.supervisorError || lifecycle.needsAction || lifecycle.nextStep || lifecycle.retryable || lifecycle.status === "failed") {
27017
+ return lifecycle;
27018
+ }
27019
+ return {
27020
+ adapterPid: lifecycle.adapterPid,
27021
+ supervisorPid: lifecycle.supervisorPid,
27022
+ status: lifecycle.status,
27023
+ lifecycleMode: lifecycle.lifecycleMode,
27024
+ persistent: lifecycle.persistent,
27025
+ degradedReason: lifecycle.degradedReason,
27026
+ cursorPid: lifecycle.cursorPid
27027
+ };
27028
+ }
26780
27029
  _assertWorkspaceConfirmed() {
26781
27030
  if (this.workspaceConfirmationRequired) {
26782
27031
  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.");
@@ -26785,7 +27034,7 @@ var CursorBridge = class {
26785
27034
  _ensureTaskCapacity() {
26786
27035
  this._trimTasks(49);
26787
27036
  if (this.tasks.size >= 50) {
26788
- 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.");
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.');
26789
27038
  }
26790
27039
  }
26791
27040
  _trimTasks(limit = 50) {
@@ -26795,16 +27044,20 @@ var CursorBridge = class {
26795
27044
  if (this.tasks.size <= limit) break;
26796
27045
  }
26797
27046
  }
26798
- async status(taskId = "") {
27047
+ async status(taskId = "", { detail = "compact" } = {}) {
27048
+ const normalizedDetail = normalizeStatusDetail(detail);
26799
27049
  if (taskId) {
26800
27050
  const job = this.tasks.get(String(taskId));
26801
- if (!job) return { found: false, taskId: String(taskId), ...this.workspaceView(), ...this.delegationView(), ...this.runtimeModeView(), ...this.modelPreferencesView(), ...this.sessionRegistryView() };
26802
- return { found: true, ...this.workspaceView(), ...this.delegationView(), ...this.runtimeModeView(), ...this.modelPreferencesView(), ...this.sessionRegistryView(), ...this._taskView(job, true) };
27051
+ if (normalizedDetail === "full") {
27052
+ if (!job) return { found: false, taskId: String(taskId), ...this.workspaceView(), ...this.delegationView(), ...this.runtimeModeView(), ...this.modelPreferencesView(), ...this.sessionRegistryView() };
27053
+ return { found: true, ...this.workspaceView(), ...this.delegationView(), ...this.runtimeModeView(), ...this.modelPreferencesView(), ...this.sessionRegistryView(), ...this._collectedTaskView(job) };
27054
+ }
27055
+ return { found: !!job, ...this._compactStatusCommon(), ...job ? this._compactTaskView(job) : { taskId: String(taskId) } };
26803
27056
  }
26804
27057
  const parallelRunning = this.activeParallel.size;
26805
27058
  const uiBusy = this.busy;
26806
27059
  const globalBlocked = this._hasGlobalReservation();
26807
- const common = {
27060
+ const common = normalizedDetail === "full" ? {
26808
27061
  pluginVersion: PLUGIN_VERSION,
26809
27062
  statusPath: "json-list",
26810
27063
  ...this.workspaceView(),
@@ -26842,6 +27095,21 @@ var CursorBridge = class {
26842
27095
  runtimeMode: this.runtimeMode,
26843
27096
  presentation: null
26844
27097
  }
27098
+ } : {
27099
+ ...this._compactStatusCommon(),
27100
+ busy: uiBusy || parallelRunning > 0 || this.queue.length > 0,
27101
+ uiBusy,
27102
+ parallelRunning,
27103
+ idle: !uiBusy && parallelRunning === 0 && this.queue.length === 0,
27104
+ queued: this.queue.length,
27105
+ blockingTaskIds: [...this.activeParallel.values()].filter((job) => !isTerminalTask(job)).map((job) => job.id),
27106
+ globallyBlocked: globalBlocked,
27107
+ blockedQueuedCount: this.activeParallel.size > 0 ? this.queue.filter((job) => globalBlocked || job.effectiveExecution !== "parallel_agent").length : 0,
27108
+ activeParallel: [...this.activeParallel.values()].map((job) => this._compactTaskView(job, { summary: true })),
27109
+ recentTasks: [...this.tasks.values()].filter((job) => !this.activeParallel.has(job.id)).slice(-10).map((job) => this._compactTaskView(job, { summary: true })),
27110
+ unreadResultTaskIds: [...this.tasks.values()].filter((job) => isTerminalTask(job) && job.result != null && !job.resultCollectedAt).map((job) => job.id),
27111
+ taskRetentionLimit: 50,
27112
+ lifecycle: this._compactLifecycleView()
26845
27113
  };
26846
27114
  try {
26847
27115
  const ver = await httpJson("/json/version");
@@ -26862,6 +27130,13 @@ function buildSearchInputSchema() {
26862
27130
  required: ["query"]
26863
27131
  };
26864
27132
  }
27133
+ function normalizeStatusDetail(detail) {
27134
+ 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");
27137
+ }
27138
+ return detail;
27139
+ }
26865
27140
  var REQUEST_CONTEXT_SCHEMA = {
26866
27141
  type: "object",
26867
27142
  additionalProperties: false,
@@ -26875,7 +27150,7 @@ function buildToolDefinitions(bridgeInstance) {
26875
27150
  return [
26876
27151
  {
26877
27152
  name: "cursor_init",
26878
- description: "Initialize or reinitialize CCE for one local workspace. Give only the project path: Bridge saves it, finds Cursor, ensures the required connection, and opens or verifies the matching project. If Cursor was opened too early without CCE access, initialization safely keeps the binding and tells the user to save, close Cursor once, and repeat the same initialization sentence; it never force-closes Cursor. Cursor login and the user's old/new UI preference are preserved. When both UIs are open, Bridge selects the new Agents Window and creates work in the matching repository instead of Home.",
27153
+ description: "Initialize or reinitialize CCE for one local workspace. Give only the project path: Bridge saves it, finds Cursor, ensures the required connection, and opens or verifies the matching project. In Agents Window, initialization registers a missing local workspace through Cursor's project service and verifies its exact path before reporting ready. If Cursor was opened too early without CCE access, initialization safely keeps the binding and tells the user to save, close Cursor once, and repeat the same initialization sentence; it never force-closes Cursor. Cursor login and the user's old/new UI preference are preserved. When both UIs are open, Bridge selects the new Agents Window and creates work in the matching repository instead of Home.",
26879
27154
  inputSchema: {
26880
27155
  type: "object",
26881
27156
  properties: {
@@ -26891,7 +27166,7 @@ function buildToolDefinitions(bridgeInstance) {
26891
27166
  },
26892
27167
  bridgeInstance.environmentDelegationMode !== "off" ? {
26893
27168
  name: "cursor_do",
26894
- 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). Cursor can do the work, but the main agent still owns review and final verification. A direct user opt-out always wins.",
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.',
26895
27170
  inputSchema: {
26896
27171
  type: "object",
26897
27172
  properties: {
@@ -26900,7 +27175,7 @@ function buildToolDefinitions(bridgeInstance) {
26900
27175
  background: { type: "boolean", default: true, description: "When true, return the task ID immediately. When false, wait for the task to finish or need attention." },
26901
27176
  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." },
26902
27177
  read_only: { type: "boolean", default: false, description: "Set true when Cursor must not change the workspace." },
26903
- 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." },
27178
+ timeout_ms: { type: "integer", minimum: 3e4, maximum: 9e5, default: 6e5, description: "One monitoring budget after submission, shared by FIFO and automatic Agent recovery. Default 10 minutes, maximum 15 minutes; task status reports requested and effective budgets. Expiry probes the bound Agent once for completion; it does not cancel or resend work. Explicit reap may start a new budget for a still-running bound Agent, including FIFO." },
26904
27179
  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." },
26905
27180
  completion_contract: { type: "string", description: "Optional acceptance checks or a required final-report format." },
26906
27181
  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." },
@@ -26912,7 +27187,7 @@ function buildToolDefinitions(bridgeInstance) {
26912
27187
  } : null,
26913
27188
  {
26914
27189
  name: "cursor_task_control",
26915
- 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 collects a stable terminal result when possible. 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.",
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.',
26916
27191
  inputSchema: {
26917
27192
  type: "object",
26918
27193
  properties: {
@@ -26968,12 +27243,14 @@ function buildToolDefinitions(bridgeInstance) {
26968
27243
  },
26969
27244
  {
26970
27245
  name: "cursor_status",
26971
- description: "Read-only snapshot of Cursor connectivity, queued/running work, reservations, execution availability, persistent model/effort defaults, sessions, and normal/minimal runtime presentation. 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.",
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.',
26972
27247
  inputSchema: {
26973
27248
  type: "object",
27249
+ additionalProperties: false,
26974
27250
  properties: {
26975
27251
  task_id: { type: "string", description: "A task ID returned by cursor_do." },
26976
- session_id: { type: "string", description: "A persistent session ID returned by cursor_do(session_mode=create)." }
27252
+ 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." }
26977
27254
  }
26978
27255
  }
26979
27256
  }
@@ -27090,15 +27367,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request2) => {
27090
27367
  if (args && args.task_id && args.session_id) {
27091
27368
  throw cursorSessionError("STATUS_SELECTOR_AMBIGUOUS", "pass task_id or session_id, not both");
27092
27369
  }
27370
+ const detail = normalizeStatusDetail(args && args.detail);
27093
27371
  const statusMs = Math.max(1e3, Number(process.env.CURSOR_BRIDGE_STATUS_TIMEOUT || 8e3));
27094
27372
  let result;
27095
27373
  try {
27096
27374
  result = await Promise.race([
27097
- args && args.session_id ? Promise.resolve(bridge.sessionStatus(args.session_id)) : bridge.status(args && args.task_id),
27375
+ args && args.session_id ? Promise.resolve(bridge.sessionStatus(args.session_id)) : bridge.status(args && args.task_id, { detail }),
27098
27376
  new Promise((_, reject) => setTimeout(() => reject(new Error(`cursor_status_timeout_${statusMs}`)), statusMs))
27099
27377
  ]);
27100
27378
  } catch (error2) {
27101
- result = {
27379
+ result = detail === "full" ? {
27102
27380
  connected: false,
27103
27381
  pluginVersion: PLUGIN_VERSION,
27104
27382
  statusPath: "json-list",
@@ -27106,7 +27384,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request2) => {
27106
27384
  ...bridge.workspaceView(),
27107
27385
  ...bridge.delegationView(),
27108
27386
  ...bridge.runtimeModeView(),
27387
+ ...bridge.modelPreferencesView(),
27109
27388
  ...bridge.sessionRegistryView()
27389
+ } : {
27390
+ connected: false,
27391
+ ...bridge._compactStatusCommon(),
27392
+ ...args && args.task_id ? { found: false, taskId: String(args.task_id) } : {},
27393
+ ...args && args.session_id ? { found: false, sessionId: String(args.session_id) } : {},
27394
+ error: error2 instanceof Error ? error2.message : String(error2)
27110
27395
  };
27111
27396
  }
27112
27397
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
@@ -27177,6 +27462,7 @@ export {
27177
27462
  exprInspectAgentWorkspace,
27178
27463
  exprInspectWorkspaceRepository,
27179
27464
  exprOpenAgent,
27465
+ exprRegisterAgentsWorkspace,
27180
27466
  isConfirmedCompletedReply,
27181
27467
  isCursorEffortOptionText,
27182
27468
  isDurablyRegisteredParallelEntry,
@@ -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.16",
13
+ packageVersion: "0.2.0",
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.16",
3
+ "version": "0.2.0",
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.10.0"
50
+ "embeddedProductVersion": "6.0.0"
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. Collect each turn 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 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."
4
4
  ---
5
5
 
6
6
  # Cursor Delegate
@@ -21,11 +21,11 @@ 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 -> collect 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 terminal result by task_id -> 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.
28
- - Once a task has been selected for delegation, normally call `cursor_do` once with `background=true`, then continue non-conflicting primary-agent work. Bridge starts FIFO work in a clean chat automatically.
28
+ - Once a task has been selected for delegation, normally call `cursor_do` once with `background=true`, receive its compact submission receipt, then continue non-conflicting primary-agent work. Bridge starts FIFO work in a clean chat automatically.
29
29
  - Prefer `execution=fifo` unless the parallel contract is clearly satisfied.
30
30
  - Do not inject a unique completion marker or impose a minimum response length. Rely on task state, stable `task_id` or `agent_id`, and the actual result.
31
31
 
@@ -75,7 +75,7 @@ Do not choose parallel execution merely because there are many tasks. When depen
75
75
 
76
76
  1. Record the relevant pre-dispatch workspace state so later review can distinguish existing user changes.
77
77
  2. Form one independent task envelope per task using [delegation-contract.md](references/delegation-contract.md). Write its narrative instructions in the language of the user's current substantive task unless the user explicitly requests another language. Do not persist an inferred language or replace a clear conversational signal with the host/OS locale.
78
- 3. Call `cursor_do` with `background=true`. Use only the documented `session_mode` and `session_id` fields when continuity is explicit; never infer continuity from the visible chat.
78
+ 3. Call `cursor_do` with `background=true` and save its compact submission receipt. Use `background=false` only when an immediate synchronous full result is required. Use only the documented `session_mode` and `session_id` fields when continuity is explicit; never infer continuity from the visible chat.
79
79
  4. Save each returned `task_id`; for persistent work also save `session_id`. Treat `agent_id` as verification evidence, not the continuation handle.
80
80
  5. If a parallel submission does not return a usable `agent_id`, stop expanding the parallel batch and use `fifo` or report the ambiguous state.
81
81
 
@@ -83,12 +83,13 @@ The envelope may contain a small number of local implementation `open_questions`
83
83
 
84
84
  ## Collect and verify
85
85
 
86
- 1. Always query `cursor_status(task_id)` for the exact task. Do not treat the currently visible Cursor chat as task identity.
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. Compare Cursor's claimed work with the real diff, `allowed_paths`, and acceptance contract.
89
- 4. 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.
90
- 5. Run risk-proportionate verification in the primary agent. Cursor's response alone cannot support a formal pass, verified state, or governance transition.
91
- 6. Record each task as complete, partial, failed, timed out, or ambiguous before summarizing the batch.
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.
89
+ 4. Compare Cursor's claimed work with the real diff, `allowed_paths`, and acceptance contract.
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
+ 6. Run risk-proportionate verification in the primary agent. Cursor's response alone cannot support a formal pass, verified state, or governance transition.
92
+ 7. Record each task as complete, partial, failed, timed out, or ambiguous before summarizing the batch.
92
93
 
93
94
  Report the accepted result in the language of the user's current task. Keep `task_id`, `agent_id`, tool names, states, enum values, paths, commands, hashes, exact permission options, and error/status codes verbatim. If Cursor returned an artifact or report in another language, preserve it and summarize the relevant facts in the current task language.
94
95
 
@@ -97,14 +98,14 @@ Read [delegation-contract.md](references/delegation-contract.md) for state inter
97
98
  ## Handle abnormal states
98
99
 
99
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.
100
- - 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 collects that exact Agent. `cursor_status` is read-only and does not reap 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
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.
102
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.
103
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.
104
- - If Cursor shows a final UI response but Bridge has not collected it, use explicit `reap` against the original bound task. A `terminal_uncollected` result keeps the reservation for retry. Do not add a completion marker, increase a response-length requirement, or submit the same task again.
105
+ - If Cursor shows a final UI response but Bridge has not collected it, use explicit `reap` against the original bound task. A `terminal_uncollected` result keeps the reservation for retry; after recovery reaches a terminal state, retrieve the result through explicit full task status. Do not add a completion marker, increase a response-length requirement, or submit the same task again.
105
106
  - Task identity and reservations are process-local. After an MCP/Codex restart, do not claim the old `task_id` is recoverable; inspect Cursor Agent History and workspace changes manually before overlapping work.
106
107
  - A ready persistent `session_id` is stored outside the versioned plugin cache and may survive MCP/Codex restart or plugin update. Query `cursor_status(session_id)` before continuing. If it reports `needs_attention`, an expired sender lease, or a missing exact Agent binding, do not resubmit or silently create a replacement session.
107
- - Use `cursor_session_control(action=reconcile)` to check the exact Agent twice after an interrupted adapter. It may return the session to `ready` only from stable terminal evidence; an interrupted completed reply remains explicitly uncollected.
108
+ - Use `cursor_session_control(action=reconcile)` to check the exact Agent twice after an interrupted adapter. It may return the session to `ready` only from stable terminal evidence; an interrupted completed reply remains explicitly uncollected and `cursor_session_control(action=collect_result)` returns that session reply in full.
108
109
  - `cursor_session_control(action=abandon)` is the last resort for an uncertain session and requires `confirm=true`, a non-empty reason, and `acknowledge_may_still_write=true`. It closes only the Bridge mapping and does not prove that Cursor stopped.
109
110
  - If a timed-out task changed files, inspect the changes before deciding whether to continue, retry, or revert.
110
111
  - If changes exceed `allowed_paths`, stop accepting the result and report the scope violation.
@@ -21,7 +21,7 @@ Provide every task independently:
21
21
  | `read_only` | Use `true` for lookup and analysis; use `false` for any file modification. |
22
22
  | `allowed_paths` | Required when `read_only=false`. Provide the smallest workspace-relative path set, with no glob, absolute path, or workspace-escaping `..`. Omit it when `read_only=true`. This is not a filesystem sandbox. |
23
23
  | `completion_contract` | State the deliverables, validation commands, permitted incomplete items, final report format, and that narrative output should follow the task language. Preserve paths, commands, identifiers, and machine tokens verbatim. |
24
- | `background` | Default to `true` so the primary agent may continue independent work. |
24
+ | `background` | Default to `true` so the primary agent may continue independent work; it returns a compact submission receipt. Set `false` only to wait synchronously for the full result body. |
25
25
 
26
26
  ## Routing contract
27
27
 
@@ -65,12 +65,20 @@ Bounded write task:
65
65
 
66
66
  For dependent or path-overlapping work, change `execution` to `fifo` and submit the next task only after accepting its predecessor.
67
67
 
68
+ Compact collection flow:
69
+
70
+ 1. After `cursor_do(background=true)`, save the compact receipt's `task_id`.
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.
73
+
68
74
  ## Identity and collection contract
69
75
 
70
76
  - `task_id` is the stable identity used by the primary agent to query and summarize a task. Save it immediately after dispatch.
71
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.
72
- - Determine task state only through `cursor_status(task_id)`, not the currently selected chat or latest visible response.
73
- - A collected result should include at least task state, summary, changed files, validation performed, failures or blockers, and the raw Cursor response.
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.
81
+ - `cursor_session_control(action=collect_result)` always returns the full session reply.
74
82
  - Do not require a unique completion marker or minimum response length. Bridge determines completion from Agent state, stopped generation, and response stability.
75
83
 
76
84
  ### State table
@@ -78,15 +86,15 @@ For dependent or path-overlapping work, change `execution` to `fifo` and submit
78
86
  | State or phase | Primary-agent action |
79
87
  |---|---|
80
88
  | `queued/submitting/running/collecting` | Keep the original task and continue polling by `task_id`. More than two minutes is not a failure. |
81
- | `completed` | Read the raw response, then inspect the real diff, allowed paths, and completion contract. |
89
+ | `completed` | Call `cursor_status(task_id, detail="full")` to read the raw response, then inspect the real diff, allowed paths, and completion contract. |
82
90
  | `failed` | Read the explicit error and determine whether the Cursor Agent actually failed before deciding to rework. |
83
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. |
84
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. |
85
- | `terminal_uncollected` | Agent History is stably terminal but the final response extraction failed. Keep the reservation and retry explicit `reap`; 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 full task status to retrieve the result. Do not release on one DOM failure. |
86
94
  | `cancelled` | The exact Agent Stop action or an unsent queued cancellation was confirmed; the reservation is released. |
87
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. |
88
96
 
89
- For an R6-style false negative, continue querying 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`. 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 explicit full task status. Do not work around collection by requiring a longer reply, injecting a completion marker, or submitting the same task again.
90
98
 
91
99
  ## Primary-agent acceptance contract
92
100
 
@@ -106,5 +114,5 @@ Cursor's completion statement means only that delegated execution ended; it is n
106
114
  - Stop automatic integration when parallel tasks conflict and return the batch to primary-agent review.
107
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.
108
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.
109
- - `cursor_status` is a pure snapshot. Reconciliation happens only through explicit `cursor_task_control`.
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`.
110
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.