pi-cursor-sdk 0.2.0 → 0.3.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/CHANGELOG.md +62 -0
- package/README.md +54 -50
- package/dist/bundled-context-windows.js +39 -0
- package/dist/context-window-cache.js +92 -0
- package/dist/context.js +338 -0
- package/dist/cursor-active-tools.js +3 -0
- package/dist/cursor-agent-message-web-tools.js +165 -0
- package/dist/cursor-agents-context-registration.js +17 -0
- package/dist/cursor-agents-context.js +113 -0
- package/dist/cursor-api-key.js +34 -0
- package/dist/cursor-bridge-contract.js +27 -0
- package/dist/cursor-cloud-lifecycle.js +650 -0
- package/dist/cursor-cloud-local-state.js +460 -0
- package/dist/cursor-cloud-options.js +145 -0
- package/dist/cursor-cloud-reporting.js +222 -0
- package/dist/cursor-compact-tool-summary.js +75 -0
- package/dist/cursor-config.js +613 -0
- package/dist/cursor-context-tools.js +4 -0
- package/dist/cursor-display-only-trace.js +11 -0
- package/dist/cursor-display-text.js +18 -0
- package/dist/cursor-durable-fs.js +53 -0
- package/dist/cursor-edit-diff.js +13 -0
- package/dist/cursor-env-boolean.js +19 -0
- package/dist/cursor-fallback-models.generated.js +7760 -0
- package/dist/cursor-fallback-warning.js +15 -0
- package/dist/cursor-http1.js +45 -0
- package/dist/cursor-incomplete-tool-visibility.js +83 -0
- package/dist/cursor-live-run-accounting.js +55 -0
- package/dist/cursor-live-run-coordinator.js +406 -0
- package/dist/cursor-mcp-timeout-override.js +116 -0
- package/dist/cursor-model-lifecycle.js +32 -0
- package/dist/cursor-model.js +5 -0
- package/dist/cursor-native-replay-routing.js +35 -0
- package/dist/cursor-native-replay-trace.js +28 -0
- package/dist/cursor-native-tool-display-registration.js +102 -0
- package/dist/cursor-native-tool-display-replay.js +428 -0
- package/dist/cursor-native-tool-display-state.js +70 -0
- package/dist/cursor-native-tool-display-tools.js +123 -0
- package/dist/cursor-native-tool-names.js +11 -0
- package/dist/cursor-partial-content-emitter.js +118 -0
- package/dist/cursor-pi-tool-bridge-abort.js +114 -0
- package/dist/cursor-pi-tool-bridge-constants.js +6 -0
- package/dist/cursor-pi-tool-bridge-diagnostics.js +125 -0
- package/dist/cursor-pi-tool-bridge-env.js +18 -0
- package/dist/cursor-pi-tool-bridge-mcp.js +100 -0
- package/dist/cursor-pi-tool-bridge-run.js +355 -0
- package/dist/cursor-pi-tool-bridge-server.js +163 -0
- package/dist/cursor-pi-tool-bridge-snapshot.js +63 -0
- package/dist/cursor-pi-tool-bridge-types.js +1 -0
- package/dist/cursor-pi-tool-bridge.js +123 -0
- package/dist/cursor-provider-errors.js +311 -0
- package/dist/cursor-provider-lazy.js +39 -0
- package/dist/cursor-provider-live-run-drain.js +412 -0
- package/dist/cursor-provider-overflow.js +90 -0
- package/dist/cursor-provider-run-finalizer.js +176 -0
- package/dist/cursor-provider-run-outcome.js +76 -0
- package/dist/cursor-provider-turn-coordinator.js +275 -0
- package/dist/cursor-provider-turn-display-router.js +158 -0
- package/dist/cursor-provider-turn-emit.js +35 -0
- package/dist/cursor-provider-turn-finalize.js +154 -0
- package/dist/cursor-provider-turn-lifecycle-emitter.js +126 -0
- package/dist/cursor-provider-turn-prepare.js +374 -0
- package/dist/cursor-provider-turn-runner.js +146 -0
- package/dist/cursor-provider-turn-sdk-normalizer.js +74 -0
- package/dist/cursor-provider-turn-send.js +138 -0
- package/dist/cursor-provider-turn-shell-output.js +117 -0
- package/dist/cursor-provider-turn-tool-ledger.js +99 -0
- package/dist/cursor-provider-turn-types.js +1 -0
- package/dist/cursor-provider.js +72 -0
- package/dist/cursor-question-tool.js +194 -0
- package/dist/cursor-record-utils.js +61 -0
- package/dist/cursor-replay-activity-builders.js +114 -0
- package/dist/cursor-replay-source-names.js +27 -0
- package/dist/cursor-replay-summary-args.js +105 -0
- package/dist/cursor-replay-tool-details.js +167 -0
- package/dist/cursor-ripgrep-path.js +27 -0
- package/dist/cursor-run-final-text.js +63 -0
- package/dist/cursor-runtime-state.js +380 -0
- package/dist/cursor-sdk-billed-usage.js +86 -0
- package/dist/cursor-sdk-event-debug-constants.js +33 -0
- package/dist/cursor-sdk-event-debug-session.js +107 -0
- package/dist/cursor-sdk-event-debug.js +461 -0
- package/dist/cursor-sdk-output-filter.js +1 -0
- package/dist/cursor-sdk-process-error-guard.js +221 -0
- package/dist/cursor-sdk-runtime.js +3 -0
- package/dist/cursor-sensitive-text.js +23 -0
- package/dist/cursor-session-agent-cleanup.js +311 -0
- package/dist/cursor-session-agent-lifecycle.js +32 -0
- package/dist/cursor-session-agent-lineage.js +103 -0
- package/dist/cursor-session-agent-resume.js +417 -0
- package/dist/cursor-session-agent.js +507 -0
- package/dist/cursor-session-compaction-prep.js +21 -0
- package/dist/cursor-session-scope.js +113 -0
- package/dist/cursor-session-send-policy.js +18 -0
- package/dist/cursor-session-store.js +105 -0
- package/dist/cursor-session-turn-queue.js +50 -0
- package/dist/cursor-setting-sources.js +15 -0
- package/dist/cursor-skill-tool.js +230 -0
- package/dist/cursor-state.js +519 -0
- package/dist/cursor-task-presentation.js +59 -0
- package/dist/cursor-tool-lifecycle.js +94 -0
- package/dist/cursor-tool-manifest.js +34 -0
- package/dist/cursor-tool-presentation-registry.js +305 -0
- package/dist/cursor-tool-result-display-readers.js +158 -0
- package/dist/cursor-tool-transcript.js +32 -0
- package/dist/cursor-tool-visibility.js +28 -0
- package/dist/cursor-transcript-tool-formatters.js +618 -0
- package/dist/cursor-transcript-tool-specs.js +284 -0
- package/dist/cursor-transcript-utils.js +170 -0
- package/dist/cursor-usage-accounting.js +166 -0
- package/dist/cursor-web-tool-activity.js +23 -0
- package/dist/cursor-web-tool-args.js +12 -0
- package/dist/index.js +82 -0
- package/dist/model-discovery.js +305 -0
- package/dist/model-list-cache.js +156 -0
- package/docs/cursor-dogfood-checklist.md +2 -1
- package/docs/cursor-live-smoke-checklist.md +13 -12
- package/docs/cursor-model-ux-spec.md +25 -6
- package/docs/cursor-native-tool-replay.md +13 -13
- package/docs/cursor-native-tool-visual-audit.md +6 -3
- package/docs/cursor-testing-lessons.md +7 -7
- package/docs/cursor-tool-surfaces.md +5 -5
- package/docs/platform-smoke.md +3 -3
- package/package.json +19 -7
- package/platform-smoke.config.mjs +1 -1
- package/scripts/build.mjs +126 -0
- package/scripts/cloud-runtime-smoke.mjs +3 -1
- package/scripts/debug-provider-events.mjs +3 -1
- package/scripts/debug-sdk-events.mjs +1 -1
- package/scripts/isolated-cursor-smoke.sh +5 -4
- package/scripts/lib/cloud-smoke-cleanup-evidence.mjs +1 -1
- package/scripts/lib/cursor-visual-render.mjs +1 -1
- package/scripts/lib/ensure-built.mjs +16 -0
- package/scripts/lib/local-resume-smoke-harness.mjs +1 -1
- package/scripts/local-resume-smoke.mjs +3 -1
- package/scripts/platform-smoke/card-detect.mjs +1 -1
- package/scripts/platform-smoke/live-suite-runner.mjs +1 -1
- package/scripts/platform-smoke/render-ansi.mjs +1 -1
- package/scripts/platform-smoke/targets.mjs +2 -2
- package/scripts/prepare.mjs +102 -0
- package/scripts/steering-rpc-smoke.mjs +4 -2
- package/scripts/tmux-live-smoke.sh +11 -11
- package/scripts/visual-tui-smoke-self-test.mjs +4 -1
- package/scripts/visual-tui-smoke.mjs +51 -7
- package/src/bundled-context-windows.ts +4 -1
- package/src/cursor-agents-context-registration.ts +2 -2
- package/src/cursor-agents-context.ts +0 -2
- package/src/cursor-api-key.ts +5 -4
- package/src/cursor-cloud-reporting.ts +23 -2
- package/src/cursor-fallback-models.generated.ts +592 -204
- package/src/cursor-live-run-coordinator.ts +1 -0
- package/src/cursor-native-tool-display-registration.ts +10 -11
- package/src/cursor-provider-errors.ts +1 -1
- package/src/cursor-provider-lazy.ts +5 -4
- package/src/cursor-provider-live-run-drain.ts +1 -0
- package/src/cursor-provider-run-finalizer.ts +1 -0
- package/src/cursor-provider-turn-finalize.ts +12 -0
- package/src/cursor-provider-turn-types.ts +2 -0
- package/src/cursor-sdk-billed-usage.ts +107 -0
- package/src/cursor-sdk-process-error-guard.ts +6 -4
- package/src/cursor-session-agent-lifecycle.ts +8 -9
- package/src/cursor-session-agent-resume.ts +23 -0
- package/src/cursor-session-compaction-prep.ts +4 -0
- package/src/cursor-tool-manifest.ts +1 -1
- package/src/cursor-usage-accounting.ts +70 -9
- package/src/index.ts +1 -1
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const DEFAULT_THINKING_TRACE_MAX_CHARS = 50000;
|
|
2
|
+
export class CursorPartialContentEmitter {
|
|
3
|
+
stream;
|
|
4
|
+
partial;
|
|
5
|
+
thinkingMaxChars;
|
|
6
|
+
mutuallyExclusive;
|
|
7
|
+
thinkingContentIndex = -1;
|
|
8
|
+
textContentIndex = -1;
|
|
9
|
+
activityTraceChars = 0;
|
|
10
|
+
activityTraceTruncated = false;
|
|
11
|
+
constructor(stream, partial, thinkingMaxChars = DEFAULT_THINKING_TRACE_MAX_CHARS, mutuallyExclusive = true) {
|
|
12
|
+
this.stream = stream;
|
|
13
|
+
this.partial = partial;
|
|
14
|
+
this.thinkingMaxChars = thinkingMaxChars;
|
|
15
|
+
this.mutuallyExclusive = mutuallyExclusive;
|
|
16
|
+
}
|
|
17
|
+
closeThinking() {
|
|
18
|
+
if (this.thinkingContentIndex < 0)
|
|
19
|
+
return;
|
|
20
|
+
const block = this.partial.content[this.thinkingContentIndex];
|
|
21
|
+
if (block.type === "thinking") {
|
|
22
|
+
this.stream.push({
|
|
23
|
+
type: "thinking_end",
|
|
24
|
+
contentIndex: this.thinkingContentIndex,
|
|
25
|
+
content: block.thinking,
|
|
26
|
+
partial: this.partial,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
this.thinkingContentIndex = -1;
|
|
30
|
+
}
|
|
31
|
+
closeText() {
|
|
32
|
+
if (this.textContentIndex < 0)
|
|
33
|
+
return "";
|
|
34
|
+
const contentIndex = this.textContentIndex;
|
|
35
|
+
const block = this.partial.content[contentIndex];
|
|
36
|
+
this.textContentIndex = -1;
|
|
37
|
+
if (block.type !== "text")
|
|
38
|
+
return "";
|
|
39
|
+
this.stream.push({
|
|
40
|
+
type: "text_end",
|
|
41
|
+
contentIndex,
|
|
42
|
+
content: block.text,
|
|
43
|
+
partial: this.partial,
|
|
44
|
+
});
|
|
45
|
+
return block.text;
|
|
46
|
+
}
|
|
47
|
+
closeAll() {
|
|
48
|
+
this.closeThinking();
|
|
49
|
+
return this.closeText();
|
|
50
|
+
}
|
|
51
|
+
appendThinkingDelta(delta, options) {
|
|
52
|
+
const closeText = options?.closeText ?? this.mutuallyExclusive;
|
|
53
|
+
if (closeText)
|
|
54
|
+
this.closeText();
|
|
55
|
+
if (this.activityTraceTruncated || !delta)
|
|
56
|
+
return;
|
|
57
|
+
let text = delta;
|
|
58
|
+
if (this.thinkingMaxChars >= 0 && this.activityTraceChars + text.length > this.thinkingMaxChars) {
|
|
59
|
+
const remainingChars = Math.max(this.thinkingMaxChars - this.activityTraceChars, 0);
|
|
60
|
+
text = `${text.slice(0, remainingChars)}\n[Cursor activity trace truncated]\n`;
|
|
61
|
+
this.activityTraceTruncated = true;
|
|
62
|
+
}
|
|
63
|
+
if (!text)
|
|
64
|
+
return;
|
|
65
|
+
if (this.thinkingContentIndex < 0) {
|
|
66
|
+
this.thinkingContentIndex = this.partial.content.length;
|
|
67
|
+
this.partial.content.push({ type: "thinking", thinking: "" });
|
|
68
|
+
this.stream.push({ type: "thinking_start", contentIndex: this.thinkingContentIndex, partial: this.partial });
|
|
69
|
+
}
|
|
70
|
+
const block = this.partial.content[this.thinkingContentIndex];
|
|
71
|
+
if (block.type !== "thinking")
|
|
72
|
+
return;
|
|
73
|
+
block.thinking += text;
|
|
74
|
+
this.activityTraceChars += text.length;
|
|
75
|
+
this.stream.push({
|
|
76
|
+
type: "thinking_delta",
|
|
77
|
+
contentIndex: this.thinkingContentIndex,
|
|
78
|
+
delta: text,
|
|
79
|
+
partial: this.partial,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
appendTextDelta(delta, options) {
|
|
83
|
+
const closeThinking = options?.closeThinking ?? this.mutuallyExclusive;
|
|
84
|
+
if (closeThinking)
|
|
85
|
+
this.closeThinking();
|
|
86
|
+
if (!delta)
|
|
87
|
+
return;
|
|
88
|
+
if (this.textContentIndex < 0) {
|
|
89
|
+
this.textContentIndex = this.partial.content.length;
|
|
90
|
+
this.partial.content.push({ type: "text", text: "" });
|
|
91
|
+
this.stream.push({ type: "text_start", contentIndex: this.textContentIndex, partial: this.partial });
|
|
92
|
+
}
|
|
93
|
+
const block = this.partial.content[this.textContentIndex];
|
|
94
|
+
if (block.type !== "text")
|
|
95
|
+
return;
|
|
96
|
+
block.text += delta;
|
|
97
|
+
this.stream.push({
|
|
98
|
+
type: "text_delta",
|
|
99
|
+
contentIndex: this.textContentIndex,
|
|
100
|
+
delta,
|
|
101
|
+
partial: this.partial,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
appendThinkingBlock(text, options) {
|
|
105
|
+
const closeText = options?.closeText ?? this.mutuallyExclusive;
|
|
106
|
+
if (closeText)
|
|
107
|
+
this.closeAll();
|
|
108
|
+
else
|
|
109
|
+
this.closeThinking();
|
|
110
|
+
this.appendThinkingDelta(text.endsWith("\n") ? text : `${text}\n`, { closeText: false });
|
|
111
|
+
this.closeThinking();
|
|
112
|
+
}
|
|
113
|
+
flushText(deltas) {
|
|
114
|
+
for (const delta of deltas)
|
|
115
|
+
this.appendTextDelta(delta);
|
|
116
|
+
return this.closeText();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
class CursorPiToolBridgeToolExecutionAbortTracker {
|
|
2
|
+
activeExecutions = new Map();
|
|
3
|
+
processSignalHandlersInstalled = false;
|
|
4
|
+
track(toolCallId, options) {
|
|
5
|
+
this.finish(toolCallId);
|
|
6
|
+
const execution = {
|
|
7
|
+
toolCallId,
|
|
8
|
+
abort: options.abort,
|
|
9
|
+
cancelPending: options.cancelPending,
|
|
10
|
+
signal: options.signal,
|
|
11
|
+
};
|
|
12
|
+
if (options.signal?.aborted) {
|
|
13
|
+
this.cancelExecution(execution, "Cursor pi bridge tool execution was already aborted");
|
|
14
|
+
this.abortExecution(execution);
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
execution.onAbort = () => {
|
|
18
|
+
this.cancelExecution(execution, "Cursor pi bridge tool execution was aborted");
|
|
19
|
+
this.abortExecution(execution);
|
|
20
|
+
this.finish(toolCallId);
|
|
21
|
+
};
|
|
22
|
+
execution.signal?.addEventListener("abort", execution.onAbort, { once: true });
|
|
23
|
+
this.activeExecutions.set(toolCallId, execution);
|
|
24
|
+
this.installProcessSignalHandlers();
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
finish(toolCallId) {
|
|
28
|
+
const execution = this.activeExecutions.get(toolCallId);
|
|
29
|
+
if (!execution)
|
|
30
|
+
return;
|
|
31
|
+
if (execution.onAbort)
|
|
32
|
+
execution.signal?.removeEventListener("abort", execution.onAbort);
|
|
33
|
+
this.activeExecutions.delete(toolCallId);
|
|
34
|
+
this.uninstallProcessSignalHandlersIfIdle();
|
|
35
|
+
}
|
|
36
|
+
finishAll() {
|
|
37
|
+
for (const toolCallId of [...this.activeExecutions.keys()])
|
|
38
|
+
this.finish(toolCallId);
|
|
39
|
+
}
|
|
40
|
+
abort(toolCallId, reason) {
|
|
41
|
+
const execution = this.activeExecutions.get(toolCallId);
|
|
42
|
+
if (!execution)
|
|
43
|
+
return false;
|
|
44
|
+
this.cancelExecution(execution, reason);
|
|
45
|
+
this.abortExecution(execution);
|
|
46
|
+
this.finish(toolCallId);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
abortAll(reason) {
|
|
50
|
+
for (const execution of [...this.activeExecutions.values()]) {
|
|
51
|
+
this.abort(execution.toolCallId, reason);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
getActiveCount() {
|
|
55
|
+
return this.activeExecutions.size;
|
|
56
|
+
}
|
|
57
|
+
emitProcessAbortSignalForTests(signal) {
|
|
58
|
+
this.abortActiveExecutions(signal, { preserveProcessSignalBehavior: true });
|
|
59
|
+
}
|
|
60
|
+
handleSigint = () => {
|
|
61
|
+
this.abortActiveExecutions("SIGINT");
|
|
62
|
+
};
|
|
63
|
+
handleSigterm = () => {
|
|
64
|
+
this.abortActiveExecutions("SIGTERM");
|
|
65
|
+
};
|
|
66
|
+
installProcessSignalHandlers() {
|
|
67
|
+
if (this.processSignalHandlersInstalled)
|
|
68
|
+
return;
|
|
69
|
+
this.processSignalHandlersInstalled = true;
|
|
70
|
+
process.on("SIGINT", this.handleSigint);
|
|
71
|
+
process.on("SIGTERM", this.handleSigterm);
|
|
72
|
+
}
|
|
73
|
+
uninstallProcessSignalHandlersIfIdle() {
|
|
74
|
+
if (!this.processSignalHandlersInstalled || this.activeExecutions.size > 0)
|
|
75
|
+
return;
|
|
76
|
+
this.processSignalHandlersInstalled = false;
|
|
77
|
+
process.off("SIGINT", this.handleSigint);
|
|
78
|
+
process.off("SIGTERM", this.handleSigterm);
|
|
79
|
+
}
|
|
80
|
+
abortActiveExecutions(signal, options = {}) {
|
|
81
|
+
if (this.activeExecutions.size === 0)
|
|
82
|
+
return;
|
|
83
|
+
const shouldRestoreDefaultSignalBehavior = options.preserveProcessSignalBehavior !== true && !this.hasExternalProcessSignalListeners(signal);
|
|
84
|
+
this.abortAll(`Cursor pi bridge tool execution interrupted by ${signal}`);
|
|
85
|
+
if (shouldRestoreDefaultSignalBehavior)
|
|
86
|
+
this.restoreDefaultProcessSignalBehavior(signal);
|
|
87
|
+
}
|
|
88
|
+
cancelExecution(execution, reason) {
|
|
89
|
+
try {
|
|
90
|
+
execution.cancelPending(reason);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Cancellation is best-effort during process abort/shutdown cleanup; keep aborting siblings.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
abortExecution(execution) {
|
|
97
|
+
try {
|
|
98
|
+
Promise.resolve(execution.abort()).catch(() => undefined);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Abort is best-effort during process abort/shutdown cleanup; keep aborting siblings.
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
hasExternalProcessSignalListeners(signal) {
|
|
105
|
+
const ownHandler = signal === "SIGINT" ? this.handleSigint : this.handleSigterm;
|
|
106
|
+
return process.listeners(signal).some((listener) => listener !== ownHandler);
|
|
107
|
+
}
|
|
108
|
+
restoreDefaultProcessSignalBehavior(signal) {
|
|
109
|
+
setImmediate(() => {
|
|
110
|
+
process.kill(process.pid, signal);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
export const bridgeToolExecutionAbortTracker = new CursorPiToolBridgeToolExecutionAbortTracker();
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const MCP_SERVER_NAME = "pi_tools";
|
|
2
|
+
export const MCP_ENDPOINT_ROOT = "/cursor-pi-tool-bridge";
|
|
3
|
+
const CURSOR_PI_BRIDGE_TOOL_CALL_ID_PATTERN = /^cursor-pi-bridge-run-[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}-tool-\d+$/i;
|
|
4
|
+
export function isCursorPiBridgeToolCallId(toolCallId) {
|
|
5
|
+
return CURSOR_PI_BRIDGE_TOOL_CALL_ID_PATTERN.test(toolCallId);
|
|
6
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { appendFileSync } from "node:fs";
|
|
2
|
+
import { stableNameHash } from "./cursor-pi-tool-bridge-mcp.js";
|
|
3
|
+
import { parseEnvBoolean } from "./cursor-env-boolean.js";
|
|
4
|
+
export const CURSOR_PI_TOOL_BRIDGE_DEBUG_ENV = "PI_CURSOR_PI_TOOL_BRIDGE_DEBUG";
|
|
5
|
+
export const CURSOR_PI_TOOL_BRIDGE_DEBUG_FILE_ENV = "PI_CURSOR_PI_TOOL_BRIDGE_DEBUG_FILE";
|
|
6
|
+
export const CURSOR_PI_TOOL_BRIDGE_DIAGNOSTIC_PREFIX = "[pi-cursor-sdk:bridge]";
|
|
7
|
+
export function resolveCursorPiToolBridgeDebugEnabled(env = process.env) {
|
|
8
|
+
return parseEnvBoolean(env[CURSOR_PI_TOOL_BRIDGE_DEBUG_ENV], false);
|
|
9
|
+
}
|
|
10
|
+
function createCursorMcpCallDiagnosticId(cursorMcpCallId) {
|
|
11
|
+
return cursorMcpCallId ? `cursor-mcp-call-${stableNameHash(cursorMcpCallId)}` : undefined;
|
|
12
|
+
}
|
|
13
|
+
function assertNeverDiagnosticEvent(_event) {
|
|
14
|
+
throw new Error("Unhandled Cursor pi tool bridge diagnostic event");
|
|
15
|
+
}
|
|
16
|
+
export function serializeCursorPiToolBridgeDiagnostic(event) {
|
|
17
|
+
switch (event.event) {
|
|
18
|
+
case "run_created":
|
|
19
|
+
return {
|
|
20
|
+
event: event.event,
|
|
21
|
+
runId: event.runId,
|
|
22
|
+
enabled: event.enabled,
|
|
23
|
+
exposedToolCount: event.exposedToolCount,
|
|
24
|
+
pendingCount: event.pendingCount,
|
|
25
|
+
};
|
|
26
|
+
case "run_skipped":
|
|
27
|
+
return {
|
|
28
|
+
event: event.event,
|
|
29
|
+
runId: event.runId,
|
|
30
|
+
enabled: event.enabled,
|
|
31
|
+
exposedToolCount: event.exposedToolCount,
|
|
32
|
+
pendingCount: event.pendingCount,
|
|
33
|
+
reason: event.reason,
|
|
34
|
+
};
|
|
35
|
+
case "tools_exposed":
|
|
36
|
+
return {
|
|
37
|
+
event: event.event,
|
|
38
|
+
runId: event.runId,
|
|
39
|
+
enabled: event.enabled,
|
|
40
|
+
exposedToolCount: event.exposedToolCount,
|
|
41
|
+
pendingCount: event.pendingCount,
|
|
42
|
+
pairs: event.pairs.map((pair) => ({ piToolName: pair.piToolName, mcpToolName: pair.mcpToolName })),
|
|
43
|
+
};
|
|
44
|
+
case "run_cancelled":
|
|
45
|
+
return {
|
|
46
|
+
event: event.event,
|
|
47
|
+
runId: event.runId,
|
|
48
|
+
enabled: event.enabled,
|
|
49
|
+
exposedToolCount: event.exposedToolCount,
|
|
50
|
+
pendingCount: event.pendingCount,
|
|
51
|
+
queuedCount: event.queuedCount,
|
|
52
|
+
cancelledRequestCount: event.cancelledRequestCount,
|
|
53
|
+
};
|
|
54
|
+
case "run_disposed":
|
|
55
|
+
return {
|
|
56
|
+
event: event.event,
|
|
57
|
+
runId: event.runId,
|
|
58
|
+
enabled: event.enabled,
|
|
59
|
+
exposedToolCount: event.exposedToolCount,
|
|
60
|
+
pendingCount: event.pendingCount,
|
|
61
|
+
};
|
|
62
|
+
case "request_queued":
|
|
63
|
+
return {
|
|
64
|
+
event: event.event,
|
|
65
|
+
runId: event.runId,
|
|
66
|
+
bridgeCallId: event.bridgeCallId,
|
|
67
|
+
cursorMcpCallId: createCursorMcpCallDiagnosticId(event.cursorMcpCallId),
|
|
68
|
+
piToolCallId: event.piToolCallId,
|
|
69
|
+
mcpToolName: event.mcpToolName,
|
|
70
|
+
piToolName: event.piToolName,
|
|
71
|
+
pendingCount: event.pendingCount,
|
|
72
|
+
};
|
|
73
|
+
case "request_resolved":
|
|
74
|
+
return {
|
|
75
|
+
event: event.event,
|
|
76
|
+
runId: event.runId,
|
|
77
|
+
bridgeCallId: event.bridgeCallId,
|
|
78
|
+
cursorMcpCallId: createCursorMcpCallDiagnosticId(event.cursorMcpCallId),
|
|
79
|
+
piToolCallId: event.piToolCallId,
|
|
80
|
+
mcpToolName: event.mcpToolName,
|
|
81
|
+
piToolName: event.piToolName,
|
|
82
|
+
pendingCount: event.pendingCount,
|
|
83
|
+
isError: event.isError,
|
|
84
|
+
};
|
|
85
|
+
case "request_rejected":
|
|
86
|
+
return {
|
|
87
|
+
event: event.event,
|
|
88
|
+
runId: event.runId,
|
|
89
|
+
bridgeCallId: event.bridgeCallId,
|
|
90
|
+
cursorMcpCallId: createCursorMcpCallDiagnosticId(event.cursorMcpCallId),
|
|
91
|
+
piToolCallId: event.piToolCallId,
|
|
92
|
+
mcpToolName: event.mcpToolName,
|
|
93
|
+
piToolName: event.piToolName,
|
|
94
|
+
pendingCount: event.pendingCount,
|
|
95
|
+
rejectionKind: event.rejectionKind,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return assertNeverDiagnosticEvent(event);
|
|
99
|
+
}
|
|
100
|
+
export function writeCursorPiToolBridgeDiagnostic(env, event, debugRecorder) {
|
|
101
|
+
try {
|
|
102
|
+
debugRecorder?.recordBridgeDiagnostic(event);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Diagnostics must never affect bridge execution.
|
|
106
|
+
}
|
|
107
|
+
const serialized = serializeCursorPiToolBridgeDiagnostic(event);
|
|
108
|
+
const debugFile = env[CURSOR_PI_TOOL_BRIDGE_DEBUG_FILE_ENV];
|
|
109
|
+
if (debugFile) {
|
|
110
|
+
try {
|
|
111
|
+
appendFileSync(debugFile, `${JSON.stringify(serialized)}\n`);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Diagnostics must never affect bridge execution.
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (!resolveCursorPiToolBridgeDebugEnabled(env))
|
|
118
|
+
return;
|
|
119
|
+
try {
|
|
120
|
+
process.stderr.write(`${CURSOR_PI_TOOL_BRIDGE_DIAGNOSTIC_PREFIX} ${JSON.stringify(serialized)}\n`);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Diagnostics must never affect bridge execution.
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { parseEnvBoolean } from "./cursor-env-boolean.js";
|
|
2
|
+
import { resolveCursorMcpToolTimeoutMs } from "./cursor-mcp-timeout-override.js";
|
|
3
|
+
export const CURSOR_PI_TOOL_BRIDGE_ENV = "PI_CURSOR_PI_TOOL_BRIDGE";
|
|
4
|
+
export const CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV = "PI_CURSOR_EXPOSE_BUILTIN_TOOLS";
|
|
5
|
+
export const CURSOR_PI_TOOL_BRIDGE_CALL_TIMEOUT_MS_ENV = "PI_CURSOR_PI_BRIDGE_CALL_TIMEOUT_MS";
|
|
6
|
+
export function resolveCursorPiToolBridgeEnabled(env = process.env) {
|
|
7
|
+
return parseEnvBoolean(env[CURSOR_PI_TOOL_BRIDGE_ENV], true);
|
|
8
|
+
}
|
|
9
|
+
export function resolveCursorPiToolBridgeBuiltinsEnabled(env = process.env) {
|
|
10
|
+
return parseEnvBoolean(env[CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV], false);
|
|
11
|
+
}
|
|
12
|
+
export function resolveCursorPiToolBridgeCallTimeoutMs(env = process.env) {
|
|
13
|
+
const mcpToolTimeoutMs = resolveCursorMcpToolTimeoutMs(env);
|
|
14
|
+
const parsed = Number(env[CURSOR_PI_TOOL_BRIDGE_CALL_TIMEOUT_MS_ENV]?.trim());
|
|
15
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
16
|
+
return mcpToolTimeoutMs;
|
|
17
|
+
return Math.min(Math.max(Math.trunc(parsed), 1), mcpToolTimeoutMs);
|
|
18
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { buildCursorPiBridgeMcpToolDescription, CURSOR_PI_BRIDGE_MCP_TOOL_PREFIX } from "./cursor-bridge-contract.js";
|
|
3
|
+
import { asRecord, stringifyUnknown } from "./cursor-record-utils.js";
|
|
4
|
+
export function normalizeMcpInputSchema(schema) {
|
|
5
|
+
const record = asRecord(schema);
|
|
6
|
+
if (record?.type === "object")
|
|
7
|
+
return record;
|
|
8
|
+
return { type: "object", properties: {} };
|
|
9
|
+
}
|
|
10
|
+
export function normalizeMcpArgs(args) {
|
|
11
|
+
const record = asRecord(args);
|
|
12
|
+
return record ? { ...record } : {};
|
|
13
|
+
}
|
|
14
|
+
export function waitForProtocolFlush() {
|
|
15
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
16
|
+
}
|
|
17
|
+
function sanitizeMcpToolNameStem(toolName) {
|
|
18
|
+
const stem = toolName
|
|
19
|
+
.trim()
|
|
20
|
+
.replace(/[^A-Za-z0-9_-]+/g, "_")
|
|
21
|
+
.replace(/^_+|_+$/g, "");
|
|
22
|
+
return stem || "tool";
|
|
23
|
+
}
|
|
24
|
+
export function stableNameHash(value) {
|
|
25
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 8);
|
|
26
|
+
}
|
|
27
|
+
export function createMcpToolName(piToolName, usedMcpToolNames) {
|
|
28
|
+
const baseName = `${CURSOR_PI_BRIDGE_MCP_TOOL_PREFIX}${sanitizeMcpToolNameStem(piToolName)}`;
|
|
29
|
+
if (!usedMcpToolNames.has(baseName)) {
|
|
30
|
+
usedMcpToolNames.add(baseName);
|
|
31
|
+
return baseName;
|
|
32
|
+
}
|
|
33
|
+
const hashedName = `${baseName}__${stableNameHash(piToolName)}`;
|
|
34
|
+
if (!usedMcpToolNames.has(hashedName)) {
|
|
35
|
+
usedMcpToolNames.add(hashedName);
|
|
36
|
+
return hashedName;
|
|
37
|
+
}
|
|
38
|
+
let counter = 2;
|
|
39
|
+
let candidate = `${hashedName}_${counter}`;
|
|
40
|
+
while (usedMcpToolNames.has(candidate)) {
|
|
41
|
+
counter += 1;
|
|
42
|
+
candidate = `${hashedName}_${counter}`;
|
|
43
|
+
}
|
|
44
|
+
usedMcpToolNames.add(candidate);
|
|
45
|
+
return candidate;
|
|
46
|
+
}
|
|
47
|
+
export function snapshotToolToMcpTool(tool) {
|
|
48
|
+
return {
|
|
49
|
+
name: tool.mcpToolName,
|
|
50
|
+
description: buildCursorPiBridgeMcpToolDescription({
|
|
51
|
+
piToolName: tool.piToolName,
|
|
52
|
+
mcpToolName: tool.mcpToolName,
|
|
53
|
+
piToolDescription: tool.description,
|
|
54
|
+
piToolPromptGuidelines: tool.promptGuidelines,
|
|
55
|
+
}),
|
|
56
|
+
inputSchema: tool.inputSchema,
|
|
57
|
+
_meta: { piToolName: tool.piToolName },
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function convertPiContentToMcpContent(content) {
|
|
61
|
+
if (!Array.isArray(content)) {
|
|
62
|
+
return [{ type: "text", text: stringifyUnknown(content) }];
|
|
63
|
+
}
|
|
64
|
+
const mcpContent = [];
|
|
65
|
+
for (const block of content) {
|
|
66
|
+
const record = asRecord(block);
|
|
67
|
+
if (record?.type === "text" && typeof record.text === "string") {
|
|
68
|
+
mcpContent.push({ type: "text", text: record.text });
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (record?.type === "image" && typeof record.data === "string" && typeof record.mimeType === "string") {
|
|
72
|
+
mcpContent.push({ type: "image", data: record.data, mimeType: record.mimeType });
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
mcpContent.push({ type: "text", text: stringifyUnknown(block) });
|
|
76
|
+
}
|
|
77
|
+
return mcpContent.length > 0 ? mcpContent : [{ type: "text", text: "" }];
|
|
78
|
+
}
|
|
79
|
+
export function asToolResultMessage(value) {
|
|
80
|
+
return value.role === "toolResult" ? value : undefined;
|
|
81
|
+
}
|
|
82
|
+
export function containsKnownMcpToolName(value, knownMcpToolNames, depth = 0) {
|
|
83
|
+
if (depth > 4)
|
|
84
|
+
return false;
|
|
85
|
+
if (Array.isArray(value))
|
|
86
|
+
return value.some((entry) => containsKnownMcpToolName(entry, knownMcpToolNames, depth + 1));
|
|
87
|
+
const record = asRecord(value);
|
|
88
|
+
if (!record)
|
|
89
|
+
return false;
|
|
90
|
+
for (const field of ["tool", "toolName", "name", "mcpToolName", "serverToolName"]) {
|
|
91
|
+
const fieldValue = record[field];
|
|
92
|
+
if (typeof fieldValue === "string" && knownMcpToolNames.has(fieldValue))
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
for (const nestedField of ["args", "arguments", "input"]) {
|
|
96
|
+
if (containsKnownMcpToolName(record[nestedField], knownMcpToolNames, depth + 1))
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|