pi-cursor-bridge 0.1.12 → 0.1.13
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 +192 -65
- package/extensions/index.ts +1 -1
- package/package.json +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-bridge",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.9.0+codex.20260905074950",
|
|
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.
|
|
13
|
+
This Pi package embeds Cursor Bridge 5.9.0. 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)
|
package/dist/cursor-bridge.mjs
CHANGED
|
@@ -22604,7 +22604,7 @@ function updateCursorSessionRegistry(filePath, mutator, options = {}) {
|
|
|
22604
22604
|
// server.mjs
|
|
22605
22605
|
init_cursor_ensure_core();
|
|
22606
22606
|
init_lifecycle_paths();
|
|
22607
|
-
var PLUGIN_VERSION = "5.
|
|
22607
|
+
var PLUGIN_VERSION = "5.9.0";
|
|
22608
22608
|
var CDP_PORT2 = Number(process.env.CURSOR_BRIDGE_CDP_PORT || 9223);
|
|
22609
22609
|
var ORIGIN = `http://localhost:${CDP_PORT2}`;
|
|
22610
22610
|
var QUERY_TIMEOUT = Number(process.env.CURSOR_BRIDGE_TIMEOUT || 3e5);
|
|
@@ -23690,6 +23690,7 @@ var CursorBridge = class {
|
|
|
23690
23690
|
this.projectPath = options.projectPath !== void 0 ? resolve7(String(options.projectPath)) : persistedWorkspace && persistedWorkspace.projectPath || null;
|
|
23691
23691
|
this.workspaceSource = options.projectPath !== void 0 ? "constructor" : persistedWorkspace ? "persistent_init" : "auto_detect";
|
|
23692
23692
|
this.workspaceUpdatedAt = persistedWorkspace && persistedWorkspace.updatedAt || null;
|
|
23693
|
+
this.workspaceConfirmationRequired = this.workspaceKey === "default" && !!persistedWorkspace;
|
|
23693
23694
|
this.modelPreferencesFile = options.modelPreferencesFile === null ? null : resolve7(options.modelPreferencesFile || resolveCursorModelPreferencesFile());
|
|
23694
23695
|
this.modelPreferences = readCursorModelPreferences(this.modelPreferencesFile);
|
|
23695
23696
|
this.sessionFile = options.sessionFile === null ? null : resolve7(options.sessionFile || resolveCursorSessionRegistryFile());
|
|
@@ -23712,6 +23713,8 @@ var CursorBridge = class {
|
|
|
23712
23713
|
const resolvedProjectPath = this.projectPath || this._lastLifecycle && this._lastLifecycle.projectPath || null;
|
|
23713
23714
|
return {
|
|
23714
23715
|
workspaceKey: this.workspaceKey,
|
|
23716
|
+
workspaceConfirmationRequired: this.workspaceConfirmationRequired,
|
|
23717
|
+
workspaceBindingWarning: this.workspaceConfirmationRequired ? "This adapter has no host workspace identity. Run cursor_init with the intended project before using its shared saved binding." : null,
|
|
23715
23718
|
projectPath: resolvedProjectPath,
|
|
23716
23719
|
workspaceSource: this.projectPath ? this.workspaceSource : resolvedProjectPath ? "host_auto_detect" : this.workspaceSource,
|
|
23717
23720
|
workspaceUpdatedAt: this.workspaceUpdatedAt,
|
|
@@ -23746,7 +23749,10 @@ var CursorBridge = class {
|
|
|
23746
23749
|
agentLabel: session.agentLabel || null,
|
|
23747
23750
|
turnIndex: Number(session.turnIndex || 0),
|
|
23748
23751
|
activeTaskId: session.activeTaskId || null,
|
|
23749
|
-
lastTask: session.lastTask
|
|
23752
|
+
lastTask: session.lastTask ? {
|
|
23753
|
+
...session.lastTask,
|
|
23754
|
+
resultUnavailable: this.tasks.get(session.lastTask.taskId)?.result == null
|
|
23755
|
+
} : null,
|
|
23750
23756
|
modelPreference: session.modelPreference || null,
|
|
23751
23757
|
readOnly: session.scopeEnvelope && session.scopeEnvelope.readOnly === true,
|
|
23752
23758
|
allowedPaths: session.scopeEnvelope && Array.isArray(session.scopeEnvelope.allowedPaths) ? session.scopeEnvelope.allowedPaths : [],
|
|
@@ -23758,6 +23764,7 @@ var CursorBridge = class {
|
|
|
23758
23764
|
};
|
|
23759
23765
|
}
|
|
23760
23766
|
_claimNewSession({ taskId, projectPath, readOnly, allowedPaths, modelPreference, requestId, timeoutMs }) {
|
|
23767
|
+
this._ensureTaskCapacity();
|
|
23761
23768
|
if (!this.sessionFile) throw cursorSessionError("SESSION_STORAGE_DISABLED", "persistent session storage is disabled");
|
|
23762
23769
|
const sessionId = createCursorSessionId();
|
|
23763
23770
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -23803,6 +23810,7 @@ var CursorBridge = class {
|
|
|
23803
23810
|
if (requestId && session.lastRequestId === requestId) {
|
|
23804
23811
|
return { duplicate: true, session: { ...session } };
|
|
23805
23812
|
}
|
|
23813
|
+
this._ensureTaskCapacity();
|
|
23806
23814
|
if (!sameSessionProject(session.projectPath, projectPath)) {
|
|
23807
23815
|
throw cursorSessionError("SESSION_WORKSPACE_MISMATCH", `session is bound to ${session.projectPath}`);
|
|
23808
23816
|
}
|
|
@@ -23822,6 +23830,15 @@ var CursorBridge = class {
|
|
|
23822
23830
|
if (session.state !== "ready") {
|
|
23823
23831
|
throw cursorSessionError("SESSION_NOT_READY", `state=${session.state}; recovery=${session.recoveryState || "none"}`);
|
|
23824
23832
|
}
|
|
23833
|
+
if (session.lastTask?.status === "completed" && !session.lastTask.resultCollectedAt && this.tasks.has(session.lastTask.taskId)) {
|
|
23834
|
+
throw cursorSessionError("SESSION_RESULT_UNCOLLECTED", `read cursor_status(task_id=${session.lastTask.taskId}) before continuing`);
|
|
23835
|
+
}
|
|
23836
|
+
if (session.lastTask?.status === "completed" && !session.lastTask.resultCollectedAt && !this.tasks.has(session.lastTask.taskId) && session.recoveryState !== "reconciled_result_uncollected") {
|
|
23837
|
+
throw cursorSessionError("SESSION_RECONCILE_REQUIRED", "the prior reply was not read before this adapter restarted; reconcile the exact Agent before continuing");
|
|
23838
|
+
}
|
|
23839
|
+
if (session.recoveryState === "reconciled_result_uncollected" && (Number(session.turnIndex) <= 1 || session.responseBaseline)) {
|
|
23840
|
+
throw cursorSessionError("SESSION_RESULT_UNCOLLECTED", "collect the interrupted reply with cursor_session_control(action=collect_result) before continuing");
|
|
23841
|
+
}
|
|
23825
23842
|
if (!session.agentId) throw cursorSessionError("SESSION_AGENT_NOT_BOUND", sessionId);
|
|
23826
23843
|
const envelope = session.scopeEnvelope || { readOnly: false, allowedPaths: [] };
|
|
23827
23844
|
if (envelope.readOnly === true) {
|
|
@@ -23841,6 +23858,7 @@ var CursorBridge = class {
|
|
|
23841
23858
|
session.epoch = epoch;
|
|
23842
23859
|
session.activeTaskId = taskId;
|
|
23843
23860
|
session.lastRequestId = requestId || null;
|
|
23861
|
+
delete session.responseBaseline;
|
|
23844
23862
|
session.lease = {
|
|
23845
23863
|
taskId,
|
|
23846
23864
|
instanceId: this.sessionInstanceId,
|
|
@@ -23869,6 +23887,20 @@ var CursorBridge = class {
|
|
|
23869
23887
|
});
|
|
23870
23888
|
job.sessionState = "busy";
|
|
23871
23889
|
}
|
|
23890
|
+
_persistSessionResponseBaseline(job) {
|
|
23891
|
+
if (!job.sessionId) return;
|
|
23892
|
+
const baseline = job.responseBaseline;
|
|
23893
|
+
if (!baseline || !["messageCount", "replyLength", "replyHash"].every((key) => Number.isFinite(baseline[key]))) {
|
|
23894
|
+
throw cursorSessionError("SESSION_BASELINE_UNAVAILABLE", "a numeric response baseline is required before sending");
|
|
23895
|
+
}
|
|
23896
|
+
updateCursorSessionRegistry(this.sessionFile, (registry2) => {
|
|
23897
|
+
const session = registry2.sessions[job.sessionId];
|
|
23898
|
+
if (!session || session.activeTaskId !== job.id || Number(session.epoch) !== Number(job.sessionEpoch)) {
|
|
23899
|
+
throw cursorSessionError("SESSION_STALE_SENDER", "the sender no longer owns this session turn");
|
|
23900
|
+
}
|
|
23901
|
+
session.responseBaseline = { messageCount: baseline.messageCount, replyLength: baseline.replyLength, replyHash: baseline.replyHash };
|
|
23902
|
+
});
|
|
23903
|
+
}
|
|
23872
23904
|
_settleSessionJob(job, outcome, options = {}) {
|
|
23873
23905
|
if (!job || !job.sessionId || !this.sessionFile) return;
|
|
23874
23906
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -23896,6 +23928,7 @@ var CursorBridge = class {
|
|
|
23896
23928
|
}
|
|
23897
23929
|
session.activeTaskId = null;
|
|
23898
23930
|
session.lease = null;
|
|
23931
|
+
if (outcome !== "completed") delete session.responseBaseline;
|
|
23899
23932
|
session.recoveryState = null;
|
|
23900
23933
|
session.attention = null;
|
|
23901
23934
|
session.state = session.agentId ? "ready" : "failed";
|
|
@@ -23910,6 +23943,19 @@ var CursorBridge = class {
|
|
|
23910
23943
|
job.sessionError = error2 instanceof Error ? error2.message : String(error2);
|
|
23911
23944
|
}
|
|
23912
23945
|
}
|
|
23946
|
+
_markTaskResultCollected(job) {
|
|
23947
|
+
if (!isTerminalTask(job) || job.result == null || job.resultCollectedAt) return;
|
|
23948
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
23949
|
+
if (job.sessionId && this.sessionFile) {
|
|
23950
|
+
updateCursorSessionRegistry(this.sessionFile, (registry2) => {
|
|
23951
|
+
const session = registry2.sessions[job.sessionId];
|
|
23952
|
+
if (!session || session.lastTask?.taskId !== job.id || Number(session.epoch) !== Number(job.sessionEpoch)) return;
|
|
23953
|
+
session.lastTask.resultCollectedAt = now;
|
|
23954
|
+
delete session.responseBaseline;
|
|
23955
|
+
});
|
|
23956
|
+
}
|
|
23957
|
+
job.resultCollectedAt = now;
|
|
23958
|
+
}
|
|
23913
23959
|
sessionStatus(sessionId) {
|
|
23914
23960
|
const session = this._readSession(sessionId);
|
|
23915
23961
|
return {
|
|
@@ -23918,17 +23964,28 @@ var CursorBridge = class {
|
|
|
23918
23964
|
...session ? this._sessionView(session) : { sessionId: String(sessionId || "") }
|
|
23919
23965
|
};
|
|
23920
23966
|
}
|
|
23921
|
-
async _reconcileSession(sessionId) {
|
|
23967
|
+
async _reconcileSession(sessionId, { collectResult = false } = {}) {
|
|
23968
|
+
const action = collectResult ? "collect_result" : "reconcile";
|
|
23922
23969
|
const initial = this._readSession(sessionId);
|
|
23923
|
-
if (!initial) return { found: false, action
|
|
23924
|
-
if (
|
|
23925
|
-
return { found: true, changed: false, action
|
|
23970
|
+
if (!initial) return { found: false, action, sessionId };
|
|
23971
|
+
if (collectResult && initial.recoveryState === "reconciled_result_collected") {
|
|
23972
|
+
return { found: true, changed: false, action, ...this._sessionView(initial), state: "already_collected", resultPersisted: false };
|
|
23973
|
+
}
|
|
23974
|
+
const unreadAfterRestart = initial.lastTask?.status === "completed" && !initial.lastTask.resultCollectedAt && !this.tasks.has(initial.lastTask.taskId);
|
|
23975
|
+
if (!collectResult && initial.state === "ready" && !unreadAfterRestart || initial.state === "closed" || initial.state === "failed") {
|
|
23976
|
+
return { found: true, changed: false, action, ...this._sessionView(initial) };
|
|
23977
|
+
}
|
|
23978
|
+
if (collectResult && (initial.state !== "ready" || initial.activeTaskId || initial.lastTask?.status !== "completed" || initial.recoveryState !== "reconciled_result_uncollected")) {
|
|
23979
|
+
throw cursorSessionError("SESSION_RESULT_NOT_READY", "reconcile a completed turn before collecting its reply");
|
|
23980
|
+
}
|
|
23981
|
+
if (collectResult && Number(initial.turnIndex) > 1 && !initial.responseBaseline) {
|
|
23982
|
+
throw cursorSessionError("SESSION_RESULT_BASELINE_UNAVAILABLE", "this older interrupted turn has no reply signature; inspect the original Agent manually before continuing");
|
|
23926
23983
|
}
|
|
23927
23984
|
if (!initial.agentId) {
|
|
23928
23985
|
return {
|
|
23929
23986
|
found: true,
|
|
23930
23987
|
changed: false,
|
|
23931
|
-
action
|
|
23988
|
+
action,
|
|
23932
23989
|
state: "agent_missing",
|
|
23933
23990
|
attention: "No exact agentId is available. Confirm the Cursor task state manually before explicit abandon.",
|
|
23934
23991
|
...this._sessionView(initial)
|
|
@@ -23946,21 +24003,44 @@ var CursorBridge = class {
|
|
|
23946
24003
|
effectiveExecution: "parallel_agent",
|
|
23947
24004
|
lastRecoveryAt: null,
|
|
23948
24005
|
recoveryState: null,
|
|
23949
|
-
error: null
|
|
24006
|
+
error: null,
|
|
24007
|
+
responseBaseline: initial.responseBaseline || null
|
|
23950
24008
|
};
|
|
23951
24009
|
const observed = await this._readStableParallelEntry(probe);
|
|
24010
|
+
let collectedReply;
|
|
24011
|
+
if (collectResult) {
|
|
24012
|
+
if (!observed.stable || !observed.entry || observed.entry.showSpinner || classifyParallelTerminalIcon(observed.entry.icon) !== "completed") {
|
|
24013
|
+
throw cursorSessionError("SESSION_RESULT_NOT_READY", "the exact Agent must have a stable completed state");
|
|
24014
|
+
}
|
|
24015
|
+
try {
|
|
24016
|
+
collectedReply = await this._withUiLock(() => this._collectParallelAgent(probe));
|
|
24017
|
+
} catch (error2) {
|
|
24018
|
+
if (probe.uiDiagnostic && error2 && typeof error2 === "object") error2.uiDiagnostic = probe.uiDiagnostic;
|
|
24019
|
+
throw error2;
|
|
24020
|
+
}
|
|
24021
|
+
if (!String(collectedReply || "").trim()) throw cursorSessionError("SESSION_RESULT_UNAVAILABLE", "no final reply was collected");
|
|
24022
|
+
}
|
|
23952
24023
|
let result;
|
|
23953
24024
|
updateCursorSessionRegistry(this.sessionFile, (registry2) => {
|
|
23954
24025
|
const session = registry2.sessions[sessionId];
|
|
23955
24026
|
if (!session) {
|
|
23956
|
-
result = { found: false, action
|
|
24027
|
+
result = { found: false, action, sessionId };
|
|
23957
24028
|
return;
|
|
23958
24029
|
}
|
|
23959
|
-
if (Number(session.epoch || 0) !== Number(initial.epoch || 0) || session.activeTaskId !== initial.activeTaskId) {
|
|
23960
|
-
result = { found: true, changed: false, action
|
|
24030
|
+
if (Number(session.epoch || 0) !== Number(initial.epoch || 0) || session.activeTaskId !== initial.activeTaskId || session.state !== initial.state) {
|
|
24031
|
+
result = { found: true, changed: false, action, state: "stale_observation", ...this._sessionView(session) };
|
|
23961
24032
|
return;
|
|
23962
24033
|
}
|
|
23963
24034
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
24035
|
+
if (collectResult) {
|
|
24036
|
+
session.lastTask.resultCollectedAt = now;
|
|
24037
|
+
delete session.responseBaseline;
|
|
24038
|
+
session.recoveryState = "reconciled_result_collected";
|
|
24039
|
+
session.attention = null;
|
|
24040
|
+
session.updatedAt = now;
|
|
24041
|
+
result = { found: true, changed: true, action, ...this._sessionView(session), result: collectedReply, resultPersisted: false, uiDiagnostic: probe.uiDiagnostic || null };
|
|
24042
|
+
return;
|
|
24043
|
+
}
|
|
23964
24044
|
if (!observed.stable || !observed.entry) {
|
|
23965
24045
|
session.state = "needs_attention";
|
|
23966
24046
|
session.recoveryState = probe.recoveryState || "history_unavailable";
|
|
@@ -24000,7 +24080,8 @@ var CursorBridge = class {
|
|
|
24000
24080
|
session.lease = null;
|
|
24001
24081
|
session.state = "ready";
|
|
24002
24082
|
session.recoveryState = terminalClass === "completed" ? "reconciled_result_uncollected" : null;
|
|
24003
|
-
session.attention = terminalClass === "completed" ? "The interrupted turn completed in Cursor, but its reply was not persisted.
|
|
24083
|
+
session.attention = terminalClass === "completed" ? Number(session.turnIndex) > 1 && !session.responseBaseline ? "This older interrupted turn has no saved reply signature. Inspect the original Agent manually before continuing; automatic collection cannot identify its reply safely." : "The interrupted turn completed in Cursor, but its reply was not persisted. Use cursor_session_control(action=collect_result) before the next turn." : null;
|
|
24084
|
+
if (terminalClass !== "completed") delete session.responseBaseline;
|
|
24004
24085
|
session.updatedAt = now;
|
|
24005
24086
|
result = { found: true, changed: true, action: "reconcile", state: terminalClass, ...this._sessionView(session) };
|
|
24006
24087
|
});
|
|
@@ -24010,10 +24091,11 @@ var CursorBridge = class {
|
|
|
24010
24091
|
const id = String(sessionId || "").trim();
|
|
24011
24092
|
if (!id) throw cursorSessionError("SESSION_REQUIRED", "session_id must not be empty");
|
|
24012
24093
|
const action = String(options.action || "").trim().toLowerCase();
|
|
24013
|
-
if (!["reconcile", "close", "forget", "abandon"].includes(action)) {
|
|
24014
|
-
throw cursorSessionError("SESSION_ACTION_UNSUPPORTED", "expected reconcile, close, forget, or abandon");
|
|
24094
|
+
if (!["reconcile", "collect_result", "close", "forget", "abandon"].includes(action)) {
|
|
24095
|
+
throw cursorSessionError("SESSION_ACTION_UNSUPPORTED", "expected reconcile, collect_result, close, forget, or abandon");
|
|
24015
24096
|
}
|
|
24016
24097
|
if (action === "reconcile") return this._reconcileSession(id);
|
|
24098
|
+
if (action === "collect_result") return this._reconcileSession(id, { collectResult: true });
|
|
24017
24099
|
let result;
|
|
24018
24100
|
updateCursorSessionRegistry(this.sessionFile, (registry2) => {
|
|
24019
24101
|
const session = registry2.sessions[id];
|
|
@@ -24071,6 +24153,7 @@ var CursorBridge = class {
|
|
|
24071
24153
|
}
|
|
24072
24154
|
const previousProjectPath = this.projectPath || this._lastLifecycle && this._lastLifecycle.projectPath || null;
|
|
24073
24155
|
const saved = writeWorkspaceBinding(this.workspaceFile, this.workspaceKey, projectPath);
|
|
24156
|
+
this.workspaceConfirmationRequired = false;
|
|
24074
24157
|
this.projectPath = saved.projectPath;
|
|
24075
24158
|
this.workspaceSource = "persistent_init";
|
|
24076
24159
|
this.workspaceUpdatedAt = saved.updatedAt;
|
|
@@ -24265,6 +24348,7 @@ var CursorBridge = class {
|
|
|
24265
24348
|
const text = String(query || "").trim();
|
|
24266
24349
|
if (!text) throw new Error("query must not be empty");
|
|
24267
24350
|
if (text.length > 2e4) throw new Error("query exceeds the 20,000-character limit");
|
|
24351
|
+
this._assertWorkspaceConfirmed();
|
|
24268
24352
|
if (this._hasGlobalReservation()) {
|
|
24269
24353
|
throw new Error("A global Cursor reservation has an unconfirmed Stop state; resolve blockingTaskIds from cursor_status first");
|
|
24270
24354
|
}
|
|
@@ -24277,7 +24361,9 @@ var CursorBridge = class {
|
|
|
24277
24361
|
allowedPaths: [],
|
|
24278
24362
|
modelPreference: this._modelPreferenceFor("cce")
|
|
24279
24363
|
});
|
|
24280
|
-
|
|
24364
|
+
const result = normalizeCceSearchResult(await job.promise);
|
|
24365
|
+
this._markTaskResultCollected(job);
|
|
24366
|
+
return result;
|
|
24281
24367
|
}
|
|
24282
24368
|
// Unlisted compatibility aliases for clients that cached the pre-3.0 tool surface.
|
|
24283
24369
|
async search(query) {
|
|
@@ -24293,6 +24379,7 @@ var CursorBridge = class {
|
|
|
24293
24379
|
const text = String(prompt || "").trim();
|
|
24294
24380
|
if (!text) throw new Error("prompt must not be empty");
|
|
24295
24381
|
if (text.length > 1e5) throw new Error("prompt exceeds the 100,000-character limit");
|
|
24382
|
+
this._assertWorkspaceConfirmed();
|
|
24296
24383
|
if (this._hasGlobalReservation()) {
|
|
24297
24384
|
throw new Error("A global Cursor reservation has an unconfirmed Stop state; no new task may be submitted until it is explicitly recovered or released");
|
|
24298
24385
|
}
|
|
@@ -24428,6 +24515,7 @@ var CursorBridge = class {
|
|
|
24428
24515
|
return `cursor-${Date.now().toString(36)}-${this.nextTaskId++}`;
|
|
24429
24516
|
}
|
|
24430
24517
|
_enqueue(kind, prompt, options) {
|
|
24518
|
+
this._ensureTaskCapacity();
|
|
24431
24519
|
const id = options.taskId || this._nextTaskId();
|
|
24432
24520
|
let resolvePromise;
|
|
24433
24521
|
let rejectPromise;
|
|
@@ -24471,6 +24559,8 @@ var CursorBridge = class {
|
|
|
24471
24559
|
targetUiFlavor: null,
|
|
24472
24560
|
fallbackReason: null,
|
|
24473
24561
|
result: null,
|
|
24562
|
+
resultCollectedAt: null,
|
|
24563
|
+
waitDeadlineAt: null,
|
|
24474
24564
|
error: null,
|
|
24475
24565
|
cancelRequested: false,
|
|
24476
24566
|
cancelReason: null,
|
|
@@ -25613,6 +25703,7 @@ var CursorBridge = class {
|
|
|
25613
25703
|
await this._applyModelPreference(c, job.modelPreference, job);
|
|
25614
25704
|
this._throwIfCancelledBeforeSend(job);
|
|
25615
25705
|
job.responseBaseline = await this._captureSessionResponseBaseline(c);
|
|
25706
|
+
this._persistSessionResponseBaseline(job);
|
|
25616
25707
|
const providerErrorBaseline = providerErrorSignature(await this._readProviderError(c));
|
|
25617
25708
|
const filled = await this._fillPrompt(c, job.prompt, job);
|
|
25618
25709
|
if (filled === "NO_INPUT") await this._throwChatPanelUnavailableAfterNoInput(c);
|
|
@@ -25687,8 +25778,16 @@ var CursorBridge = class {
|
|
|
25687
25778
|
job.monitorPromise = monitorPromise;
|
|
25688
25779
|
return true;
|
|
25689
25780
|
}
|
|
25781
|
+
_taskWaitDeadline(job, timeoutMs = job?.timeoutMs) {
|
|
25782
|
+
if (!job) return Date.now() + timeoutMs;
|
|
25783
|
+
if (!Number.isFinite(job.waitDeadlineAt)) {
|
|
25784
|
+
const sentAt = Date.parse(job.sentAt || "");
|
|
25785
|
+
job.waitDeadlineAt = (Number.isFinite(sentAt) ? sentAt : Date.now()) + timeoutMs;
|
|
25786
|
+
}
|
|
25787
|
+
return job.waitDeadlineAt;
|
|
25788
|
+
}
|
|
25690
25789
|
async _monitorParallelAgent(job, generation) {
|
|
25691
|
-
const
|
|
25790
|
+
const deadline = this._taskWaitDeadline(job);
|
|
25692
25791
|
let sawGenerating = false;
|
|
25693
25792
|
let completedStable = 0;
|
|
25694
25793
|
let missingPolls = 0;
|
|
@@ -25697,7 +25796,7 @@ var CursorBridge = class {
|
|
|
25697
25796
|
let lastTerminalErrorSignature = "";
|
|
25698
25797
|
let collectionAttempts = 0;
|
|
25699
25798
|
let lastCollectionError = "";
|
|
25700
|
-
while (Date.now()
|
|
25799
|
+
while (Date.now() < deadline) {
|
|
25701
25800
|
if (!this._monitorOwns(job, generation)) return;
|
|
25702
25801
|
await sleep2(1400);
|
|
25703
25802
|
if (!this._monitorOwns(job, generation)) return;
|
|
@@ -25815,51 +25914,63 @@ var CursorBridge = class {
|
|
|
25815
25914
|
}
|
|
25816
25915
|
throw new Error(`After opening ${agentId}, it could not be confirmed as the selected Agent`);
|
|
25817
25916
|
}
|
|
25917
|
+
async _withRestoredAgentSelection(c, job, operation) {
|
|
25918
|
+
const entries = await this._readAgentEntries(c, true);
|
|
25919
|
+
const previousSelectedId = (entries.find((entry) => entry.isSelected) || {}).id || null;
|
|
25920
|
+
try {
|
|
25921
|
+
return await operation();
|
|
25922
|
+
} finally {
|
|
25923
|
+
if (previousSelectedId && previousSelectedId !== job.agentId) {
|
|
25924
|
+
try {
|
|
25925
|
+
const opened = await this._requestExactAgentSelection(c, previousSelectedId);
|
|
25926
|
+
if (opened !== "OPENED") throw new Error(`Unable to restore ${previousSelectedId}: ${opened}`);
|
|
25927
|
+
await this._waitForSelectedAgent(c, previousSelectedId);
|
|
25928
|
+
} catch (error2) {
|
|
25929
|
+
job.uiDiagnostic = { ...job.uiDiagnostic, selectionRestore: { state: "failed", agentId: previousSelectedId, detail: String(error2.message).slice(0, 500) } };
|
|
25930
|
+
}
|
|
25931
|
+
}
|
|
25932
|
+
}
|
|
25933
|
+
}
|
|
25818
25934
|
async _collectParallelAgent(job) {
|
|
25819
25935
|
const page = await findPage({ targetId: job.targetId, purpose: "parallel_agent" });
|
|
25820
25936
|
const c = makeClient(page.webSocketDebuggerUrl);
|
|
25821
25937
|
await c.ready;
|
|
25822
|
-
let previousSelectedId = null;
|
|
25823
25938
|
try {
|
|
25824
|
-
|
|
25825
|
-
|
|
25826
|
-
|
|
25827
|
-
|
|
25828
|
-
|
|
25829
|
-
|
|
25830
|
-
|
|
25831
|
-
|
|
25832
|
-
|
|
25833
|
-
|
|
25834
|
-
|
|
25835
|
-
|
|
25836
|
-
|
|
25837
|
-
|
|
25838
|
-
|
|
25839
|
-
|
|
25840
|
-
|
|
25841
|
-
|
|
25842
|
-
|
|
25843
|
-
|
|
25844
|
-
|
|
25845
|
-
|
|
25846
|
-
|
|
25939
|
+
return await this._withRestoredAgentSelection(c, job, async () => {
|
|
25940
|
+
const opened = await evalJS(c, exprOpenAgent(job.agentId));
|
|
25941
|
+
if (opened !== "OPENED") throw new Error(`Unable to open ${job.agentId}: ${opened}`);
|
|
25942
|
+
await this._closeHistory(c);
|
|
25943
|
+
await this._waitForSelectedAgent(c, job.agentId);
|
|
25944
|
+
let answer = "";
|
|
25945
|
+
let lastKey = "";
|
|
25946
|
+
let stable = 0;
|
|
25947
|
+
for (let i = 0; i < 80; i++) {
|
|
25948
|
+
const snap = await this._readResponseSnapshot(c);
|
|
25949
|
+
const candidate = String(await evalJS(c, EXPR_EXTRACT) || "").trim();
|
|
25950
|
+
const hasNewTurn = isSessionTurnReplyReady(job.responseBaseline, snap);
|
|
25951
|
+
if (candidate && hasNewTurn && Number(snap.stop || 0) === 0) {
|
|
25952
|
+
const key = `${snap.replyLength}:${snap.replyHash}`;
|
|
25953
|
+
if (key === lastKey) stable++;
|
|
25954
|
+
else {
|
|
25955
|
+
lastKey = key;
|
|
25956
|
+
stable = 0;
|
|
25957
|
+
}
|
|
25958
|
+
if (stable >= 1) {
|
|
25959
|
+
answer = candidate;
|
|
25960
|
+
break;
|
|
25961
|
+
}
|
|
25847
25962
|
}
|
|
25963
|
+
await sleep2(300);
|
|
25848
25964
|
}
|
|
25849
|
-
|
|
25850
|
-
|
|
25851
|
-
|
|
25852
|
-
if (previousSelectedId && previousSelectedId !== job.agentId) {
|
|
25853
|
-
if (await this._ensureHistoryOpen(c)) {
|
|
25854
|
-
await evalJS(c, exprOpenAgent(previousSelectedId));
|
|
25855
|
-
await this._closeHistory(c);
|
|
25856
|
-
await this._waitForSelectedAgent(c, previousSelectedId);
|
|
25857
|
-
}
|
|
25858
|
-
}
|
|
25859
|
-
return answer;
|
|
25965
|
+
if (!answer) throw new Error(`${job.agentId} was opened, but no final assistant reply was found`);
|
|
25966
|
+
return answer;
|
|
25967
|
+
});
|
|
25860
25968
|
} finally {
|
|
25861
|
-
|
|
25862
|
-
|
|
25969
|
+
try {
|
|
25970
|
+
await this._closeHistory(c);
|
|
25971
|
+
} finally {
|
|
25972
|
+
c.close();
|
|
25973
|
+
}
|
|
25863
25974
|
}
|
|
25864
25975
|
}
|
|
25865
25976
|
_reapWithoutResult(job, error2, evidence) {
|
|
@@ -25874,7 +25985,6 @@ var CursorBridge = class {
|
|
|
25874
25985
|
job.terminalEvidence = evidence || "stable_completed_history_icon";
|
|
25875
25986
|
job.recoveryState = "terminal_result_uncollected";
|
|
25876
25987
|
job.reservationScope = uncertainSubmissionReservationScope(job, error2);
|
|
25877
|
-
job.recoveryState = error2.recoveryState || "monitoring_uncertain_submission";
|
|
25878
25988
|
this._safeSettleSessionJob(job, "terminal_result_uncollected", { needsAttention: true });
|
|
25879
25989
|
}
|
|
25880
25990
|
_abandonJob(job, reason) {
|
|
@@ -25951,6 +26061,7 @@ var CursorBridge = class {
|
|
|
25951
26061
|
job.phase = "running";
|
|
25952
26062
|
job.error = null;
|
|
25953
26063
|
job.recoveryState = "monitoring";
|
|
26064
|
+
if (options.reattach !== false && !job.cancelRequested) job.waitDeadlineAt = Date.now() + job.timeoutMs;
|
|
25954
26065
|
const attached = options.reattach !== false && !job.cancelRequested && this._startParallelMonitor(job);
|
|
25955
26066
|
return { changed: attached, state: "running", monitorReattached: attached, task: this._taskView(job, true) };
|
|
25956
26067
|
}
|
|
@@ -26349,12 +26460,12 @@ var CursorBridge = class {
|
|
|
26349
26460
|
}).catch((e) => console.error("\u26A0\uFE0F Failed to restore the original Cursor Agent: " + e.message));
|
|
26350
26461
|
}
|
|
26351
26462
|
async _waitComplete(c, timeoutMs = QUERY_TIMEOUT, baselineCount = 0, job = null, providerErrorBaseline = "") {
|
|
26352
|
-
const
|
|
26463
|
+
const deadline = this._taskWaitDeadline(job, timeoutMs);
|
|
26353
26464
|
const INTERVAL = 1e3;
|
|
26354
26465
|
let sawStop = false;
|
|
26355
26466
|
let lastReplyKey = "", stableReply = 0;
|
|
26356
26467
|
await sleep2(1200);
|
|
26357
|
-
while (Date.now()
|
|
26468
|
+
while (Date.now() < deadline) {
|
|
26358
26469
|
await this._throwIfNewProviderError(c, providerErrorBaseline);
|
|
26359
26470
|
if (job && job.cancelRequested) {
|
|
26360
26471
|
if (job.agentId) {
|
|
@@ -26419,6 +26530,7 @@ var CursorBridge = class {
|
|
|
26419
26530
|
throw new Error(`Cursor task timed out (${timeoutMs}ms) before generation was confirmed stopped with a complete assistant reply${taskHint}`);
|
|
26420
26531
|
}
|
|
26421
26532
|
_taskView(job, includeResult = false) {
|
|
26533
|
+
if (includeResult) this._markTaskResultCollected(job);
|
|
26422
26534
|
const view = {
|
|
26423
26535
|
taskId: job.id,
|
|
26424
26536
|
kind: job.kind,
|
|
@@ -26457,6 +26569,8 @@ var CursorBridge = class {
|
|
|
26457
26569
|
monitorAttached: job.monitorAttached,
|
|
26458
26570
|
monitorGeneration: job.monitorGeneration,
|
|
26459
26571
|
resultUnavailable: job.resultUnavailable,
|
|
26572
|
+
resultCollectedAt: job.resultCollectedAt || null,
|
|
26573
|
+
waitDeadlineAt: Number.isFinite(job.waitDeadlineAt) ? new Date(job.waitDeadlineAt).toISOString() : null,
|
|
26460
26574
|
terminalEvidence: job.terminalEvidence,
|
|
26461
26575
|
providerError: job.providerError,
|
|
26462
26576
|
uiDiagnostic: job.uiDiagnostic,
|
|
@@ -26466,7 +26580,7 @@ var CursorBridge = class {
|
|
|
26466
26580
|
blocksFifo: this.activeParallel.has(job.id) && !isTerminalTask(job),
|
|
26467
26581
|
blocksAll: this.activeParallel.has(job.id) && !isTerminalTask(job) && job.reservationScope === "global"
|
|
26468
26582
|
};
|
|
26469
|
-
if (includeResult
|
|
26583
|
+
if (includeResult) view.result = job.result;
|
|
26470
26584
|
if (job.phase === "orphaned") {
|
|
26471
26585
|
if (job.recoveryState === "terminal_result_uncollected") {
|
|
26472
26586
|
view.attention = "Agent History proves that the underlying task ended, but its final reply has not been collected. Retry with reap, or explicitly abandon only if a missing reply is acceptable.";
|
|
@@ -26476,11 +26590,22 @@ var CursorBridge = class {
|
|
|
26476
26590
|
}
|
|
26477
26591
|
return view;
|
|
26478
26592
|
}
|
|
26479
|
-
|
|
26480
|
-
if (this.
|
|
26593
|
+
_assertWorkspaceConfirmed() {
|
|
26594
|
+
if (this.workspaceConfirmationRequired) {
|
|
26595
|
+
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.");
|
|
26596
|
+
}
|
|
26597
|
+
}
|
|
26598
|
+
_ensureTaskCapacity() {
|
|
26599
|
+
this._trimTasks(49);
|
|
26600
|
+
if (this.tasks.size >= 50) {
|
|
26601
|
+
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.");
|
|
26602
|
+
}
|
|
26603
|
+
}
|
|
26604
|
+
_trimTasks(limit = 50) {
|
|
26605
|
+
if (this.tasks.size <= limit) return;
|
|
26481
26606
|
for (const [id, job] of this.tasks) {
|
|
26482
|
-
if (isTerminalTask(job)) this.tasks.delete(id);
|
|
26483
|
-
if (this.tasks.size <=
|
|
26607
|
+
if (isTerminalTask(job) && (job.result == null || job.resultCollectedAt)) this.tasks.delete(id);
|
|
26608
|
+
if (this.tasks.size <= limit) break;
|
|
26484
26609
|
}
|
|
26485
26610
|
}
|
|
26486
26611
|
async status(taskId = "") {
|
|
@@ -26510,6 +26635,8 @@ var CursorBridge = class {
|
|
|
26510
26635
|
blockedQueuedCount: this.activeParallel.size > 0 ? this.queue.filter((job) => globalBlocked || job.effectiveExecution !== "parallel_agent").length : 0,
|
|
26511
26636
|
activeParallel: [...this.activeParallel.values()].map((job) => this._taskView(job)),
|
|
26512
26637
|
recentTasks: [...this.tasks.values()].slice(-10).map((job) => this._taskView(job)),
|
|
26638
|
+
unreadResultTaskIds: [...this.tasks.values()].filter((job) => isTerminalTask(job) && job.result != null && !job.resultCollectedAt).map((job) => job.id),
|
|
26639
|
+
taskRetentionLimit: 50,
|
|
26513
26640
|
cdpPort: CDP_PORT2,
|
|
26514
26641
|
lifecycle: this._lastLifecycle || {
|
|
26515
26642
|
adapterPid: process.pid,
|
|
@@ -26575,7 +26702,7 @@ function buildToolDefinitions(bridgeInstance) {
|
|
|
26575
26702
|
background: { type: "boolean", default: true, description: "When true, return the task ID immediately. When false, wait for the task to finish or need attention." },
|
|
26576
26703
|
execution: { type: "string", enum: ["fifo", "parallel_agent"], default: "fifo", description: "fifo is the first-in, first-out serial queue and runs one task at a time in a clean chat. parallel_agent creates a separate top-level Cursor Agent." },
|
|
26577
26704
|
read_only: { type: "boolean", default: false, description: "Set true when Cursor must not change the workspace." },
|
|
26578
|
-
timeout_ms: { type: "integer", minimum: 3e4, maximum: 9e5, default: 6e5, description: "
|
|
26705
|
+
timeout_ms: { type: "integer", minimum: 3e4, maximum: 9e5, default: 6e5, description: "One monitoring budget after submission, shared by FIFO and automatic Agent recovery. Expiry needs attention; it does not cancel work. Explicit reap may start a new budget. The default is 10 minutes." },
|
|
26579
26706
|
allowed_paths: { type: "array", items: { type: "string" }, description: "Workspace-relative paths Cursor may write. Parallel write tasks require non-overlapping paths. This declaration is not a filesystem sandbox." },
|
|
26580
26707
|
completion_contract: { type: "string", description: "Optional acceptance checks or a required final-report format." },
|
|
26581
26708
|
session_mode: { type: "string", enum: [...CURSOR_SESSION_MODES], default: "isolated", description: "isolated preserves the current clean-task behavior. create starts an update-safe persistent session. continue sends one new turn to the exact session_id." },
|
|
@@ -26603,12 +26730,12 @@ function buildToolDefinitions(bridgeInstance) {
|
|
|
26603
26730
|
},
|
|
26604
26731
|
{
|
|
26605
26732
|
name: "cursor_session_control",
|
|
26606
|
-
description: "Recover, close, or forget one exact persistent cursor_do session. reconcile checks the exact Agent twice and never resends. close prevents future sends but does not stop or delete the Cursor Agent. An active or uncertain session cannot be closed. abandon is the explicit last resort when exact stop evidence is unavailable. forget requires confirm=true and is allowed only after close; it deletes only the Bridge mapping.",
|
|
26733
|
+
description: "Recover, collect a reply, close, or forget one exact persistent cursor_do session. reconcile checks the exact Agent twice and never resends. collect_result reads the completed reconciled turn from that Agent without sending a prompt; it may temporarily select that Agent. close prevents future sends but does not stop or delete the Cursor Agent. An active or uncertain session cannot be closed. abandon is the explicit last resort when exact stop evidence is unavailable. forget requires confirm=true and is allowed only after close; it deletes only the Bridge mapping.",
|
|
26607
26734
|
inputSchema: {
|
|
26608
26735
|
type: "object",
|
|
26609
26736
|
properties: {
|
|
26610
26737
|
session_id: { type: "string", description: "The exact session ID returned by cursor_do(session_mode=create)." },
|
|
26611
|
-
action: { type: "string", enum: ["reconcile", "close", "forget", "abandon"], description: "reconcile reads the exact Agent state; close ends safe continuity; forget removes a closed mapping; abandon releases an uncertain mapping without stop proof." },
|
|
26738
|
+
action: { type: "string", enum: ["reconcile", "collect_result", "close", "forget", "abandon"], description: "reconcile reads the exact Agent state; collect_result retrieves its completed latest turn before continuing; close ends safe continuity; forget removes a closed mapping; abandon releases an uncertain mapping without stop proof." },
|
|
26612
26739
|
confirm: { type: "boolean", default: false, description: "Required for forget and abandon." },
|
|
26613
26740
|
reason: { type: "string", description: "Required and non-empty for abandon." },
|
|
26614
26741
|
acknowledge_may_still_write: { type: "boolean", default: false, description: "Required for abandon; acknowledges that the underlying Cursor Agent may still run or write." }
|
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.13",
|
|
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.13",
|
|
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.
|
|
50
|
+
"embeddedProductVersion": "5.9.0"
|
|
51
51
|
}
|
|
52
52
|
}
|