pi-cursor-bridge 0.1.4 → 0.1.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.
@@ -181,6 +181,8 @@ function startMinimalWindowGuard(pid, options = {}) {
181
181
  "-EncodedCommand",
182
182
  encodePowerShell(script)
183
183
  ], { detached: !lifetime, stdio: "ignore", windowsHide: true });
184
+ if (child && typeof child.once === "function") child.once("error", () => {
185
+ });
184
186
  if (!lifetime && child && typeof child.unref === "function") child.unref();
185
187
  return {
186
188
  started: true,
@@ -542,6 +544,58 @@ async function waitForCdp(maxMs = 3e4, stepMs = 1e3) {
542
544
  }
543
545
  return false;
544
546
  }
547
+ async function spawnDetachedSafely(spawnImpl, file, args, spawnOptions) {
548
+ let child;
549
+ try {
550
+ child = spawnImpl(file, args, spawnOptions);
551
+ } catch (error) {
552
+ return {
553
+ ok: false,
554
+ child: null,
555
+ error,
556
+ errorCode: error && typeof error === "object" && error.code != null ? String(error.code) : null
557
+ };
558
+ }
559
+ if (child && typeof child.once === "function") {
560
+ const startup = await new Promise((resolvePromise) => {
561
+ let settled = false;
562
+ const finish = (result) => {
563
+ if (settled) return;
564
+ settled = true;
565
+ child.off?.("spawn", onSpawn);
566
+ child.off?.("error", onError);
567
+ resolvePromise(result);
568
+ };
569
+ const onSpawn = () => finish({ ok: true });
570
+ const onError = (error) => finish({ ok: false, error });
571
+ child.once("spawn", onSpawn);
572
+ child.once("error", onError);
573
+ if (Number.isInteger(child.pid) && child.pid > 0) queueMicrotask(onSpawn);
574
+ });
575
+ if (!startup.ok) {
576
+ return {
577
+ ok: false,
578
+ child,
579
+ error: startup.error,
580
+ errorCode: startup.error && typeof startup.error === "object" && startup.error.code != null ? String(startup.error.code) : null
581
+ };
582
+ }
583
+ child.once("error", () => {
584
+ });
585
+ }
586
+ if (child && typeof child.unref === "function") child.unref();
587
+ return { ok: true, child };
588
+ }
589
+ function attachedPresentation(runtimeMode, port) {
590
+ if (runtimeMode !== "minimal") return null;
591
+ return {
592
+ supported: true,
593
+ applied: false,
594
+ action: "hide",
595
+ port,
596
+ reason: "attached lifecycle cannot start the PowerShell window guard under the current process policy"
597
+ };
598
+ }
545
599
  async function ensureCursorRunningLocal(options = {}) {
546
600
  const waitMs = Number(options.waitMs || 3e4);
547
601
  const runtimeMode = options.runtimeMode || "normal";
@@ -553,12 +607,14 @@ async function ensureCursorRunningLocal(options = {}) {
553
607
  const projectPath = Object.hasOwn(options, "projectPath") ? options.projectPath ? resolve2(String(options.projectPath)) : null : resolveProjectPath();
554
608
  const listCdpPageTargetsImpl = options.listCdpPageTargetsImpl || listCdpPageTargets;
555
609
  const spawnImpl = options.spawnImpl || spawn2;
610
+ const allowSpawn = options.allowSpawn !== false;
611
+ const allowProcessControl = options.allowProcessControl !== false;
556
612
  if (await cdpUpImpl()) {
557
613
  const isCursor = await cdpIsCursorImpl();
558
614
  if (isCursor) {
559
- const cursorPid2 = findCursorPidByPort(CDP_PORT);
560
- const windowGuard2 = effectiveRuntimeMode === "minimal" && cursorPid2 ? startMinimalWindowGuard(cursorPid2) : null;
561
- const presentation2 = effectiveRuntimeMode === "minimal" ? setCursorWindowPresentation({ action: "hide", port: CDP_PORT, pid: cursorPid2 }) : null;
615
+ const cursorPid2 = allowProcessControl ? findCursorPidByPort(CDP_PORT) : null;
616
+ const windowGuard2 = allowProcessControl && effectiveRuntimeMode === "minimal" && cursorPid2 ? startMinimalWindowGuard(cursorPid2) : null;
617
+ const presentation2 = effectiveRuntimeMode === "minimal" ? allowProcessControl ? setCursorWindowPresentation({ action: "hide", port: CDP_PORT, pid: cursorPid2 }) : attachedPresentation(effectiveRuntimeMode, CDP_PORT) : null;
562
618
  const currentTargets = await listCdpPageTargetsImpl();
563
619
  const projectKey = normalizeProjectKey(projectPath);
564
620
  let targetId2 = projectKey ? PROJECT_TARGETS.get(projectKey) || null : currentTargets[0] && currentTargets[0].id || null;
@@ -587,6 +643,22 @@ async function ensureCursorRunningLocal(options = {}) {
587
643
  }
588
644
  }
589
645
  if (projectPath && existsSync(projectPath) && !targetId2) {
646
+ if (!allowSpawn) {
647
+ return {
648
+ ok: false,
649
+ status: "workspace-not-ready",
650
+ port: CDP_PORT,
651
+ cursorPid: cursorPid2,
652
+ runtimeMode: effectiveRuntimeMode,
653
+ projectPath,
654
+ presentation: presentation2,
655
+ windowGuard: windowGuard2,
656
+ needsAction: "open_workspace_in_cursor",
657
+ retryable: true,
658
+ nextStep: `Open workspace ${projectPath} in the existing Cursor Agents Window, then retry the same operation.`,
659
+ message: `CCE connected to Cursor, but the current workspace target for ${projectPath} is not ready and the current lifecycle cannot open a new window.`
660
+ };
661
+ }
590
662
  const cursorExecutable2 = findCursorExeDetailsImpl();
591
663
  const exe2 = cursorExecutable2 && cursorExecutable2.path;
592
664
  if (!exe2) {
@@ -606,12 +678,28 @@ async function ensureCursorRunningLocal(options = {}) {
606
678
  };
607
679
  }
608
680
  const beforeTargetIds = new Set(currentTargets.map((target2) => target2.id));
609
- const opener = spawnImpl(exe2, ["--new-window", projectPath], {
681
+ const opened = await spawnDetachedSafely(spawnImpl, exe2, ["--new-window", projectPath], {
610
682
  detached: true,
611
683
  stdio: "ignore",
612
684
  windowsHide: effectiveRuntimeMode === "minimal"
613
685
  });
614
- opener.unref();
686
+ if (!opened.ok) {
687
+ return {
688
+ ok: false,
689
+ status: "spawn-blocked",
690
+ port: CDP_PORT,
691
+ cursorPid: cursorPid2,
692
+ runtimeMode: effectiveRuntimeMode,
693
+ projectPath,
694
+ presentation: presentation2,
695
+ windowGuard: windowGuard2,
696
+ errorCode: opened.errorCode,
697
+ needsAction: "open_workspace_in_cursor",
698
+ retryable: true,
699
+ nextStep: `Open workspace ${projectPath} in Cursor, then retry the same operation.`,
700
+ message: `Cursor Bridge could not open a new workspace window: ${opened.error instanceof Error ? opened.error.message : String(opened.error)}`
701
+ };
702
+ }
615
703
  workspaceAction = "opened-new-window";
616
704
  const openedTarget2 = await waitForNewCdpTarget(beforeTargetIds, 12e3, projectPath, listCdpPageTargetsImpl);
617
705
  if (!openedTarget2) {
@@ -660,6 +748,21 @@ async function ensureCursorRunningLocal(options = {}) {
660
748
  message: `CCE cannot connect to Cursor because required local port ${CDP_PORT} is occupied by another program.`
661
749
  };
662
750
  }
751
+ if (!allowSpawn) {
752
+ return {
753
+ ok: false,
754
+ status: "external-launch-required",
755
+ port: CDP_PORT,
756
+ cursorPid: null,
757
+ runtimeMode: effectiveRuntimeMode,
758
+ projectPath,
759
+ presentation: attachedPresentation(effectiveRuntimeMode, CDP_PORT),
760
+ needsAction: "launch_cursor_with_cdp",
761
+ retryable: true,
762
+ nextStep: `Start Cursor with its remote debugging connection on port ${CDP_PORT}, open ${projectPath || "the target workspace"}, then retry the same operation.`,
763
+ message: `Cursor is not reachable on the configured CDP port ${CDP_PORT}, and the current lifecycle policy cannot launch it.`
764
+ };
765
+ }
663
766
  if (cursorRunningImpl()) {
664
767
  const cursorExecutable2 = findCursorExeDetailsImpl();
665
768
  return {
@@ -697,13 +800,29 @@ async function ensureCursorRunningLocal(options = {}) {
697
800
  );
698
801
  }
699
802
  if (projectPath && existsSync(projectPath)) args.push(projectPath);
700
- const child = spawnImpl(exe, args, {
803
+ const launched = await spawnDetachedSafely(spawnImpl, exe, args, {
701
804
  detached: true,
702
805
  stdio: "ignore",
703
806
  windowsHide: effectiveRuntimeMode === "minimal"
704
807
  });
705
- child.unref();
706
- const startupWindowGuard = effectiveRuntimeMode === "minimal" ? startMinimalWindowGuard(child.pid) : null;
808
+ if (!launched.ok) {
809
+ return {
810
+ ok: false,
811
+ status: "spawn-blocked",
812
+ exe,
813
+ port: CDP_PORT,
814
+ cursorPid: null,
815
+ runtimeMode: effectiveRuntimeMode,
816
+ projectPath,
817
+ errorCode: launched.errorCode,
818
+ needsAction: "launch_cursor_manually",
819
+ retryable: true,
820
+ nextStep: `Start Cursor with its remote debugging connection on port ${CDP_PORT}, then retry the same operation.`,
821
+ message: `Cursor Bridge could not launch Cursor: ${launched.error instanceof Error ? launched.error.message : String(launched.error)}`
822
+ };
823
+ }
824
+ const child = launched.child;
825
+ const startupWindowGuard = effectiveRuntimeMode === "minimal" ? startMinimalWindowGuard(child && child.pid) : null;
707
826
  const up = await waitForCdp(waitMs);
708
827
  if (!up) {
709
828
  return {
@@ -1048,7 +1167,8 @@ async function startSupervisor(options = {}) {
1048
1167
  tryRemove(sock);
1049
1168
  }
1050
1169
  const ensureLocal = await loadEnsure(ensureModule);
1051
- let ensureInflight = null;
1170
+ const ensureInflight = /* @__PURE__ */ new Map();
1171
+ let ensureTail = Promise.resolve();
1052
1172
  let ensureCount = 0;
1053
1173
  let lastEnsure = null;
1054
1174
  const clients = /* @__PURE__ */ new Set();
@@ -1074,14 +1194,17 @@ async function startSupervisor(options = {}) {
1074
1194
  if (typeof idleTimer.unref === "function") idleTimer.unref();
1075
1195
  };
1076
1196
  const runEnsure = async (request = {}) => {
1077
- if (ensureInflight) return ensureInflight;
1078
- ensureInflight = (async () => {
1197
+ const requestRuntimeMode = request.runtimeMode || "normal";
1198
+ const requestProjectPath = Object.hasOwn(request, "projectPath") ? request.projectPath : null;
1199
+ const ensureKey = JSON.stringify([requestRuntimeMode, requestProjectPath]);
1200
+ if (ensureInflight.has(ensureKey)) return ensureInflight.get(ensureKey);
1201
+ const task = ensureTail.then(async () => {
1079
1202
  ensureCount += 1;
1080
1203
  const waitMs = Number(request.waitMs || 3e4);
1081
1204
  const result = await ensureLocal({
1082
1205
  waitMs,
1083
- runtimeMode: request.runtimeMode || "normal",
1084
- projectPath: Object.hasOwn(request, "projectPath") ? request.projectPath : null
1206
+ runtimeMode: requestRuntimeMode,
1207
+ projectPath: requestProjectPath
1085
1208
  });
1086
1209
  lastEnsure = {
1087
1210
  ...result,
@@ -1089,8 +1212,8 @@ async function startSupervisor(options = {}) {
1089
1212
  at: (/* @__PURE__ */ new Date()).toISOString(),
1090
1213
  requestReason: request.reason || null,
1091
1214
  requestAdapterPid: request.adapterPid || null,
1092
- requestRuntimeMode: request.runtimeMode || "normal",
1093
- requestProjectPath: request.projectPath || null
1215
+ requestRuntimeMode,
1216
+ requestProjectPath
1094
1217
  };
1095
1218
  writeSupervisorDiag(logPath, "ensure-result", {
1096
1219
  ok: !!result.ok,
@@ -1100,11 +1223,14 @@ async function startSupervisor(options = {}) {
1100
1223
  ensureCount
1101
1224
  });
1102
1225
  return lastEnsure;
1103
- })();
1226
+ });
1227
+ ensureInflight.set(ensureKey, task);
1228
+ ensureTail = task.catch(() => {
1229
+ });
1104
1230
  try {
1105
- return await ensureInflight;
1231
+ return await task;
1106
1232
  } finally {
1107
- ensureInflight = null;
1233
+ if (ensureInflight.get(ensureKey) === task) ensureInflight.delete(ensureKey);
1108
1234
  }
1109
1235
  };
1110
1236
  const server = net.createServer((socket) => {
@@ -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.3",
13
+ packageVersion: "0.1.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.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Use Cursor Context Engine and bounded 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.5.0"
50
+ "embeddedProductVersion": "5.6.1"
51
51
  }
52
52
  }
@@ -5,6 +5,8 @@ description: "Use Cursor Bridge's read-only cursor_context_engine for unfamiliar
5
5
 
6
6
  # CCE Routing
7
7
 
8
+ `cursor_context_engine` automatically inherits the persistent CCE model and reasoning-effort default configured through `cursor_model`. Do not change or reset that default unless the user explicitly asks; if selection cannot be confirmed, report the pre-send failure instead of retrying with Auto.
9
+
8
10
  Route project-understanding questions to the cheapest evidence surface that can answer them without guessing.
9
11
 
10
12
  ## Choose CCE for project semantics
@@ -7,12 +7,13 @@ description: "Delegate bounded light-to-medium implementation, limited investiga
7
7
 
8
8
  Use Cursor as an execution partner. Keep direction, scope decisions, risk ownership, result review, and final verification with the primary agent.
9
9
 
10
- ## Respect execution controls
10
+ ## Respect execution controls
11
11
 
12
12
  - 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.
13
13
  - If `cursor_do` is unavailable, or `cursor_status` reports delegation as disabled, do not bypass the setting, repeatedly retry, or ask Cursor to re-enable itself. Complete the work in the primary agent.
14
14
  - Treat `CURSOR_BRIDGE_DELEGATION=off` as an administrator-level host switch. It disables delegated execution but does not by itself disable `cursor_context_engine`, `cursor_init`, or `cursor_status`.
15
- - Cursor Bridge exposes one fixed delegation contract. Do not invent participation levels, call-frequency controls, or slash commands.
15
+ - Cursor Bridge exposes one fixed delegation contract. Do not invent participation levels, call-frequency controls, or slash commands.
16
+ - `cursor_model` owns persistent model and reasoning-effort defaults. Call its `set` or `reset` action only when the user explicitly asks to change those defaults; ordinary delegation must inherit the stored `cursor_do` choice without silently changing it.
16
17
 
17
18
  ## Follow the default workflow
18
19
 
@@ -70,9 +71,10 @@ The envelope may contain a small number of local implementation `open_questions`
70
71
 
71
72
  1. Always query `cursor_status(task_id)` for the exact task. Do not treat the currently visible Cursor chat as task identity.
72
73
  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.
73
- 3. Compare Cursor's claimed work with the real diff, `allowed_paths`, and acceptance contract.
74
- 4. Run risk-proportionate verification in the primary agent. Cursor's response alone cannot support a formal pass, verified state, or governance transition.
75
- 5. Record each task as complete, partial, failed, timed out, or ambiguous before summarizing the batch.
74
+ 3. Compare Cursor's claimed work with the real diff, `allowed_paths`, and acceptance contract.
75
+ 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.
76
+ 5. Run risk-proportionate verification in the primary agent. Cursor's response alone cannot support a formal pass, verified state, or governance transition.
77
+ 6. Record each task as complete, partial, failed, timed out, or ambiguous before summarizing the batch.
76
78
 
77
79
  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.
78
80