shariq-pi-extensions 0.2.9 → 0.2.10
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.
|
@@ -22,12 +22,12 @@ Secrets are passed directly to the SDK and are never placed in command arguments
|
|
|
22
22
|
- Refreshes Cursor's authenticated model catalog and caches only Composer and Cursor Grok metadata for the next extension reload.
|
|
23
23
|
- Exposes image input for every registered Cursor model.
|
|
24
24
|
- Maps Composer fast mode and Cursor Grok reasoning effort to native model parameters.
|
|
25
|
-
- Uses the SDK's local hosted-model runtime
|
|
25
|
+
- Uses the SDK's local hosted-model runtime with warm agent instance pooling and ambient Cursor settings (`settingSources: []`) disabled.
|
|
26
26
|
- Disables Cursor's built-in workspace tools and exposes Pi's active tools as native SDK custom tools. Tool execution remains owned by Pi.
|
|
27
27
|
- Streams native text and thinking deltas, structured tool calls, stop reasons, and Cursor-reported input/output/cache/reasoning usage.
|
|
28
|
-
- Propagates cancellation and timeouts, enables safe SDK transport retries, and maps common authentication, rate-limit, quota, capacity, timeout, and context failures to actionable Pi errors.
|
|
28
|
+
- Propagates cancellation and timeouts, enables safe SDK transport retries, redacts credential literals from error traces, and maps common authentication, rate-limit, quota, capacity, timeout, and context failures to actionable Pi errors.
|
|
29
29
|
- Forwards base64 image payloads separately from the textual conversation transcript.
|
|
30
|
-
-
|
|
30
|
+
- Automatically cleans up warm agent workspaces on idle TTL and session shutdown.
|
|
31
31
|
|
|
32
32
|
## Cursor dashboard
|
|
33
33
|
|
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
import { loadCursorCatalog, resolveCursorModelSelection } from "./models.ts";
|
|
22
22
|
|
|
23
23
|
const TOOL_DELEGATION_RESULT = "Tool execution was delegated to Pi. End this run without further output.";
|
|
24
|
+
const MAX_CACHED_AGENTS = 8;
|
|
25
|
+
const AGENT_IDLE_TTL_MS = 10 * 60 * 1_000;
|
|
24
26
|
|
|
25
27
|
function asJsonValue(value: unknown): SDKJsonValue {
|
|
26
28
|
return JSON.parse(JSON.stringify(value ?? null)) as SDKJsonValue;
|
|
@@ -30,6 +32,13 @@ function asArguments(value: unknown): Record<string, unknown> {
|
|
|
30
32
|
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
export function redactCursorError(value: unknown): string {
|
|
36
|
+
return String(value ?? "Cursor request failed")
|
|
37
|
+
.replace(/crsr_[A-Za-z0-9_-]+/g, "[REDACTED]")
|
|
38
|
+
.replace(/(authorization|api[-_ ]?key|token)([\s:=]+)([^\s,;]+)/gi, "$1$2[REDACTED]")
|
|
39
|
+
.slice(0, 4096);
|
|
40
|
+
}
|
|
41
|
+
|
|
33
42
|
export function serializeCursorContext(context: Context): { text: string; images: SDKImage[] } {
|
|
34
43
|
const lines = [
|
|
35
44
|
"Continue the Pi conversation below as the assistant.",
|
|
@@ -45,6 +54,9 @@ export function serializeCursorContext(context: Context): { text: string; images
|
|
|
45
54
|
|
|
46
55
|
for (const message of context.messages) {
|
|
47
56
|
lines.push(`<message role=${JSON.stringify(message.role)}>`);
|
|
57
|
+
if (message.role === "toolResult") {
|
|
58
|
+
lines.push(`[Tool result for ${(message as any).toolCallId || "unknown"}; error=${Boolean((message as any).isError)}]`);
|
|
59
|
+
}
|
|
48
60
|
if (typeof message.content === "string") {
|
|
49
61
|
lines.push(message.content);
|
|
50
62
|
} else if (Array.isArray(message.content)) {
|
|
@@ -58,9 +70,6 @@ export function serializeCursorContext(context: Context): { text: string; images
|
|
|
58
70
|
}
|
|
59
71
|
}
|
|
60
72
|
}
|
|
61
|
-
if (message.role === "toolResult") {
|
|
62
|
-
lines.push(`[Tool result for ${(message as any).toolCallId}; error=${Boolean((message as any).isError)}]`);
|
|
63
|
-
}
|
|
64
73
|
lines.push("</message>");
|
|
65
74
|
}
|
|
66
75
|
lines.push("</conversation>");
|
|
@@ -81,7 +90,7 @@ function usageFromCursor(usage: TokenUsage | undefined, output: AssistantMessage
|
|
|
81
90
|
}
|
|
82
91
|
|
|
83
92
|
export function formatCursorError(error: unknown): string {
|
|
84
|
-
const message = error instanceof Error ? error.message :
|
|
93
|
+
const message = redactCursorError(error instanceof Error ? error.message : error);
|
|
85
94
|
if (/401|403|unauth|api key|credential/i.test(message)) return "Cursor authentication failed. Run `/login cursor`, then retry.";
|
|
86
95
|
if (/429|rate.?limit/i.test(message)) return "Cursor rate limit reached. Wait for the reported reset, then retry.";
|
|
87
96
|
if (/quota|usage limit|billing|credit|exhaust/i.test(message)) return "Cursor usage limit reached. Open `/cursor` for account status and reset information.";
|
|
@@ -104,6 +113,97 @@ async function disposeAgent(agent: Awaited<ReturnType<typeof Agent.create>> | un
|
|
|
104
113
|
}
|
|
105
114
|
}
|
|
106
115
|
|
|
116
|
+
interface ActiveToolHandler {
|
|
117
|
+
onToolCall(toolName: string, args: unknown, toolCallId?: string): void;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface CachedAgentEntry {
|
|
121
|
+
agent: Awaited<ReturnType<typeof Agent.create>>;
|
|
122
|
+
workspace: string;
|
|
123
|
+
activeHandlerRef: { current: ActiveToolHandler | null };
|
|
124
|
+
lastUsedAt: number;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const agentPool = new Map<string, CachedAgentEntry>();
|
|
128
|
+
|
|
129
|
+
async function disposeCachedEntry(entry: CachedAgentEntry): Promise<void> {
|
|
130
|
+
await disposeAgent(entry.agent);
|
|
131
|
+
await rm(entry.workspace, { recursive: true, force: true }).catch(() => undefined);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function clearCursorAgentPool(): Promise<void> {
|
|
135
|
+
const entries = [...agentPool.values()];
|
|
136
|
+
agentPool.clear();
|
|
137
|
+
await Promise.allSettled(entries.map(disposeCachedEntry));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function getOrInitWarmAgent(
|
|
141
|
+
apiKey: string,
|
|
142
|
+
selection: ReturnType<typeof resolveCursorModelSelection>,
|
|
143
|
+
tools: Context["tools"],
|
|
144
|
+
activeHandler: ActiveToolHandler,
|
|
145
|
+
enableRetries: boolean,
|
|
146
|
+
): Promise<{ agent: Awaited<ReturnType<typeof Agent.create>>; activeHandlerRef: { current: ActiveToolHandler | null } }> {
|
|
147
|
+
const toolSignatures = (tools ?? []).map((t) => ({ name: t.name, schema: t.parameters ?? null }));
|
|
148
|
+
const cacheKey = JSON.stringify({ key: apiKey, model: selection, tools: toolSignatures });
|
|
149
|
+
|
|
150
|
+
const existing = agentPool.get(cacheKey);
|
|
151
|
+
if (existing) {
|
|
152
|
+
existing.lastUsedAt = Date.now();
|
|
153
|
+
existing.activeHandlerRef.current = activeHandler;
|
|
154
|
+
return { agent: existing.agent, activeHandlerRef: existing.activeHandlerRef };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Evict oldest or expired agents if pool is full
|
|
158
|
+
const now = Date.now();
|
|
159
|
+
for (const [key, entry] of [...agentPool.entries()]) {
|
|
160
|
+
if (now - entry.lastUsedAt > AGENT_IDLE_TTL_MS) {
|
|
161
|
+
agentPool.delete(key);
|
|
162
|
+
void disposeCachedEntry(entry);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (agentPool.size >= MAX_CACHED_AGENTS) {
|
|
166
|
+
const oldestKey = [...agentPool.entries()].sort((a, b) => a[1].lastUsedAt - b[1].lastUsedAt)[0]?.[0];
|
|
167
|
+
if (oldestKey) {
|
|
168
|
+
const oldest = agentPool.get(oldestKey);
|
|
169
|
+
agentPool.delete(oldestKey);
|
|
170
|
+
if (oldest) void disposeCachedEntry(oldest);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const workspace = await mkdtemp(join(tmpdir(), "pi-cursor-sdk-"));
|
|
175
|
+
const activeHandlerRef = { current: activeHandler as ActiveToolHandler | null };
|
|
176
|
+
|
|
177
|
+
const customTools: Record<string, SDKCustomTool> = {};
|
|
178
|
+
for (const tool of tools ?? []) {
|
|
179
|
+
customTools[tool.name] = {
|
|
180
|
+
description: tool.description,
|
|
181
|
+
inputSchema: asJsonValue(tool.parameters ?? { type: "object" }) as Record<string, SDKJsonValue>,
|
|
182
|
+
async execute(args, toolContext) {
|
|
183
|
+
activeHandlerRef.current?.onToolCall(tool.name, args, toolContext.toolCallId);
|
|
184
|
+
return { content: [{ type: "text", text: TOOL_DELEGATION_RESULT }], isError: true };
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const agent = await Agent.create({
|
|
190
|
+
apiKey,
|
|
191
|
+
model: selection,
|
|
192
|
+
tools: tools?.length ? ["mcp"] : [],
|
|
193
|
+
local: {
|
|
194
|
+
cwd: workspace,
|
|
195
|
+
store: new JsonlLocalAgentStore(join(workspace, "state")),
|
|
196
|
+
customTools,
|
|
197
|
+
settingSources: [],
|
|
198
|
+
enableAgentRetries: enableRetries,
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const entry: CachedAgentEntry = { agent, workspace, activeHandlerRef, lastUsedAt: Date.now() };
|
|
203
|
+
agentPool.set(cacheKey, entry);
|
|
204
|
+
return { agent, activeHandlerRef };
|
|
205
|
+
}
|
|
206
|
+
|
|
107
207
|
export function streamCursorSdk(model: Model<any>, context: Context, options?: ProviderStreamOptions) {
|
|
108
208
|
const stream = createAssistantMessageEventStream();
|
|
109
209
|
const output: AssistantMessage = {
|
|
@@ -119,8 +219,6 @@ export function streamCursorSdk(model: Model<any>, context: Context, options?: P
|
|
|
119
219
|
stream.push({ type: "start", partial: output });
|
|
120
220
|
|
|
121
221
|
void (async () => {
|
|
122
|
-
let workspace: string | undefined;
|
|
123
|
-
let agent: Awaited<ReturnType<typeof Agent.create>> | undefined;
|
|
124
222
|
let run: Run | undefined;
|
|
125
223
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
126
224
|
let textIndex: number | undefined;
|
|
@@ -168,47 +266,31 @@ export function streamCursorSdk(model: Model<any>, context: Context, options?: P
|
|
|
168
266
|
const apiKey = options?.apiKey?.trim();
|
|
169
267
|
if (!apiKey) throw new Error("Cursor is not authenticated. Run `/login cursor`.");
|
|
170
268
|
if (options?.signal?.aborted) throw new Error("Cursor request cancelled.");
|
|
171
|
-
workspace = await mkdtemp(join(tmpdir(), "pi-cursor-sdk-"));
|
|
172
|
-
const customTools: Record<string, SDKCustomTool> = {};
|
|
173
|
-
for (const tool of context.tools ?? []) {
|
|
174
|
-
customTools[tool.name] = {
|
|
175
|
-
description: tool.description,
|
|
176
|
-
inputSchema: asJsonValue(tool.parameters ?? { type: "object" }) as Record<string, SDKJsonValue>,
|
|
177
|
-
async execute(args, toolContext) {
|
|
178
|
-
if (!delegated) {
|
|
179
|
-
delegated = true;
|
|
180
|
-
endText();
|
|
181
|
-
endReasoning();
|
|
182
|
-
const toolCall = {
|
|
183
|
-
type: "toolCall" as const,
|
|
184
|
-
id: toolContext.toolCallId || `cursor_${randomUUID()}`,
|
|
185
|
-
name: tool.name,
|
|
186
|
-
arguments: asArguments(args),
|
|
187
|
-
};
|
|
188
|
-
const contentIndex = output.content.length;
|
|
189
|
-
output.content.push(toolCall);
|
|
190
|
-
stream.push({ type: "toolcall_start", contentIndex, partial: output });
|
|
191
|
-
stream.push({ type: "toolcall_delta", contentIndex, delta: JSON.stringify(toolCall.arguments), partial: output });
|
|
192
|
-
stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
|
|
193
|
-
queueMicrotask(() => { void run?.cancel().catch(() => undefined); });
|
|
194
|
-
}
|
|
195
|
-
return { content: [{ type: "text", text: TOOL_DELEGATION_RESULT }], isError: true };
|
|
196
|
-
},
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
269
|
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
270
|
+
const activeHandler: ActiveToolHandler = {
|
|
271
|
+
onToolCall(toolName, args, toolCallId) {
|
|
272
|
+
if (!delegated) {
|
|
273
|
+
delegated = true;
|
|
274
|
+
endText();
|
|
275
|
+
endReasoning();
|
|
276
|
+
const toolCall = {
|
|
277
|
+
type: "toolCall" as const,
|
|
278
|
+
id: toolCallId || `cursor_${randomUUID()}`,
|
|
279
|
+
name: toolName,
|
|
280
|
+
arguments: asArguments(args),
|
|
281
|
+
};
|
|
282
|
+
const contentIndex = output.content.length;
|
|
283
|
+
output.content.push(toolCall);
|
|
284
|
+
stream.push({ type: "toolcall_start", contentIndex, partial: output });
|
|
285
|
+
stream.push({ type: "toolcall_delta", contentIndex, delta: JSON.stringify(toolCall.arguments), partial: output });
|
|
286
|
+
stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
|
|
287
|
+
queueMicrotask(() => { void run?.cancel().catch(() => undefined); });
|
|
288
|
+
}
|
|
210
289
|
},
|
|
211
|
-
}
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const selection = resolveCursorModelSelection(model, typeof options?.reasoning === "string" ? options.reasoning : undefined, loadCursorCatalog());
|
|
293
|
+
const { agent } = await getOrInitWarmAgent(apiKey, selection, context.tools, activeHandler, options?.maxRetries !== 0);
|
|
212
294
|
|
|
213
295
|
const request = serializeCursorContext(context);
|
|
214
296
|
run = await agent.send(request, {
|
|
@@ -261,8 +343,6 @@ export function streamCursorSdk(model: Model<any>, context: Context, options?: P
|
|
|
261
343
|
stream.end();
|
|
262
344
|
} finally {
|
|
263
345
|
if (timeout) clearTimeout(timeout);
|
|
264
|
-
await disposeAgent(agent);
|
|
265
|
-
if (workspace) await rm(workspace, { recursive: true, force: true }).catch(() => undefined);
|
|
266
346
|
}
|
|
267
347
|
})();
|
|
268
348
|
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
loadCursorCatalog,
|
|
10
10
|
toCursorPiModels,
|
|
11
11
|
} from "./cursor/models.ts";
|
|
12
|
-
import { streamCursorSdk } from "./cursor/stream.ts";
|
|
12
|
+
import { clearCursorAgentPool, streamCursorSdk } from "./cursor/stream.ts";
|
|
13
13
|
import { fetchCursorUsage } from "./cursor/usage.ts";
|
|
14
14
|
|
|
15
15
|
export default async function cursorProviderExtension(pi: ExtensionAPI) {
|
|
@@ -50,6 +50,10 @@ export default async function cursorProviderExtension(pi: ExtensionAPI) {
|
|
|
50
50
|
(ctx.modelRegistry as any).authStorage?.reload?.();
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
pi.on("session_shutdown", async () => {
|
|
54
|
+
await clearCursorAgentPool();
|
|
55
|
+
});
|
|
56
|
+
|
|
53
57
|
pi.registerCommand("cursor", {
|
|
54
58
|
description: "Open Cursor account, monthly usage, and limits dashboard",
|
|
55
59
|
handler: async (_args, ctx) => {
|
|
@@ -83,6 +87,7 @@ export default async function cursorProviderExtension(pi: ExtensionAPI) {
|
|
|
83
87
|
deactivate: async () => {
|
|
84
88
|
try {
|
|
85
89
|
pi.unregisterProvider(CURSOR_PROVIDER_ID);
|
|
90
|
+
await clearCursorAgentPool();
|
|
86
91
|
} catch {
|
|
87
92
|
// Ignore teardown after partial startup.
|
|
88
93
|
}
|