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.
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/dist/cursor-bridge.mjs +1033 -242
- package/dist/cursor-lifecycle-supervisor.mjs +144 -18
- package/extensions/index.ts +1 -1
- package/package.json +2 -2
- package/skills/cce-routing/SKILL.md +2 -0
- package/skills/cursor-delegate/SKILL.md +7 -5
|
@@ -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
|
|
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
|
-
|
|
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
|
|
803
|
+
const launched = await spawnDetachedSafely(spawnImpl, exe, args, {
|
|
701
804
|
detached: true,
|
|
702
805
|
stdio: "ignore",
|
|
703
806
|
windowsHide: effectiveRuntimeMode === "minimal"
|
|
704
807
|
});
|
|
705
|
-
|
|
706
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1078
|
-
|
|
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:
|
|
1084
|
-
projectPath:
|
|
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
|
|
1093
|
-
requestProjectPath
|
|
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
|
|
1231
|
+
return await task;
|
|
1106
1232
|
} finally {
|
|
1107
|
-
ensureInflight
|
|
1233
|
+
if (ensureInflight.get(ensureKey) === task) ensureInflight.delete(ensureKey);
|
|
1108
1234
|
}
|
|
1109
1235
|
};
|
|
1110
1236
|
const server = net.createServer((socket) => {
|
package/extensions/index.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
75
|
-
5.
|
|
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
|
|