@rama_nigg/open-cursor 2.3.19 → 2.4.0
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/README.md +50 -48
- package/dist/cli/discover.js +177 -8
- package/dist/cli/mcptool.js +234 -5649
- package/dist/cli/opencode-cursor.js +930 -50
- package/dist/index.js +899 -5952
- package/dist/plugin-entry.js +877 -5932
- package/package.json +4 -2
- package/src/auth.ts +3 -1
- package/src/cli/model-discovery.ts +3 -2
- package/src/cli/opencode-cursor.ts +402 -23
- package/src/client/simple.ts +6 -3
- package/src/mcp/config.ts +49 -0
- package/src/mcp/tool-bridge.ts +1 -1
- package/src/models/discovery.ts +3 -2
- package/src/models/pricing.ts +196 -0
- package/src/models/variants.ts +446 -0
- package/src/plugin-toggle.ts +7 -1
- package/src/plugin.ts +167 -36
- package/src/provider/boundary.ts +10 -0
- package/src/provider/tool-loop-guard.ts +8 -3
- package/src/proxy/formatter.ts +30 -12
- package/src/proxy/prompt-builder.ts +10 -1
- package/src/streaming/types.ts +5 -0
- package/src/tools/defaults.ts +166 -0
- package/src/tools/executors/cli.ts +1 -0
- package/src/usage.ts +112 -0
- package/src/utils/binary.ts +57 -0
package/src/plugin.ts
CHANGED
|
@@ -10,7 +10,12 @@ import { startCursorOAuth } from "./auth";
|
|
|
10
10
|
import { LineBuffer } from "./streaming/line-buffer.js";
|
|
11
11
|
import { StreamToSseConverter, formatSseDone } from "./streaming/openai-sse.js";
|
|
12
12
|
import { parseStreamJsonLine } from "./streaming/parser.js";
|
|
13
|
-
import { extractText, extractThinking, isAssistantText, isThinking } from "./streaming/types.js";
|
|
13
|
+
import { extractText, extractThinking, isAssistantText, isResult, isThinking } from "./streaming/types.js";
|
|
14
|
+
import {
|
|
15
|
+
createChatCompletionUsageChunk,
|
|
16
|
+
extractOpenAiUsageFromResult,
|
|
17
|
+
type OpenAiUsage,
|
|
18
|
+
} from "./usage.js";
|
|
14
19
|
import { createLogger } from "./utils/logger";
|
|
15
20
|
import { RequestPerf } from "./utils/perf";
|
|
16
21
|
import { parseAgentError, formatErrorForUser, stripAnsi } from "./utils/errors";
|
|
@@ -25,7 +30,7 @@ import { ToolRouter } from "./tools/router.js";
|
|
|
25
30
|
import { SkillLoader } from "./tools/skills/loader.js";
|
|
26
31
|
import { SkillResolver } from "./tools/skills/resolver.js";
|
|
27
32
|
import { autoRefreshModels } from "./models/sync.js";
|
|
28
|
-
import { readMcpConfigs } from "./mcp/config.js";
|
|
33
|
+
import { readMcpConfigs, readSubagentNames } from "./mcp/config.js";
|
|
29
34
|
import { McpClientManager } from "./mcp/client-manager.js";
|
|
30
35
|
import { buildMcpToolHookEntries, buildMcpToolDefinitions } from "./mcp/tool-bridge.js";
|
|
31
36
|
import { createOpencodeClient } from "@opencode-ai/sdk";
|
|
@@ -52,6 +57,7 @@ import {
|
|
|
52
57
|
parseToolLoopMaxRepeat,
|
|
53
58
|
type ToolLoopGuard,
|
|
54
59
|
} from "./provider/tool-loop-guard.js";
|
|
60
|
+
import { resolveCursorAgentBinary } from "./utils/binary.js";
|
|
55
61
|
|
|
56
62
|
const log = createLogger("plugin");
|
|
57
63
|
|
|
@@ -92,6 +98,7 @@ export function buildAvailableToolsSystemMessage(
|
|
|
92
98
|
lastToolMap: Array<{ id: string; name: string }>,
|
|
93
99
|
mcpToolDefs: any[],
|
|
94
100
|
mcpToolSummaries?: McpToolSummary[],
|
|
101
|
+
subagentNames: string[] = [],
|
|
95
102
|
): string | null {
|
|
96
103
|
const parts: string[] = [];
|
|
97
104
|
|
|
@@ -132,6 +139,12 @@ export function buildAvailableToolsSystemMessage(
|
|
|
132
139
|
parts.push(lines.join("\n"));
|
|
133
140
|
}
|
|
134
141
|
|
|
142
|
+
if (subagentNames.length > 0) {
|
|
143
|
+
parts.push(
|
|
144
|
+
`When calling the task tool, set subagent_type to one of: ${subagentNames.join(", ")}. Do not omit this parameter.`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
135
148
|
return parts.length > 0 ? parts.join("\n\n") : null;
|
|
136
149
|
}
|
|
137
150
|
|
|
@@ -184,7 +197,7 @@ function canonicalizePathForCompare(pathValue: string): string {
|
|
|
184
197
|
normalizedPath = resolvedPath;
|
|
185
198
|
}
|
|
186
199
|
|
|
187
|
-
if (process.platform === "darwin") {
|
|
200
|
+
if (process.platform === "darwin" || process.platform === "win32") {
|
|
188
201
|
return normalizedPath.toLowerCase();
|
|
189
202
|
}
|
|
190
203
|
|
|
@@ -212,37 +225,76 @@ function isNonConfigPath(pathValue: string): boolean {
|
|
|
212
225
|
return !isWithinPath(getOpenCodeConfigPrefix(), pathValue);
|
|
213
226
|
}
|
|
214
227
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
228
|
+
// Filesystem roots are never a meaningful workspace: accepting "/" (or a bare
|
|
229
|
+
// Windows drive root like "C:\") makes every tool treat the whole machine as
|
|
230
|
+
// the project, which is both unsafe and a common symptom of a daemon that
|
|
231
|
+
// was launched without a real cwd (e.g. systemd unit without WorkingDirectory).
|
|
232
|
+
export function isRootPath(pathValue: string): boolean {
|
|
233
|
+
if (!pathValue) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
const resolved = resolve(pathValue);
|
|
237
|
+
if (resolved === "/") {
|
|
238
|
+
return true;
|
|
221
239
|
}
|
|
240
|
+
return /^[A-Za-z]:[\\/]?$/.test(resolved);
|
|
241
|
+
}
|
|
222
242
|
|
|
223
|
-
|
|
224
|
-
if (
|
|
225
|
-
return
|
|
243
|
+
function isAcceptableWorkspace(pathValue: string, configPrefix: string): boolean {
|
|
244
|
+
if (!pathValue) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
if (isRootPath(pathValue)) {
|
|
248
|
+
return false;
|
|
226
249
|
}
|
|
250
|
+
if (isWithinPath(configPrefix, pathValue)) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
227
255
|
|
|
256
|
+
const SESSION_WORKSPACE_CACHE_LIMIT = 200;
|
|
257
|
+
|
|
258
|
+
export function resolveWorkspaceDirectory(
|
|
259
|
+
worktree: string | undefined,
|
|
260
|
+
directory: string | undefined,
|
|
261
|
+
): string {
|
|
228
262
|
const configPrefix = getOpenCodeConfigPrefix();
|
|
229
263
|
|
|
264
|
+
const envWorkspace = resolveCandidate(process.env.CURSOR_ACP_WORKSPACE);
|
|
265
|
+
if (envWorkspace && !isRootPath(envWorkspace)) {
|
|
266
|
+
return envWorkspace;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const envProjectDir = resolveCandidate(process.env.OPENCODE_CURSOR_PROJECT_DIR);
|
|
270
|
+
if (envProjectDir && !isRootPath(envProjectDir)) {
|
|
271
|
+
return envProjectDir;
|
|
272
|
+
}
|
|
273
|
+
|
|
230
274
|
const worktreeCandidate = resolveCandidate(worktree);
|
|
231
|
-
if (worktreeCandidate
|
|
275
|
+
if (isAcceptableWorkspace(worktreeCandidate, configPrefix)) {
|
|
232
276
|
return worktreeCandidate;
|
|
233
277
|
}
|
|
234
278
|
|
|
235
279
|
const dirCandidate = resolveCandidate(directory);
|
|
236
|
-
if (dirCandidate
|
|
280
|
+
if (isAcceptableWorkspace(dirCandidate, configPrefix)) {
|
|
237
281
|
return dirCandidate;
|
|
238
282
|
}
|
|
239
283
|
|
|
240
284
|
const cwd = resolve(process.cwd());
|
|
241
|
-
if (cwd
|
|
285
|
+
if (isAcceptableWorkspace(cwd, configPrefix)) {
|
|
242
286
|
return cwd;
|
|
243
287
|
}
|
|
244
288
|
|
|
245
|
-
|
|
289
|
+
// Fall back to the user's home directory rather than "/" when every other
|
|
290
|
+
// signal is unusable. $HOME is always writable for the current user and
|
|
291
|
+
// keeps tool scopes sane even when the daemon was spawned from root.
|
|
292
|
+
const home = resolveCandidate(homedir());
|
|
293
|
+
if (home && !isRootPath(home)) {
|
|
294
|
+
return home;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return configPrefix;
|
|
246
298
|
}
|
|
247
299
|
|
|
248
300
|
type ProxyRuntimeState = {
|
|
@@ -251,7 +303,11 @@ type ProxyRuntimeState = {
|
|
|
251
303
|
};
|
|
252
304
|
|
|
253
305
|
export function normalizeWorkspaceForCompare(pathValue: string): string {
|
|
254
|
-
|
|
306
|
+
const resolved = resolve(pathValue);
|
|
307
|
+
if (process.platform === "darwin" || process.platform === "win32") {
|
|
308
|
+
return resolved.toLowerCase();
|
|
309
|
+
}
|
|
310
|
+
return resolved;
|
|
255
311
|
}
|
|
256
312
|
|
|
257
313
|
export function isReusableProxyHealthPayload(payload: any, workspaceDirectory: string): boolean {
|
|
@@ -314,7 +370,12 @@ export function resolveChatParamTools(
|
|
|
314
370
|
return PROVIDER_BOUNDARY.resolveChatParamTools(mode, existingTools, refreshedTools);
|
|
315
371
|
}
|
|
316
372
|
|
|
317
|
-
function createChatCompletionResponse(
|
|
373
|
+
function createChatCompletionResponse(
|
|
374
|
+
model: string,
|
|
375
|
+
content: string,
|
|
376
|
+
reasoningContent?: string,
|
|
377
|
+
usage?: OpenAiUsage,
|
|
378
|
+
) {
|
|
318
379
|
const message: { role: "assistant"; content: string; reasoning_content?: string } = {
|
|
319
380
|
role: "assistant",
|
|
320
381
|
content,
|
|
@@ -324,7 +385,18 @@ function createChatCompletionResponse(model: string, content: string, reasoningC
|
|
|
324
385
|
message.reasoning_content = reasoningContent;
|
|
325
386
|
}
|
|
326
387
|
|
|
327
|
-
|
|
388
|
+
const response: {
|
|
389
|
+
id: string;
|
|
390
|
+
object: string;
|
|
391
|
+
created: number;
|
|
392
|
+
model: string;
|
|
393
|
+
choices: Array<{
|
|
394
|
+
index: number;
|
|
395
|
+
message: typeof message;
|
|
396
|
+
finish_reason: string;
|
|
397
|
+
}>;
|
|
398
|
+
usage?: OpenAiUsage;
|
|
399
|
+
} = {
|
|
328
400
|
id: `cursor-acp-${Date.now()}`,
|
|
329
401
|
object: "chat.completion",
|
|
330
402
|
created: Math.floor(Date.now() / 1000),
|
|
@@ -337,6 +409,12 @@ function createChatCompletionResponse(model: string, content: string, reasoningC
|
|
|
337
409
|
},
|
|
338
410
|
],
|
|
339
411
|
};
|
|
412
|
+
|
|
413
|
+
if (usage) {
|
|
414
|
+
response.usage = usage;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return response;
|
|
340
418
|
}
|
|
341
419
|
|
|
342
420
|
function createChatCompletionChunk(id: string, created: number, model: string, deltaContent: string, done = false) {
|
|
@@ -355,11 +433,17 @@ function createChatCompletionChunk(id: string, created: number, model: string, d
|
|
|
355
433
|
};
|
|
356
434
|
}
|
|
357
435
|
|
|
358
|
-
function extractCompletionFromStream(output: string): {
|
|
436
|
+
export function extractCompletionFromStream(output: string): {
|
|
437
|
+
assistantText: string;
|
|
438
|
+
reasoningText: string;
|
|
439
|
+
usage?: OpenAiUsage;
|
|
440
|
+
} {
|
|
359
441
|
const lines = output.split("\n");
|
|
360
442
|
let assistantText = "";
|
|
361
443
|
let reasoningText = "";
|
|
444
|
+
let usage: OpenAiUsage | undefined;
|
|
362
445
|
let sawAssistantPartials = false;
|
|
446
|
+
let sawThinkingPartials = false;
|
|
363
447
|
|
|
364
448
|
for (const line of lines) {
|
|
365
449
|
const event = parseStreamJsonLine(line);
|
|
@@ -383,12 +467,22 @@ function extractCompletionFromStream(output: string): { assistantText: string; r
|
|
|
383
467
|
if (isThinking(event)) {
|
|
384
468
|
const thinking = extractThinking(event);
|
|
385
469
|
if (thinking) {
|
|
386
|
-
|
|
470
|
+
const isPartial = typeof (event as any).timestamp_ms === "number";
|
|
471
|
+
if (isPartial) {
|
|
472
|
+
reasoningText += thinking;
|
|
473
|
+
sawThinkingPartials = true;
|
|
474
|
+
} else if (!sawThinkingPartials) {
|
|
475
|
+
reasoningText = thinking;
|
|
476
|
+
}
|
|
387
477
|
}
|
|
388
478
|
}
|
|
479
|
+
|
|
480
|
+
if (isResult(event)) {
|
|
481
|
+
usage = extractOpenAiUsageFromResult(event) ?? usage;
|
|
482
|
+
}
|
|
389
483
|
}
|
|
390
484
|
|
|
391
|
-
return { assistantText, reasoningText };
|
|
485
|
+
return { assistantText, reasoningText, usage };
|
|
392
486
|
}
|
|
393
487
|
|
|
394
488
|
function formatToolUpdateEvent(update: ToolUpdate): string {
|
|
@@ -563,7 +657,7 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
563
657
|
if (url.pathname === "/v1/models" || url.pathname === "/models") {
|
|
564
658
|
try {
|
|
565
659
|
const bunAny = globalThis as any;
|
|
566
|
-
const proc = bunAny.Bun.spawn([
|
|
660
|
+
const proc = bunAny.Bun.spawn([resolveCursorAgentBinary(), "models"], {
|
|
567
661
|
stdout: "pipe",
|
|
568
662
|
stderr: "pipe",
|
|
569
663
|
});
|
|
@@ -614,6 +708,7 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
614
708
|
// DEBUG: Log raw request structure for tool-loop investigation
|
|
615
709
|
debugLogToFile("raw_request_body", {
|
|
616
710
|
model: body?.model,
|
|
711
|
+
cursorModel: body?.cursorModel,
|
|
617
712
|
stream,
|
|
618
713
|
toolCount: tools.length,
|
|
619
714
|
toolNames: tools.map((t: any) => t?.function?.name ?? t?.name ?? "unknown"),
|
|
@@ -628,9 +723,10 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
628
723
|
const toolLoopGuard = createToolLoopGuard(messages, TOOL_LOOP_MAX_REPEAT);
|
|
629
724
|
const boundaryContext = createBoundaryRuntimeContext("bun-handler");
|
|
630
725
|
|
|
631
|
-
const
|
|
632
|
-
const
|
|
633
|
-
|
|
726
|
+
const subagentNames = readSubagentNames();
|
|
727
|
+
const prompt = buildPromptFromMessages(messages, tools, subagentNames);
|
|
728
|
+
const model = boundaryContext.run("resolveRuntimeModel", (boundary) =>
|
|
729
|
+
boundary.resolveRuntimeModel(body?.model, body?.cursorModel),
|
|
634
730
|
);
|
|
635
731
|
const msgSummaryBun = messages.map((m: any, i: number) => {
|
|
636
732
|
const role = m?.role ?? "?";
|
|
@@ -656,7 +752,7 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
656
752
|
}
|
|
657
753
|
|
|
658
754
|
const cmd = [
|
|
659
|
-
|
|
755
|
+
resolveCursorAgentBinary(),
|
|
660
756
|
"--print",
|
|
661
757
|
"--output-format",
|
|
662
758
|
"stream-json",
|
|
@@ -757,6 +853,7 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
757
853
|
model,
|
|
758
854
|
completion.assistantText || stdout || stderr,
|
|
759
855
|
completion.reasoningText || undefined,
|
|
856
|
+
completion.usage,
|
|
760
857
|
);
|
|
761
858
|
return new Response(JSON.stringify(payload), {
|
|
762
859
|
status: 200,
|
|
@@ -778,6 +875,7 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
778
875
|
async start(controller) {
|
|
779
876
|
let streamTerminated = false;
|
|
780
877
|
let firstTokenReceived = false;
|
|
878
|
+
let usage: OpenAiUsage | undefined;
|
|
781
879
|
try {
|
|
782
880
|
const reader = (child.stdout as ReadableStream<Uint8Array>).getReader();
|
|
783
881
|
const converter = new StreamToSseConverter(model, { id, created });
|
|
@@ -832,6 +930,10 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
832
930
|
continue;
|
|
833
931
|
}
|
|
834
932
|
|
|
933
|
+
if (isResult(event)) {
|
|
934
|
+
usage = extractOpenAiUsageFromResult(event) ?? usage;
|
|
935
|
+
}
|
|
936
|
+
|
|
835
937
|
if (event.type === "tool_call") {
|
|
836
938
|
perf.mark("tool-call");
|
|
837
939
|
const result = await handleToolLoopEventWithFallback({
|
|
@@ -898,6 +1000,9 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
898
1000
|
if (!event) {
|
|
899
1001
|
continue;
|
|
900
1002
|
}
|
|
1003
|
+
if (isResult(event)) {
|
|
1004
|
+
usage = extractOpenAiUsageFromResult(event) ?? usage;
|
|
1005
|
+
}
|
|
901
1006
|
if (event.type === "tool_call") {
|
|
902
1007
|
const result = await handleToolLoopEventWithFallback({
|
|
903
1008
|
event: event as any,
|
|
@@ -986,6 +1091,10 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
986
1091
|
|
|
987
1092
|
const doneChunk = createChatCompletionChunk(id, created, model, "", true);
|
|
988
1093
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify(doneChunk)}\n\n`));
|
|
1094
|
+
if (usage) {
|
|
1095
|
+
const usageChunk = createChatCompletionUsageChunk(id, created, model, usage);
|
|
1096
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(usageChunk)}\n\n`));
|
|
1097
|
+
}
|
|
989
1098
|
controller.enqueue(encoder.encode(formatSseDone()));
|
|
990
1099
|
} finally {
|
|
991
1100
|
perf.mark("request:done");
|
|
@@ -1046,8 +1155,8 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
1046
1155
|
// Dynamic model discovery via cursor-agent models (Node.js handler)
|
|
1047
1156
|
if (url.pathname === "/v1/models" || url.pathname === "/models") {
|
|
1048
1157
|
try {
|
|
1049
|
-
const {
|
|
1050
|
-
const output =
|
|
1158
|
+
const { execFileSync } = await import("child_process");
|
|
1159
|
+
const output = execFileSync(resolveCursorAgentBinary(), ["models"], { encoding: "utf-8", timeout: 30000 });
|
|
1051
1160
|
const clean = stripAnsi(output);
|
|
1052
1161
|
const models: Array<{ id: string; object: string; created: number; owned_by: string }> = [];
|
|
1053
1162
|
for (const line of clean.split("\n")) {
|
|
@@ -1092,9 +1201,10 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
1092
1201
|
const toolLoopGuard = createToolLoopGuard(messages, TOOL_LOOP_MAX_REPEAT);
|
|
1093
1202
|
const boundaryContext = createBoundaryRuntimeContext("node-handler");
|
|
1094
1203
|
|
|
1095
|
-
const
|
|
1096
|
-
const
|
|
1097
|
-
|
|
1204
|
+
const subagentNames = readSubagentNames();
|
|
1205
|
+
const prompt = buildPromptFromMessages(messages, tools, subagentNames);
|
|
1206
|
+
const model = boundaryContext.run("resolveRuntimeModel", (boundary) =>
|
|
1207
|
+
boundary.resolveRuntimeModel(bodyData?.model, bodyData?.cursorModel),
|
|
1098
1208
|
);
|
|
1099
1209
|
const msgSummary = messages.map((m: any, i: number) => {
|
|
1100
1210
|
const role = m?.role ?? "?";
|
|
@@ -1114,7 +1224,7 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
1114
1224
|
});
|
|
1115
1225
|
|
|
1116
1226
|
const cmd = [
|
|
1117
|
-
|
|
1227
|
+
resolveCursorAgentBinary(),
|
|
1118
1228
|
"--print",
|
|
1119
1229
|
"--output-format",
|
|
1120
1230
|
"stream-json",
|
|
@@ -1128,7 +1238,10 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
1128
1238
|
cmd.push("--force");
|
|
1129
1239
|
}
|
|
1130
1240
|
|
|
1131
|
-
const child = spawn(cmd[0], cmd.slice(1), {
|
|
1241
|
+
const child = spawn(cmd[0], cmd.slice(1), {
|
|
1242
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1243
|
+
shell: process.platform === "win32",
|
|
1244
|
+
});
|
|
1132
1245
|
|
|
1133
1246
|
// Write prompt to stdin to avoid E2BIG error
|
|
1134
1247
|
child.stdin.write(prompt);
|
|
@@ -1216,6 +1329,7 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
1216
1329
|
model,
|
|
1217
1330
|
completion.assistantText || stdout || stderr,
|
|
1218
1331
|
completion.reasoningText || undefined,
|
|
1332
|
+
completion.usage,
|
|
1219
1333
|
);
|
|
1220
1334
|
|
|
1221
1335
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -1242,6 +1356,7 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
1242
1356
|
const stderrChunks: Buffer[] = [];
|
|
1243
1357
|
let streamTerminated = false;
|
|
1244
1358
|
let firstTokenReceived = false;
|
|
1359
|
+
let usage: OpenAiUsage | undefined;
|
|
1245
1360
|
child.stderr.on("data", (chunk) => {
|
|
1246
1361
|
stderrChunks.push(Buffer.from(chunk));
|
|
1247
1362
|
});
|
|
@@ -1314,6 +1429,10 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
1314
1429
|
continue;
|
|
1315
1430
|
}
|
|
1316
1431
|
|
|
1432
|
+
if (isResult(event)) {
|
|
1433
|
+
usage = extractOpenAiUsageFromResult(event) ?? usage;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1317
1436
|
if (event.type === "tool_call") {
|
|
1318
1437
|
perf.mark("tool-call");
|
|
1319
1438
|
const result = await handleToolLoopEventWithFallback({
|
|
@@ -1386,6 +1505,10 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
1386
1505
|
continue;
|
|
1387
1506
|
}
|
|
1388
1507
|
|
|
1508
|
+
if (isResult(event)) {
|
|
1509
|
+
usage = extractOpenAiUsageFromResult(event) ?? usage;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1389
1512
|
if (event.type === "tool_call") {
|
|
1390
1513
|
const result = await handleToolLoopEventWithFallback({
|
|
1391
1514
|
event: event as any,
|
|
@@ -1490,6 +1613,10 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
1490
1613
|
],
|
|
1491
1614
|
};
|
|
1492
1615
|
res.write(`data: ${JSON.stringify(doneChunk)}\n\n`);
|
|
1616
|
+
if (usage) {
|
|
1617
|
+
const usageChunk = createChatCompletionUsageChunk(id, created, model, usage);
|
|
1618
|
+
res.write(`data: ${JSON.stringify(usageChunk)}\n\n`);
|
|
1619
|
+
}
|
|
1493
1620
|
res.write(formatSseDone());
|
|
1494
1621
|
res.end();
|
|
1495
1622
|
});
|
|
@@ -1585,7 +1712,7 @@ function jsonSchemaToZod(jsonSchema: any): any {
|
|
|
1585
1712
|
}
|
|
1586
1713
|
break;
|
|
1587
1714
|
case "object":
|
|
1588
|
-
zodType = z.record(z.any());
|
|
1715
|
+
zodType = z.record(z.string(), z.any());
|
|
1589
1716
|
if (p.description) {
|
|
1590
1717
|
zodType = zodType.describe(p.description);
|
|
1591
1718
|
}
|
|
@@ -2058,7 +2185,11 @@ export const CursorPlugin: Plugin = async ({ $, directory, worktree, client, ser
|
|
|
2058
2185
|
|
|
2059
2186
|
async "experimental.chat.system.transform"(input: any, output: { system: string[] }) {
|
|
2060
2187
|
if (!toolsEnabled) return;
|
|
2061
|
-
const
|
|
2188
|
+
const subagentNames = readSubagentNames();
|
|
2189
|
+
const systemMessage = buildAvailableToolsSystemMessage(
|
|
2190
|
+
lastToolNames, lastToolMap, mcpToolDefs, mcpToolSummaries,
|
|
2191
|
+
subagentNames,
|
|
2192
|
+
);
|
|
2062
2193
|
if (!systemMessage) return;
|
|
2063
2194
|
output.system = output.system || [];
|
|
2064
2195
|
output.system.push(systemMessage);
|
package/src/provider/boundary.ts
CHANGED
|
@@ -36,6 +36,7 @@ export interface ProviderBoundary {
|
|
|
36
36
|
): ToolLoopFlags;
|
|
37
37
|
matchesProvider(inputModel: any): boolean;
|
|
38
38
|
normalizeRuntimeModel(model: unknown): string;
|
|
39
|
+
resolveRuntimeModel(model: unknown, cursorModel: unknown): string;
|
|
39
40
|
applyChatParamDefaults(
|
|
40
41
|
output: any,
|
|
41
42
|
proxyBaseURL: string | undefined,
|
|
@@ -137,6 +138,15 @@ function createSharedBoundary(
|
|
|
137
138
|
return raw;
|
|
138
139
|
},
|
|
139
140
|
|
|
141
|
+
resolveRuntimeModel(model, cursorModel) {
|
|
142
|
+
const rawCursorModel = typeof cursorModel === "string" ? cursorModel.trim() : "";
|
|
143
|
+
if (rawCursorModel.length > 0) {
|
|
144
|
+
return this.normalizeRuntimeModel(rawCursorModel);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return this.normalizeRuntimeModel(model);
|
|
148
|
+
},
|
|
149
|
+
|
|
140
150
|
applyChatParamDefaults(output, proxyBaseURL, defaultBaseURL, defaultApiKey) {
|
|
141
151
|
output.options = output.options || {};
|
|
142
152
|
output.options.baseURL = proxyBaseURL || defaultBaseURL;
|
|
@@ -44,6 +44,7 @@ const EXPLORATION_TOOLS = new Set([
|
|
|
44
44
|
"bash",
|
|
45
45
|
"shell",
|
|
46
46
|
"webfetch",
|
|
47
|
+
"task",
|
|
47
48
|
]);
|
|
48
49
|
|
|
49
50
|
export interface ToolLoopGuardDecision {
|
|
@@ -511,16 +512,20 @@ function evaluateWithFingerprints(
|
|
|
511
512
|
};
|
|
512
513
|
}
|
|
513
514
|
|
|
515
|
+
const isExplorationTool = EXPLORATION_TOOLS.has(toolName.toLowerCase());
|
|
516
|
+
const effectiveMaxRepeat = isExplorationTool
|
|
517
|
+
? maxRepeat * EXPLORATION_LIMIT_MULTIPLIER
|
|
518
|
+
: maxRepeat;
|
|
519
|
+
|
|
514
520
|
const strictRepeatCount = (strictCounts.get(strictFingerprint) ?? 0) + 1;
|
|
515
521
|
strictCounts.set(strictFingerprint, strictRepeatCount);
|
|
516
|
-
const strictTriggered = strictRepeatCount >
|
|
522
|
+
const strictTriggered = strictRepeatCount > effectiveMaxRepeat;
|
|
517
523
|
|
|
518
|
-
const isExplorationTool = EXPLORATION_TOOLS.has(toolName.toLowerCase());
|
|
519
524
|
if (isExplorationTool) {
|
|
520
525
|
return {
|
|
521
526
|
fingerprint: strictFingerprint,
|
|
522
527
|
repeatCount: strictRepeatCount,
|
|
523
|
-
maxRepeat,
|
|
528
|
+
maxRepeat: effectiveMaxRepeat,
|
|
524
529
|
errorClass,
|
|
525
530
|
triggered: strictTriggered,
|
|
526
531
|
tracked: true,
|
package/src/proxy/formatter.ts
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import type { OpenAiUsage } from "../usage.js";
|
|
2
|
+
|
|
3
|
+
export function createChatCompletionResponse(
|
|
4
|
+
model: string,
|
|
5
|
+
content: string,
|
|
6
|
+
usage?: OpenAiUsage,
|
|
7
|
+
) {
|
|
8
|
+
const response: {
|
|
9
|
+
id: string;
|
|
10
|
+
object: string;
|
|
11
|
+
created: number;
|
|
12
|
+
model: string;
|
|
13
|
+
choices: Array<{
|
|
14
|
+
index: number;
|
|
15
|
+
message: { role: string; content: string };
|
|
16
|
+
finish_reason: string;
|
|
17
|
+
}>;
|
|
18
|
+
usage?: OpenAiUsage;
|
|
19
|
+
} = {
|
|
3
20
|
id: `cursor-acp-${Date.now()}`,
|
|
4
21
|
object: "chat.completion",
|
|
5
22
|
created: Math.floor(Date.now() / 1000),
|
|
@@ -8,15 +25,16 @@ export function createChatCompletionResponse(model: string, content: string) {
|
|
|
8
25
|
{
|
|
9
26
|
index: 0,
|
|
10
27
|
message: { role: "assistant", content },
|
|
11
|
-
finish_reason: "stop"
|
|
28
|
+
finish_reason: "stop",
|
|
12
29
|
}
|
|
13
30
|
],
|
|
14
|
-
usage: {
|
|
15
|
-
prompt_tokens: 0,
|
|
16
|
-
completion_tokens: 0,
|
|
17
|
-
total_tokens: 0
|
|
18
|
-
}
|
|
19
31
|
};
|
|
32
|
+
|
|
33
|
+
if (usage) {
|
|
34
|
+
response.usage = usage;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return response;
|
|
20
38
|
}
|
|
21
39
|
|
|
22
40
|
export function createChatCompletionChunk(
|
|
@@ -24,7 +42,7 @@ export function createChatCompletionChunk(
|
|
|
24
42
|
created: number,
|
|
25
43
|
model: string,
|
|
26
44
|
deltaContent: string,
|
|
27
|
-
done = false
|
|
45
|
+
done = false,
|
|
28
46
|
) {
|
|
29
47
|
return {
|
|
30
48
|
id,
|
|
@@ -35,8 +53,8 @@ export function createChatCompletionChunk(
|
|
|
35
53
|
{
|
|
36
54
|
index: 0,
|
|
37
55
|
delta: deltaContent ? { content: deltaContent } : {},
|
|
38
|
-
finish_reason: done ? "stop" : null
|
|
56
|
+
finish_reason: done ? "stop" : null,
|
|
39
57
|
}
|
|
40
|
-
]
|
|
58
|
+
],
|
|
41
59
|
};
|
|
42
|
-
}
|
|
60
|
+
}
|
|
@@ -36,7 +36,7 @@ function debugLogToFile(message: string, data: any): void {
|
|
|
36
36
|
* Handles role:"tool" result messages and assistant tool_calls that
|
|
37
37
|
* plain text flattening would silently drop.
|
|
38
38
|
*/
|
|
39
|
-
export function buildPromptFromMessages(messages: Array<any>, tools: Array<any
|
|
39
|
+
export function buildPromptFromMessages(messages: Array<any>, tools: Array<any>, subagentNames: string[] = []): string {
|
|
40
40
|
// DEBUG: Log incoming message structure to file for root cause analysis
|
|
41
41
|
const messageSummary = messages.map((m: any, i: number) => {
|
|
42
42
|
const role = m?.role ?? "?";
|
|
@@ -98,6 +98,15 @@ export function buildPromptFromMessages(messages: Array<any>, tools: Array<any>)
|
|
|
98
98
|
`SYSTEM: You have access to the following tools. When you need to use one, respond with a tool_call in the standard OpenAI format.\n` +
|
|
99
99
|
`Tool guidance: prefer write/edit for file changes; use bash mainly to run commands/tests.\n\nAvailable tools:\n${toolDescs}`,
|
|
100
100
|
);
|
|
101
|
+
const hasTaskTool = tools.some((t: any) => {
|
|
102
|
+
const name = (t?.function?.name ?? t?.name ?? "").toLowerCase();
|
|
103
|
+
return name === "task";
|
|
104
|
+
});
|
|
105
|
+
if (hasTaskTool && subagentNames.length > 0) {
|
|
106
|
+
lines.push(
|
|
107
|
+
`When calling the task tool, set subagent_type to one of: ${subagentNames.join(", ")}. Do not omit this parameter.`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
101
110
|
}
|
|
102
111
|
|
|
103
112
|
for (const message of messages) {
|
package/src/streaming/types.ts
CHANGED
|
@@ -75,8 +75,13 @@ export type StreamJsonResultEvent = {
|
|
|
75
75
|
type: "result";
|
|
76
76
|
subtype?: "success" | "error" | string;
|
|
77
77
|
timestamp?: number;
|
|
78
|
+
duration_ms?: number;
|
|
79
|
+
duration_api_ms?: number;
|
|
78
80
|
session_id?: string;
|
|
81
|
+
request_id?: string;
|
|
82
|
+
result?: string;
|
|
79
83
|
is_error?: boolean;
|
|
84
|
+
usage?: Record<string, unknown>;
|
|
80
85
|
error?: {
|
|
81
86
|
message?: string;
|
|
82
87
|
code?: number | string;
|