pi-cursor-bridge 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursor-bridge",
3
- "version": "6.0.4+codex.20260921093343",
3
+ "version": "6.0.6",
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
@@ -12,6 +12,6 @@ pi install npm:pi-cursor-bridge
12
12
 
13
13
  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.
14
14
 
15
- Pi wrapper 0.2.4 embeds Cursor Bridge 6.0.4, including explicit request provenance for CCE and delegated tasks. Its result contract returns a compact receipt for background `cursor_do` work: poll `cursor_status(task_id)` normally, retrieve the terminal raw reply with `cursor_status(task_id, detail="result")`, check `isError` before using its content, and use `detail="full"` only when diagnostics are needed. Cursor must be installed and signed in. Current end-to-end compatibility claims remain scoped to the environments documented in the main repository.
15
+ Pi wrapper 0.2.6 embeds Cursor Bridge 6.0.6, including Cursor 3.21 model-provider alias matching and `reasoning_effort` verification for Grok 4.7/high. Its result contract returns a compact receipt for background `cursor_do` work: poll `cursor_status(task_id)` normally, retrieve the terminal raw reply with `cursor_status(task_id, detail="result")`, check `isError` before using its content, and use `detail="full"` only when diagnostics are needed. Cursor must be installed and signed in. Current end-to-end compatibility claims remain scoped to the environments documented in the main repository.
16
16
 
17
17
  Full documentation: [English](https://github.com/Vanyangyang/cursor-bridge#readme) · [简体中文](https://github.com/Vanyangyang/cursor-bridge/blob/main/README.zh-CN.md)
@@ -12989,8 +12989,8 @@ function withLifecycle(result, mode, extra = {}) {
12989
12989
  };
12990
12990
  }
12991
12991
  async function ensureCursorRunning(options = {}) {
12992
- const bindingFile = options.workspaceFile || resolveWorkspaceBindingFile();
12993
- const bindingKey = options.workspaceKey || resolveWorkspaceBindingKey();
12992
+ const bindingFile = Object.hasOwn(options, "workspaceFile") ? options.workspaceFile : resolveWorkspaceBindingFile();
12993
+ const bindingKey = options.workspaceKey || resolveWorkspaceBindingKey(process.env, { cwd: options.adapterStartCwd ?? process.cwd() });
12994
12994
  const persistedBinding = readWorkspaceBinding(bindingFile, bindingKey);
12995
12995
  const hostProjectPath = resolveClaudeProjectPath();
12996
12996
  const projectPath = Object.hasOwn(options, "projectPath") ? options.projectPath : resolveProjectPath(persistedBinding && persistedBinding.projectPath || process.env.CURSOR_PROJECT_PATH, {
@@ -22985,7 +22985,7 @@ function updateCursorSessionRegistry(filePath, mutator, options = {}) {
22985
22985
  // server.mjs
22986
22986
  init_cursor_ensure_core();
22987
22987
  init_lifecycle_paths();
22988
- var PLUGIN_VERSION = "6.0.4";
22988
+ var PLUGIN_VERSION = "6.0.6";
22989
22989
  var CDP_PORT2 = Number(process.env.CURSOR_BRIDGE_CDP_PORT || 9223);
22990
22990
  var ORIGIN = `http://localhost:${CDP_PORT2}`;
22991
22991
  var QUERY_TIMEOUT = Number(process.env.CURSOR_BRIDGE_TIMEOUT || 3e5);
@@ -23550,6 +23550,10 @@ var EXPR_SELECTED_AGENT_MODEL_CONFIG = `(function(){
23550
23550
  function normalizeModelPickerText(value) {
23551
23551
  return String(value || "").trim().toLowerCase().replace(/extra[\s_-]*high/g, "xhigh").replace(/[^a-z0-9]+/g, "");
23552
23552
  }
23553
+ function normalizeModelPickerModelText(value) {
23554
+ const normalized = normalizeModelPickerText(value);
23555
+ return normalized.startsWith("cursor") && normalized.length > "cursor".length ? normalized.slice("cursor".length) : normalized;
23556
+ }
23553
23557
  function isCursorEffortOptionText(text) {
23554
23558
  return CURSOR_MODEL_EFFORTS.includes(normalizeModelPickerText(text));
23555
23559
  }
@@ -23559,10 +23563,11 @@ function modelPickerAvailableIsDecisive(kind, available) {
23559
23563
  return available.some((text) => isCursorEffortOptionText(text));
23560
23564
  }
23561
23565
  function selectModelPickerRow(rows, requested, kind = "model") {
23562
- const wanted = normalizeModelPickerText(requested);
23566
+ const normalize = kind === "parameter" ? normalizeModelPickerText : normalizeModelPickerModelText;
23567
+ const wanted = normalize(requested);
23563
23568
  if (!wanted) return null;
23564
23569
  const candidates = (Array.isArray(rows) ? rows : []).filter((row) => row && row.disabled !== true && (kind === "any" || row.kind === kind)).map((row) => {
23565
- const normalized = normalizeModelPickerText(row.text);
23570
+ const normalized = normalize(row.text);
23566
23571
  let score = normalized === wanted ? 1e3 : 0;
23567
23572
  if (kind !== "parameter") {
23568
23573
  if (!score && normalized.startsWith(wanted)) score = 800;
@@ -23690,15 +23695,16 @@ function exprCreateAgentForWorkspace(projectPath) {
23690
23695
  }
23691
23696
  function matchSelectedAgentModelConfig(snapshot, requestedModel, requestedEffort) {
23692
23697
  if (!snapshot || snapshot.found !== true) return null;
23693
- const requested = normalizeModelPickerText(requestedModel);
23698
+ const requested = normalizeModelPickerModelText(requestedModel);
23694
23699
  const entries = Array.isArray(snapshot.selectedModels) && snapshot.selectedModels.length ? snapshot.selectedModels : [{ modelId: snapshot.modelName, parameters: {} }];
23695
23700
  const matches = entries.filter((entry) => {
23696
- const candidate = normalizeModelPickerText(entry && entry.modelId);
23697
- return candidate && (candidate === requested || requested === `cursor${candidate}`);
23701
+ const candidate = normalizeModelPickerModelText(entry && entry.modelId);
23702
+ return candidate && candidate === requested;
23698
23703
  });
23699
23704
  if (matches.length !== 1) return null;
23700
23705
  const match = matches[0];
23701
- const effort = normalizeCursorModelEffort(match.parameters && match.parameters.effort, "");
23706
+ const parameters = match.parameters || {};
23707
+ const effort = normalizeCursorModelEffort(parameters.reasoning_effort ?? parameters.effort, "");
23702
23708
  if (requestedEffort && effort !== requestedEffort) return null;
23703
23709
  return { modelId: match.modelId, effort: effort || null };
23704
23710
  }
@@ -24276,12 +24282,12 @@ var CursorBridge = class {
24276
24282
  this.runtimeModeSource = options.runtimeMode !== void 0 ? "constructor" : persistedRuntimeMode ? "persistent" : process.env.CURSOR_BRIDGE_RUNTIME_MODE ? "environment" : "default";
24277
24283
  this.runtimeModeScope = persistedRuntimeMode ? "persistent" : options.runtimeMode !== void 0 ? "constructor" : process.env.CURSOR_BRIDGE_RUNTIME_MODE ? "environment" : "default";
24278
24284
  this.workspaceFile = options.workspaceFile === null ? null : resolve8(options.workspaceFile || resolveWorkspaceBindingFile());
24279
- this.workspaceKey = options.workspaceKey || resolveWorkspaceBindingKey();
24285
+ this.workspaceKey = options.workspaceKey || resolveWorkspaceBindingKey(process.env, { cwd: this.adapterStartCwd });
24280
24286
  const persistedWorkspace = options.projectPath === void 0 ? readWorkspaceBinding(this.workspaceFile, this.workspaceKey) : null;
24281
24287
  this.projectPath = options.projectPath !== void 0 ? resolve8(String(options.projectPath)) : persistedWorkspace && persistedWorkspace.projectPath || null;
24282
24288
  this.workspaceSource = options.projectPath !== void 0 ? "constructor" : persistedWorkspace ? "persistent_init" : "auto_detect";
24283
24289
  this.workspaceUpdatedAt = persistedWorkspace && persistedWorkspace.updatedAt || null;
24284
- this.workspaceConfirmationRequired = this.workspaceKey === "default" && !!persistedWorkspace;
24290
+ this.workspaceConfirmationRequired = this.workspaceKey === "default" && options.projectPath === void 0;
24285
24291
  this.modelPreferencesFile = options.modelPreferencesFile === null ? null : resolve8(options.modelPreferencesFile || resolveCursorModelPreferencesFile());
24286
24292
  this.modelPreferences = readCursorModelPreferences(this.modelPreferencesFile);
24287
24293
  this.sessionFile = options.sessionFile === null ? null : resolve8(options.sessionFile || resolveCursorSessionRegistryFile());
@@ -24297,6 +24303,8 @@ var CursorBridge = class {
24297
24303
  this.activeParallel = /* @__PURE__ */ new Map();
24298
24304
  this.currentJob = null;
24299
24305
  this._uiTail = Promise.resolve();
24306
+ this._workspaceTail = Promise.resolve();
24307
+ this._workspacePending = 0;
24300
24308
  this.parallelRestoreAgentId = null;
24301
24309
  this.parallelRestoreTargetId = null;
24302
24310
  }
@@ -24304,6 +24312,8 @@ var CursorBridge = class {
24304
24312
  const resolvedProjectPath = this.projectPath || this._lastLifecycle && this._lastLifecycle.projectPath || null;
24305
24313
  return {
24306
24314
  workspaceKey: this.workspaceKey,
24315
+ workspaceIdentitySource: this.workspaceKey === "default" ? "unavailable" : this.workspaceKey.split(":")[0],
24316
+ workspaceBusy: this._workspacePending > 0,
24307
24317
  workspaceConfirmationRequired: this.workspaceConfirmationRequired,
24308
24318
  workspaceBindingWarning: this.workspaceConfirmationRequired ? "This adapter has no host workspace identity. Run cursor_init with the intended project before using its shared saved binding." : null,
24309
24319
  projectPath: resolvedProjectPath,
@@ -24686,8 +24696,12 @@ var CursorBridge = class {
24686
24696
  if (!["reconcile", "collect_result", "close", "forget", "abandon"].includes(action)) {
24687
24697
  throw cursorSessionError("SESSION_ACTION_UNSUPPORTED", "expected reconcile, collect_result, close, forget, or abandon");
24688
24698
  }
24689
- if (action === "reconcile") return this._reconcileSession(id);
24690
- if (action === "collect_result") return this._reconcileSession(id, { collectResult: true });
24699
+ if (action === "reconcile" || action === "collect_result") {
24700
+ return this._withWorkspaceAdmission(() => {
24701
+ this._assertWorkspaceConfirmed();
24702
+ return this._reconcileSession(id, { collectResult: action === "collect_result" });
24703
+ });
24704
+ }
24691
24705
  let result;
24692
24706
  updateCursorSessionRegistry(this.sessionFile, (registry2) => {
24693
24707
  const session = registry2.sessions[id];
@@ -24740,6 +24754,10 @@ var CursorBridge = class {
24740
24754
  return result;
24741
24755
  }
24742
24756
  async initializeWorkspace(projectPath) {
24757
+ const result = await this._withWorkspaceAdmission(() => this._initializeWorkspace(projectPath));
24758
+ return { ...result, workspaceBusy: this._workspacePending > 0 };
24759
+ }
24760
+ async _initializeWorkspace(projectPath) {
24743
24761
  if (this._healing) await this._healing.catch(() => {
24744
24762
  });
24745
24763
  if (this.busy || this.activeParallel.size > 0 || this.queue.length > 0) {
@@ -24965,19 +24983,23 @@ var CursorBridge = class {
24965
24983
  if (!text) throw new Error("query must not be empty");
24966
24984
  if (text.length > 2e4) throw new Error("query exceeds the 20,000-character limit");
24967
24985
  const requestContext = normalizeRequestContext(options.requestContext);
24968
- this._assertWorkspaceConfirmed();
24969
- if (this._hasGlobalReservation()) {
24970
- throw new Error("A global Cursor reservation has an unconfirmed Stop state; resolve blockingTaskIds from cursor_status first");
24971
- }
24972
- await this._ensureCursor();
24973
- const job = this._enqueue("context_engine", buildContextEnginePrompt(text, requestContext), {
24974
- requestContext,
24975
- timeoutMs: QUERY_TIMEOUT,
24976
- newChat: true,
24977
- execution: "fifo",
24978
- readOnly: true,
24979
- allowedPaths: [],
24980
- modelPreference: this._modelPreferenceFor("cce")
24986
+ const job = await this._withWorkspaceAdmission(async () => {
24987
+ this._assertWorkspaceConfirmed(options.workspacePath);
24988
+ if (this._hasGlobalReservation()) {
24989
+ throw new Error("A global Cursor reservation has an unconfirmed Stop state; resolve blockingTaskIds from cursor_status first");
24990
+ }
24991
+ await this._ensureCursor();
24992
+ this._assertWorkspaceConfirmed(options.workspacePath);
24993
+ return this._enqueue("context_engine", buildContextEnginePrompt(text, requestContext), {
24994
+ requestContext,
24995
+ timeoutMs: QUERY_TIMEOUT,
24996
+ newChat: true,
24997
+ execution: "fifo",
24998
+ readOnly: true,
24999
+ allowedPaths: [],
25000
+ modelPreference: this._modelPreferenceFor("cce"),
25001
+ projectPath: this._lastLifecycle?.projectPath || this.projectPath || null
25002
+ });
24981
25003
  });
24982
25004
  const result = normalizeCceSearchResult(await job.promise);
24983
25005
  this._markTaskResultCollected(job);
@@ -24991,6 +25013,14 @@ var CursorBridge = class {
24991
25013
  return this.contextEngine(query);
24992
25014
  }
24993
25015
  async doTask(prompt, options = {}) {
25016
+ const admitted = await this._withWorkspaceAdmission(() => this._prepareTask(prompt, options));
25017
+ if (admitted.duplicate) return admitted.response;
25018
+ const job = admitted.job;
25019
+ if (options.background !== false) return this._compactTaskView(job);
25020
+ await job.promise;
25021
+ return this._collectedTaskView(job);
25022
+ }
25023
+ async _prepareTask(prompt, options = {}) {
24994
25024
  if (!this.delegationEnabled) {
24995
25025
  throw new Error("cursor_do is disabled by CURSOR_BRIDGE_DELEGATION=off; restart the MCP server without that setting to enable delegation");
24996
25026
  }
@@ -24998,7 +25028,7 @@ var CursorBridge = class {
24998
25028
  if (!text) throw new Error("prompt must not be empty");
24999
25029
  if (text.length > 1e5) throw new Error("prompt exceeds the 100,000-character limit");
25000
25030
  const requestContext = normalizeRequestContext(options.requestContext);
25001
- this._assertWorkspaceConfirmed();
25031
+ this._assertWorkspaceConfirmed(options.workspacePath);
25002
25032
  if (this._hasGlobalReservation()) {
25003
25033
  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");
25004
25034
  }
@@ -25043,6 +25073,7 @@ var CursorBridge = class {
25043
25073
  throw cursorSessionError("SESSION_SCOPE_REQUIRED", "repeat read_only=true or an allowed_paths subset for every continued turn");
25044
25074
  }
25045
25075
  await this._ensureCursor();
25076
+ this._assertWorkspaceConfirmed(options.workspacePath);
25046
25077
  const projectPath = this._lastLifecycle && this._lastLifecycle.projectPath || this.projectPath || null;
25047
25078
  if (sessionMode !== "isolated" && !projectPath) {
25048
25079
  throw cursorSessionError("SESSION_WORKSPACE_REQUIRED", "initialize one workspace before creating or continuing a session");
@@ -25083,7 +25114,8 @@ var CursorBridge = class {
25083
25114
  modelPreference = session.modelPreference || null;
25084
25115
  if (duplicate) {
25085
25116
  const existing = this.tasks.get(session.activeTaskId || session.lastTask && session.lastTask.taskId || "");
25086
- return existing ? { duplicate: true, ...options.background === false ? this._collectedTaskView(existing) : this._compactTaskView(existing) } : { duplicate: true, ...this._sessionView(session) };
25117
+ const response = existing ? { duplicate: true, ...options.background === false ? this._collectedTaskView(existing) : this._compactTaskView(existing) } : { duplicate: true, ...this._sessionView(session) };
25118
+ return { duplicate: true, response };
25087
25119
  }
25088
25120
  }
25089
25121
  const job = this._enqueue("do", fullPrompt, {
@@ -25107,9 +25139,7 @@ var CursorBridge = class {
25107
25139
  agentId: sessionMode === "continue" ? session && session.agentId : null,
25108
25140
  agentLabel: sessionMode === "continue" ? session && session.agentLabel : null
25109
25141
  });
25110
- if (options.background !== false) return this._compactTaskView(job);
25111
- await job.promise;
25112
- return this._collectedTaskView(job);
25142
+ return { job };
25113
25143
  }
25114
25144
  _assertNoParallelPathConflict(allowedPaths) {
25115
25145
  if (this._hasGlobalReservation()) {
@@ -25299,6 +25329,17 @@ var CursorBridge = class {
25299
25329
  });
25300
25330
  return run;
25301
25331
  }
25332
+ // Serialize target changes with preparation through enqueue, never with a running Agent.
25333
+ // The UI lock cannot serve this purpose: initialization may acquire it to register a project.
25334
+ _withWorkspaceAdmission(fn) {
25335
+ this._workspacePending++;
25336
+ const run = this._workspaceTail.then(fn, fn).finally(() => {
25337
+ this._workspacePending--;
25338
+ });
25339
+ this._workspaceTail = run.catch(() => {
25340
+ });
25341
+ return run;
25342
+ }
25302
25343
  _withJobLock(job, fn) {
25303
25344
  const previous = job.controlTail || Promise.resolve();
25304
25345
  const run = previous.then(fn, fn);
@@ -25330,6 +25371,8 @@ var CursorBridge = class {
25330
25371
  reason: "adapter-heal",
25331
25372
  runtimeMode: this.runtimeMode,
25332
25373
  adapterStartCwd: this.adapterStartCwd,
25374
+ workspaceKey: this.workspaceKey,
25375
+ workspaceFile: this.workspaceFile,
25333
25376
  ...this.projectPath ? { projectPath: this.projectPath } : {}
25334
25377
  });
25335
25378
  this._lastLifecycle = lifecycleFromEnsureResult(rr, this.runtimeMode);
@@ -25905,7 +25948,7 @@ var CursorBridge = class {
25905
25948
  }
25906
25949
  stage = "verify_model";
25907
25950
  let trigger2 = await this._readModelPickerTrigger(c);
25908
- if (!trigger2.found || !normalizeModelPickerText(trigger2.text).includes(normalizeModelPickerText(requestedModel))) {
25951
+ if (!trigger2.found || !normalizeModelPickerModelText(trigger2.text).includes(normalizeModelPickerModelText(requestedModel))) {
25909
25952
  const reopened = await this._openModelPicker(c);
25910
25953
  located = await this._findModelPickerModel(c, reopened, requestedModel);
25911
25954
  const selected = selectModelPickerRow(located.snapshot.rows.filter((row) => row.selected), requestedModel, "model");
@@ -27531,6 +27574,8 @@ var CursorBridge = class {
27531
27574
  pluginVersion: PLUGIN_VERSION,
27532
27575
  statusPath: "json-list",
27533
27576
  workspaceKey: workspace.workspaceKey,
27577
+ workspaceIdentitySource: workspace.workspaceIdentitySource,
27578
+ workspaceBusy: workspace.workspaceBusy,
27534
27579
  workspaceConfirmationRequired: workspace.workspaceConfirmationRequired,
27535
27580
  workspaceBindingWarning: workspace.workspaceBindingWarning,
27536
27581
  projectPath: workspace.projectPath,
@@ -27562,9 +27607,45 @@ var CursorBridge = class {
27562
27607
  cursorPid: lifecycle.cursorPid
27563
27608
  };
27564
27609
  }
27565
- _assertWorkspaceConfirmed() {
27610
+ _workspaceRecoveryError(code, message, expectedProjectPath = null) {
27611
+ const error2 = new Error(`${code}: ${message}`);
27612
+ error2.code = code;
27613
+ error2.workspaceRecovery = {
27614
+ workspaceKey: this.workspaceKey,
27615
+ currentProjectPath: this._lastLifecycle?.projectPath || this.projectPath || null,
27616
+ expectedProjectPath,
27617
+ tool: "cursor_init",
27618
+ arguments: expectedProjectPath ? { path: expectedProjectPath } : null,
27619
+ requiresIdle: true,
27620
+ retrySafe: true,
27621
+ submissionStarted: false,
27622
+ nextStep: "Read cursor_status. When idle with no blocking tasks, run cursor_init with the caller's intended absolute project path, verify ready and the exact path, then retry this unsent request once. Never infer the target from a saved default binding or repository name."
27623
+ };
27624
+ return error2;
27625
+ }
27626
+ _assertWorkspaceConfirmed(workspacePath) {
27627
+ let expected = null;
27628
+ if (workspacePath !== void 0) {
27629
+ if (typeof workspacePath !== "string" || !isAbsoluteWorkspacePath(workspacePath)) {
27630
+ throw cursorSessionError("WORKSPACE_PATH_INVALID", "workspace_path must be an absolute project directory or .code-workspace path");
27631
+ }
27632
+ expected = normalizeWorkspacePath(workspacePath);
27633
+ }
27566
27634
  if (this.workspaceConfirmationRequired) {
27567
- 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.");
27635
+ throw this._workspaceRecoveryError("WORKSPACE_CONFIRMATION_REQUIRED", "this adapter has no host workspace identity. Run cursor_init with the intended project before submitting work; a saved default path is not confirmation.", expected);
27636
+ }
27637
+ if (expected) {
27638
+ const current = this._lastLifecycle?.projectPath || this.projectPath;
27639
+ if (!current) {
27640
+ throw this._workspaceRecoveryError("WORKSPACE_INITIALIZATION_REQUIRED", "Run cursor_init with workspace_path before submitting work.", expected);
27641
+ }
27642
+ const canonical = (path) => {
27643
+ const normalized = normalizeWorkspacePath(path);
27644
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
27645
+ };
27646
+ if (canonical(current) !== canonical(expected)) {
27647
+ throw this._workspaceRecoveryError("WORKSPACE_MISMATCH", "workspace_path does not match the current binding. Run cursor_init with the intended project when idle; no work was submitted.", expected);
27648
+ }
27568
27649
  }
27569
27650
  }
27570
27651
  _ensureTaskCapacity() {
@@ -27612,10 +27693,10 @@ var CursorBridge = class {
27612
27693
  ...this.runtimeModeView(),
27613
27694
  ...this.modelPreferencesView(),
27614
27695
  ...this.sessionRegistryView(),
27615
- busy: uiBusy || parallelRunning > 0 || this.queue.length > 0,
27696
+ busy: uiBusy || parallelRunning > 0 || this.queue.length > 0 || this._workspacePending > 0,
27616
27697
  uiBusy,
27617
27698
  parallelRunning,
27618
- idle: !uiBusy && parallelRunning === 0 && this.queue.length === 0,
27699
+ idle: !uiBusy && parallelRunning === 0 && this.queue.length === 0 && this._workspacePending === 0,
27619
27700
  queued: this.queue.length,
27620
27701
  blockingTaskIds: [...this.activeParallel.values()].filter((job) => !isTerminalTask(job)).map((job) => job.id),
27621
27702
  globallyBlocked: globalBlocked,
@@ -27644,10 +27725,10 @@ var CursorBridge = class {
27644
27725
  }
27645
27726
  } : {
27646
27727
  ...this._compactStatusCommon(),
27647
- busy: uiBusy || parallelRunning > 0 || this.queue.length > 0,
27728
+ busy: uiBusy || parallelRunning > 0 || this.queue.length > 0 || this._workspacePending > 0,
27648
27729
  uiBusy,
27649
27730
  parallelRunning,
27650
- idle: !uiBusy && parallelRunning === 0 && this.queue.length === 0,
27731
+ idle: !uiBusy && parallelRunning === 0 && this.queue.length === 0 && this._workspacePending === 0,
27651
27732
  queued: this.queue.length,
27652
27733
  blockingTaskIds: [...this.activeParallel.values()].filter((job) => !isTerminalTask(job)).map((job) => job.id),
27653
27734
  globallyBlocked: globalBlocked,
@@ -27672,6 +27753,7 @@ function buildSearchInputSchema() {
27672
27753
  type: "object",
27673
27754
  properties: {
27674
27755
  query: { type: "string", description: "Describe the behavior, concept, symbol relationship, or ownership boundary to locate. State intent instead of guessing a directory." },
27756
+ workspace_path: WORKSPACE_PATH_SCHEMA,
27675
27757
  request_context: REQUEST_CONTEXT_SCHEMA
27676
27758
  },
27677
27759
  required: ["query"]
@@ -27693,6 +27775,10 @@ var REQUEST_CONTEXT_SCHEMA = {
27693
27775
  source: { type: "string", enum: ["user", "model", "mixed", "unknown"], description: "user for explicitly supplied user requirements, model for model-authored tasks/inferences, mixed when the prompt clearly separates both; unknown when not established." }
27694
27776
  }
27695
27777
  };
27778
+ var WORKSPACE_PATH_SCHEMA = {
27779
+ type: "string",
27780
+ description: "The caller's intended absolute project directory or .code-workspace path. Supply it when known to reject stale or cross-task bindings before sending. This is a target assertion, not an automatic workspace switch. On a workspace confirmation, initialization, or mismatch error, check idle status, run cursor_init with this path, verify ready, and retry once."
27781
+ };
27696
27782
  function buildToolDefinitions(bridgeInstance) {
27697
27783
  return [
27698
27784
  {
@@ -27718,6 +27804,7 @@ function buildToolDefinitions(bridgeInstance) {
27718
27804
  type: "object",
27719
27805
  properties: {
27720
27806
  prompt: { type: "string", description: "The task Cursor should receive. State the goal, boundaries, and what a complete result looks like." },
27807
+ workspace_path: WORKSPACE_PATH_SCHEMA,
27721
27808
  request_context: REQUEST_CONTEXT_SCHEMA,
27722
27809
  background: { type: "boolean", default: true, description: "When true, return the task ID immediately. When false, wait for the task to finish or need attention." },
27723
27810
  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." },
@@ -27810,29 +27897,35 @@ var server = new Server(
27810
27897
  { capabilities: { tools: { listChanged: true } } }
27811
27898
  );
27812
27899
  async function ensureBridgeCursor(targetBridge, reason) {
27813
- targetBridge._refreshPersistedRuntimeMode();
27814
- const { ensureCursorRunning: ensureCursorRunning2 } = await Promise.resolve().then(() => (init_launch_cursor(), launch_cursor_exports));
27815
- const r = await ensureCursorRunning2({
27816
- reason,
27817
- runtimeMode: targetBridge.runtimeMode,
27818
- adapterStartCwd: targetBridge.adapterStartCwd,
27819
- ...targetBridge.projectPath ? { projectPath: targetBridge.projectPath } : {}
27900
+ return targetBridge._withWorkspaceAdmission(async () => {
27901
+ targetBridge._refreshPersistedRuntimeMode();
27902
+ const { ensureCursorRunning: ensureCursorRunning2 } = await Promise.resolve().then(() => (init_launch_cursor(), launch_cursor_exports));
27903
+ const r = await ensureCursorRunning2({
27904
+ reason,
27905
+ runtimeMode: targetBridge.runtimeMode,
27906
+ adapterStartCwd: targetBridge.adapterStartCwd,
27907
+ workspaceKey: targetBridge.workspaceKey,
27908
+ workspaceFile: targetBridge.workspaceFile,
27909
+ // Prewarming Cursor is not permission to open another task's saved default project.
27910
+ ...targetBridge.workspaceConfirmationRequired ? { projectPath: null } : targetBridge.projectPath ? { projectPath: targetBridge.projectPath } : {}
27911
+ });
27912
+ targetBridge._lastLifecycle = lifecycleFromEnsureResult(r, targetBridge.runtimeMode);
27913
+ if (targetBridge.runtimeMode === "minimal") {
27914
+ targetBridge._lastPresentation = r.presentation ? { ...r.presentation, at: (/* @__PURE__ */ new Date()).toISOString() } : await targetBridge.applyRuntimePresentation("hide");
27915
+ }
27916
+ return r;
27820
27917
  });
27821
- targetBridge._lastLifecycle = lifecycleFromEnsureResult(r, targetBridge.runtimeMode);
27822
- if (targetBridge.runtimeMode === "minimal") {
27823
- targetBridge._lastPresentation = r.presentation ? { ...r.presentation, at: (/* @__PURE__ */ new Date()).toISOString() } : await targetBridge.applyRuntimePresentation("hide");
27824
- }
27825
- return r;
27826
27918
  }
27827
27919
  function toolErrorResult(error2) {
27828
27920
  const message = error2 instanceof Error ? error2.message : String(error2);
27829
- if (error2 && (error2.uiDiagnostic || error2.modelSelection || error2.pickerRead || error2.cdp)) {
27921
+ if (error2 && (error2.uiDiagnostic || error2.modelSelection || error2.pickerRead || error2.cdp || error2.workspaceRecovery)) {
27830
27922
  const payload = {
27831
27923
  error: { code: error2.code || error2.uiDiagnostic?.code || "CURSOR_BRIDGE_ERROR", message },
27832
27924
  ...error2.uiDiagnostic ? { uiDiagnostic: error2.uiDiagnostic } : {},
27833
27925
  ...error2.modelSelection ? { modelSelection: error2.modelSelection } : {},
27834
27926
  ...error2.pickerRead ? { pickerRead: error2.pickerRead } : {},
27835
- ...error2.cdp ? { cdp: error2.cdp } : {}
27927
+ ...error2.cdp ? { cdp: error2.cdp } : {},
27928
+ ...error2.workspaceRecovery ? { workspaceRecovery: error2.workspaceRecovery } : {}
27836
27929
  };
27837
27930
  return {
27838
27931
  content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
@@ -27853,11 +27946,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request2) => {
27853
27946
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
27854
27947
  }
27855
27948
  if (name === "cursor_context_engine" || name === "cursor_search" || name === "cursor_search_deep") {
27856
- const result = await bridge.contextEngine(String(args && args.query || ""), { requestContext: args && args.request_context });
27949
+ const result = await bridge.contextEngine(String(args && args.query || ""), { requestContext: args && args.request_context, workspacePath: args && args.workspace_path });
27857
27950
  return { content: [{ type: "text", text: String(result) }] };
27858
27951
  }
27859
27952
  if (name === "cursor_do") {
27860
27953
  const result = await bridge.doTask(String(args && args.prompt || ""), {
27954
+ workspacePath: args && args.workspace_path,
27861
27955
  requestContext: args && args.request_context,
27862
27956
  background: !args || args.background !== false,
27863
27957
  execution: args && args.execution,
@@ -28033,6 +28127,7 @@ export {
28033
28127
  normalizeCursorModelEffort,
28034
28128
  normalizeCursorRuntimeMode,
28035
28129
  normalizeDelegationMode,
28130
+ normalizeModelPickerModelText,
28036
28131
  normalizeModelPickerText,
28037
28132
  normalizeRequestContext,
28038
28133
  pathsOverlap,
@@ -10,7 +10,7 @@ const hostWorkspaceId = hostCwd.replace(/\\/g, "/").toLowerCase();
10
10
  export default createStdioMcpExtension({
11
11
  label: "Cursor Bridge",
12
12
  clientName: "pi-cursor-bridge",
13
- packageVersion: "0.2.4",
13
+ packageVersion: "0.2.6",
14
14
  serverName: "cursor-bridge",
15
15
  serverScript: join(packageRoot, "dist", "cursor-bridge.mjs"),
16
16
  cwd: hostCwd,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cursor-bridge",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Windows only. Use Cursor Context Engine and bounded, explicitly continuous Cursor Agent execution from the Pi coding agent.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -47,6 +47,6 @@
47
47
  },
48
48
  "piPackage": {
49
49
  "embeddedProduct": "Cursor Bridge",
50
- "embeddedProductVersion": "6.0.4"
50
+ "embeddedProductVersion": "6.0.6"
51
51
  }
52
52
  }
@@ -20,7 +20,7 @@ Call `cursor_context_engine` when one or more of these are true:
20
20
 
21
21
  Prefer one CCE investigation over starting an Explore subagent merely to locate or understand project code. Let Cursor choose focused or extended depth from what it discovers.
22
22
 
23
- When these semantic conditions match, make CCE the first project-discovery surface. Do not establish the answer through generic context-mode, grep, or blind local exploration before trying CCE. If Claude Code denies an initial context-mode collection call with a CCE routing message, call `cursor_context_engine` once instead of retrying another `ctx_*` tool. A failed, denied, unavailable, or `NOT_FOUND` CCE attempt releases this priority and allows a bounded local fallback.
23
+ When these semantic conditions match, make CCE the first project-discovery surface. Do not establish the answer through generic context-mode, grep, or blind local exploration before trying CCE. If Claude Code denies an initial context-mode collection call with a CCE routing message, call `cursor_context_engine` once instead of retrying another `ctx_*` tool. A failed, denied, unavailable, or `NOT_FOUND` CCE attempt releases this priority and allows a bounded local fallback. A pre-send workspace-confirmation error may be recoverable; handle it below before falling back.
24
24
 
25
25
  ## Keep deterministic work local
26
26
 
@@ -30,10 +30,28 @@ Do not call CCE when any of these apply:
30
30
  - The needed code is already present in the current context.
31
31
  - The work is a trivial single-file edit or only requires running tests, inspecting logs, checking a build, or examining Git state.
32
32
  - The question concerns external libraries, APIs, current documentation, or the public web.
33
- - The user opted out of Cursor, CCE is unavailable, or the workspace is not initialized.
33
+ - The user opted out of Cursor or CCE is unavailable.
34
34
 
35
35
  Do not submit the same lookup to CCE and another semantic system in parallel. Use a second evidence surface only to close a specific gap or verify a consequential claim.
36
36
 
37
+ ## Bind a known workspace before sending
38
+
39
+ Pass the intended absolute `workspace_path` to `cursor_context_engine` when the installed schema supports it. Use the host-provided workspace root/current-task cwd unless the request explicitly targets another project. The field asserts the target before sending; it never switches or registers a project. Do not infer the target from a repository basename or a saved default binding.
40
+
41
+ For an older schema without `workspace_path`, use `cursor_status` to verify the exact ready project path before sending. `initialized=true` only shows that a binding exists; it does not prove the current request is confirmed for the intended workspace. `cursor_init` is the only explicit operation that may initialize, register, or switch a workspace.
42
+
43
+ If no target is known, do not infer or add `workspace_path`, and never call `cursor_init` for a guessed path. Existing safe host bindings may still be used; a workspace-confirmation error without a known target cannot use this recovery and should be reported before any necessary local fallback.
44
+
45
+ ## Recover one known workspace
46
+
47
+ For a pre-send `WORKSPACE_CONFIRMATION_REQUIRED`, `WORKSPACE_INITIALIZATION_REQUIRED`, or `WORKSPACE_MISMATCH` error with a known target:
48
+
49
+ 1. Call `cursor_status`. Continue only when it confirms idle, no queued or blocking work, and `workspaceBusy=false` when that field is exposed.
50
+ 2. Call `cursor_init` with that exact absolute path. Require `ready=true`, `workspaceConfirmationRequired=false`, and the exact `projectPath`; in Agents Window also check `workspaceBinding.ok`, the local file-URI identity and workspace ID.
51
+ 3. Retry the original CCE query once.
52
+
53
+ Do not treat these errors as CCE unavailability before this recovery. Do not initialize when status is busy, identity is ambiguous, `needs_attention` is present, or send state is uncertain; explain the condition and use only the necessary local fallback. Do not cancel other work, change the model, or retry in a loop.
54
+
37
55
  ## Submit one natural-language intent
38
56
 
39
57
  Call `cursor_context_engine` once with the question's real intent. Include a known symbol, subsystem, or path only when it is a useful lead.
@@ -9,6 +9,14 @@ Use Cursor as an execution partner. Keep direction, scope decisions, risk owners
9
9
 
10
10
  Declare `request_context` for each call: an AI caller uses `sender="model"`; set `source="user"` for explicitly supplied user requirements, `"model"` for your own task/inference, or `"mixed"` when both appear. In mixed prompts, label user-confirmed requirements separately from your additions. Use `"unknown"` where provenance is unavailable; a user asking you to use Cursor does not make your authored message user-authored. These labels are declarations, not authentication or extra permission, and do not carry over to later session turns.
11
11
 
12
+ ## Bind a known workspace
13
+
14
+ For CCE or `cursor_do`, pass the intended absolute `workspace_path` when the installed tool schema supports it. Use the host-provided workspace root/current-task cwd unless the request explicitly targets another project. The field is a pre-send assertion: it never selects, switches, or registers a workspace. Do not infer the target from a repository basename or a saved default binding.
15
+
16
+ With an older schema, use `cursor_status` to verify the exact ready project path before sending. `initialized=true` only means a binding exists; it does not confirm the workspace for this request. `cursor_init` remains the only explicit initialization, registration, or switch operation.
17
+
18
+ On pre-send `WORKSPACE_CONFIRMATION_REQUIRED`, `WORKSPACE_INITIALIZATION_REQUIRED`, or `WORKSPACE_MISMATCH` with a known target, first call `cursor_status`. Only if it confirms idle, no queued or blocking work, and `workspaceBusy=false` when exposed, call `cursor_init` for the exact path. Require `ready=true`, `workspaceConfirmationRequired=false`, and the exact `projectPath`; in Agents Window also check `workspaceBinding.ok`, the local file-URI identity and workspace ID. Retry the original CCE query or `cursor_do` submission once. Do not run this recovery for `needs_attention`, uncertain send state, busy status, or ambiguous identity; report the condition and use the necessary local fallback. Do not cancel other work, change model settings, or retry in a loop.
19
+
12
20
  ## Respect execution controls
13
21
 
14
22
  - Do not call `cursor_do` when the user explicitly says not to use Cursor or not to delegate. A direct user opt-out always wins.
@@ -75,7 +83,7 @@ Do not choose parallel execution merely because there are many tasks. When depen
75
83
 
76
84
  1. Record the relevant pre-dispatch workspace state so later review can distinguish existing user changes.
77
85
  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` 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.
86
+ 3. Call `cursor_do` with `background=true` and save its compact submission receipt. Include `workspace_path` when the exact target is known and the schema supports it. 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
87
  4. Save each returned `task_id`; for persistent work also save `session_id`. Treat `agent_id` as verification evidence, not the continuation handle.
80
88
  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
89