@rama_nigg/open-cursor 2.3.20 → 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 +6 -1
- package/dist/cli/opencode-cursor.js +930 -50
- package/dist/index.js +578 -226
- package/dist/plugin-entry.js +556 -206
- 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/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 +150 -32
- package/src/provider/boundary.ts +10 -0
- package/src/proxy/formatter.ts +30 -12
- 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";
|
|
@@ -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
|
|
|
@@ -191,7 +197,7 @@ function canonicalizePathForCompare(pathValue: string): string {
|
|
|
191
197
|
normalizedPath = resolvedPath;
|
|
192
198
|
}
|
|
193
199
|
|
|
194
|
-
if (process.platform === "darwin") {
|
|
200
|
+
if (process.platform === "darwin" || process.platform === "win32") {
|
|
195
201
|
return normalizedPath.toLowerCase();
|
|
196
202
|
}
|
|
197
203
|
|
|
@@ -219,37 +225,76 @@ function isNonConfigPath(pathValue: string): boolean {
|
|
|
219
225
|
return !isWithinPath(getOpenCodeConfigPrefix(), pathValue);
|
|
220
226
|
}
|
|
221
227
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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;
|
|
228
235
|
}
|
|
236
|
+
const resolved = resolve(pathValue);
|
|
237
|
+
if (resolved === "/") {
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
return /^[A-Za-z]:[\\/]?$/.test(resolved);
|
|
241
|
+
}
|
|
229
242
|
|
|
230
|
-
|
|
231
|
-
if (
|
|
232
|
-
return
|
|
243
|
+
function isAcceptableWorkspace(pathValue: string, configPrefix: string): boolean {
|
|
244
|
+
if (!pathValue) {
|
|
245
|
+
return false;
|
|
233
246
|
}
|
|
247
|
+
if (isRootPath(pathValue)) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
if (isWithinPath(configPrefix, pathValue)) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const SESSION_WORKSPACE_CACHE_LIMIT = 200;
|
|
234
257
|
|
|
258
|
+
export function resolveWorkspaceDirectory(
|
|
259
|
+
worktree: string | undefined,
|
|
260
|
+
directory: string | undefined,
|
|
261
|
+
): string {
|
|
235
262
|
const configPrefix = getOpenCodeConfigPrefix();
|
|
236
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
|
+
|
|
237
274
|
const worktreeCandidate = resolveCandidate(worktree);
|
|
238
|
-
if (worktreeCandidate
|
|
275
|
+
if (isAcceptableWorkspace(worktreeCandidate, configPrefix)) {
|
|
239
276
|
return worktreeCandidate;
|
|
240
277
|
}
|
|
241
278
|
|
|
242
279
|
const dirCandidate = resolveCandidate(directory);
|
|
243
|
-
if (dirCandidate
|
|
280
|
+
if (isAcceptableWorkspace(dirCandidate, configPrefix)) {
|
|
244
281
|
return dirCandidate;
|
|
245
282
|
}
|
|
246
283
|
|
|
247
284
|
const cwd = resolve(process.cwd());
|
|
248
|
-
if (cwd
|
|
285
|
+
if (isAcceptableWorkspace(cwd, configPrefix)) {
|
|
249
286
|
return cwd;
|
|
250
287
|
}
|
|
251
288
|
|
|
252
|
-
|
|
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;
|
|
253
298
|
}
|
|
254
299
|
|
|
255
300
|
type ProxyRuntimeState = {
|
|
@@ -258,7 +303,11 @@ type ProxyRuntimeState = {
|
|
|
258
303
|
};
|
|
259
304
|
|
|
260
305
|
export function normalizeWorkspaceForCompare(pathValue: string): string {
|
|
261
|
-
|
|
306
|
+
const resolved = resolve(pathValue);
|
|
307
|
+
if (process.platform === "darwin" || process.platform === "win32") {
|
|
308
|
+
return resolved.toLowerCase();
|
|
309
|
+
}
|
|
310
|
+
return resolved;
|
|
262
311
|
}
|
|
263
312
|
|
|
264
313
|
export function isReusableProxyHealthPayload(payload: any, workspaceDirectory: string): boolean {
|
|
@@ -321,7 +370,12 @@ export function resolveChatParamTools(
|
|
|
321
370
|
return PROVIDER_BOUNDARY.resolveChatParamTools(mode, existingTools, refreshedTools);
|
|
322
371
|
}
|
|
323
372
|
|
|
324
|
-
function createChatCompletionResponse(
|
|
373
|
+
function createChatCompletionResponse(
|
|
374
|
+
model: string,
|
|
375
|
+
content: string,
|
|
376
|
+
reasoningContent?: string,
|
|
377
|
+
usage?: OpenAiUsage,
|
|
378
|
+
) {
|
|
325
379
|
const message: { role: "assistant"; content: string; reasoning_content?: string } = {
|
|
326
380
|
role: "assistant",
|
|
327
381
|
content,
|
|
@@ -331,7 +385,18 @@ function createChatCompletionResponse(model: string, content: string, reasoningC
|
|
|
331
385
|
message.reasoning_content = reasoningContent;
|
|
332
386
|
}
|
|
333
387
|
|
|
334
|
-
|
|
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
|
+
} = {
|
|
335
400
|
id: `cursor-acp-${Date.now()}`,
|
|
336
401
|
object: "chat.completion",
|
|
337
402
|
created: Math.floor(Date.now() / 1000),
|
|
@@ -344,6 +409,12 @@ function createChatCompletionResponse(model: string, content: string, reasoningC
|
|
|
344
409
|
},
|
|
345
410
|
],
|
|
346
411
|
};
|
|
412
|
+
|
|
413
|
+
if (usage) {
|
|
414
|
+
response.usage = usage;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return response;
|
|
347
418
|
}
|
|
348
419
|
|
|
349
420
|
function createChatCompletionChunk(id: string, created: number, model: string, deltaContent: string, done = false) {
|
|
@@ -362,11 +433,17 @@ function createChatCompletionChunk(id: string, created: number, model: string, d
|
|
|
362
433
|
};
|
|
363
434
|
}
|
|
364
435
|
|
|
365
|
-
function extractCompletionFromStream(output: string): {
|
|
436
|
+
export function extractCompletionFromStream(output: string): {
|
|
437
|
+
assistantText: string;
|
|
438
|
+
reasoningText: string;
|
|
439
|
+
usage?: OpenAiUsage;
|
|
440
|
+
} {
|
|
366
441
|
const lines = output.split("\n");
|
|
367
442
|
let assistantText = "";
|
|
368
443
|
let reasoningText = "";
|
|
444
|
+
let usage: OpenAiUsage | undefined;
|
|
369
445
|
let sawAssistantPartials = false;
|
|
446
|
+
let sawThinkingPartials = false;
|
|
370
447
|
|
|
371
448
|
for (const line of lines) {
|
|
372
449
|
const event = parseStreamJsonLine(line);
|
|
@@ -390,12 +467,22 @@ function extractCompletionFromStream(output: string): { assistantText: string; r
|
|
|
390
467
|
if (isThinking(event)) {
|
|
391
468
|
const thinking = extractThinking(event);
|
|
392
469
|
if (thinking) {
|
|
393
|
-
|
|
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
|
+
}
|
|
394
477
|
}
|
|
395
478
|
}
|
|
479
|
+
|
|
480
|
+
if (isResult(event)) {
|
|
481
|
+
usage = extractOpenAiUsageFromResult(event) ?? usage;
|
|
482
|
+
}
|
|
396
483
|
}
|
|
397
484
|
|
|
398
|
-
return { assistantText, reasoningText };
|
|
485
|
+
return { assistantText, reasoningText, usage };
|
|
399
486
|
}
|
|
400
487
|
|
|
401
488
|
function formatToolUpdateEvent(update: ToolUpdate): string {
|
|
@@ -570,7 +657,7 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
570
657
|
if (url.pathname === "/v1/models" || url.pathname === "/models") {
|
|
571
658
|
try {
|
|
572
659
|
const bunAny = globalThis as any;
|
|
573
|
-
const proc = bunAny.Bun.spawn([
|
|
660
|
+
const proc = bunAny.Bun.spawn([resolveCursorAgentBinary(), "models"], {
|
|
574
661
|
stdout: "pipe",
|
|
575
662
|
stderr: "pipe",
|
|
576
663
|
});
|
|
@@ -621,6 +708,7 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
621
708
|
// DEBUG: Log raw request structure for tool-loop investigation
|
|
622
709
|
debugLogToFile("raw_request_body", {
|
|
623
710
|
model: body?.model,
|
|
711
|
+
cursorModel: body?.cursorModel,
|
|
624
712
|
stream,
|
|
625
713
|
toolCount: tools.length,
|
|
626
714
|
toolNames: tools.map((t: any) => t?.function?.name ?? t?.name ?? "unknown"),
|
|
@@ -637,8 +725,8 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
637
725
|
|
|
638
726
|
const subagentNames = readSubagentNames();
|
|
639
727
|
const prompt = buildPromptFromMessages(messages, tools, subagentNames);
|
|
640
|
-
const model = boundaryContext.run("
|
|
641
|
-
boundary.
|
|
728
|
+
const model = boundaryContext.run("resolveRuntimeModel", (boundary) =>
|
|
729
|
+
boundary.resolveRuntimeModel(body?.model, body?.cursorModel),
|
|
642
730
|
);
|
|
643
731
|
const msgSummaryBun = messages.map((m: any, i: number) => {
|
|
644
732
|
const role = m?.role ?? "?";
|
|
@@ -664,7 +752,7 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
664
752
|
}
|
|
665
753
|
|
|
666
754
|
const cmd = [
|
|
667
|
-
|
|
755
|
+
resolveCursorAgentBinary(),
|
|
668
756
|
"--print",
|
|
669
757
|
"--output-format",
|
|
670
758
|
"stream-json",
|
|
@@ -765,6 +853,7 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
765
853
|
model,
|
|
766
854
|
completion.assistantText || stdout || stderr,
|
|
767
855
|
completion.reasoningText || undefined,
|
|
856
|
+
completion.usage,
|
|
768
857
|
);
|
|
769
858
|
return new Response(JSON.stringify(payload), {
|
|
770
859
|
status: 200,
|
|
@@ -786,6 +875,7 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
786
875
|
async start(controller) {
|
|
787
876
|
let streamTerminated = false;
|
|
788
877
|
let firstTokenReceived = false;
|
|
878
|
+
let usage: OpenAiUsage | undefined;
|
|
789
879
|
try {
|
|
790
880
|
const reader = (child.stdout as ReadableStream<Uint8Array>).getReader();
|
|
791
881
|
const converter = new StreamToSseConverter(model, { id, created });
|
|
@@ -840,6 +930,10 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
840
930
|
continue;
|
|
841
931
|
}
|
|
842
932
|
|
|
933
|
+
if (isResult(event)) {
|
|
934
|
+
usage = extractOpenAiUsageFromResult(event) ?? usage;
|
|
935
|
+
}
|
|
936
|
+
|
|
843
937
|
if (event.type === "tool_call") {
|
|
844
938
|
perf.mark("tool-call");
|
|
845
939
|
const result = await handleToolLoopEventWithFallback({
|
|
@@ -906,6 +1000,9 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
906
1000
|
if (!event) {
|
|
907
1001
|
continue;
|
|
908
1002
|
}
|
|
1003
|
+
if (isResult(event)) {
|
|
1004
|
+
usage = extractOpenAiUsageFromResult(event) ?? usage;
|
|
1005
|
+
}
|
|
909
1006
|
if (event.type === "tool_call") {
|
|
910
1007
|
const result = await handleToolLoopEventWithFallback({
|
|
911
1008
|
event: event as any,
|
|
@@ -994,6 +1091,10 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
994
1091
|
|
|
995
1092
|
const doneChunk = createChatCompletionChunk(id, created, model, "", true);
|
|
996
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
|
+
}
|
|
997
1098
|
controller.enqueue(encoder.encode(formatSseDone()));
|
|
998
1099
|
} finally {
|
|
999
1100
|
perf.mark("request:done");
|
|
@@ -1054,8 +1155,8 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
1054
1155
|
// Dynamic model discovery via cursor-agent models (Node.js handler)
|
|
1055
1156
|
if (url.pathname === "/v1/models" || url.pathname === "/models") {
|
|
1056
1157
|
try {
|
|
1057
|
-
const {
|
|
1058
|
-
const output =
|
|
1158
|
+
const { execFileSync } = await import("child_process");
|
|
1159
|
+
const output = execFileSync(resolveCursorAgentBinary(), ["models"], { encoding: "utf-8", timeout: 30000 });
|
|
1059
1160
|
const clean = stripAnsi(output);
|
|
1060
1161
|
const models: Array<{ id: string; object: string; created: number; owned_by: string }> = [];
|
|
1061
1162
|
for (const line of clean.split("\n")) {
|
|
@@ -1102,8 +1203,8 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
1102
1203
|
|
|
1103
1204
|
const subagentNames = readSubagentNames();
|
|
1104
1205
|
const prompt = buildPromptFromMessages(messages, tools, subagentNames);
|
|
1105
|
-
const model = boundaryContext.run("
|
|
1106
|
-
boundary.
|
|
1206
|
+
const model = boundaryContext.run("resolveRuntimeModel", (boundary) =>
|
|
1207
|
+
boundary.resolveRuntimeModel(bodyData?.model, bodyData?.cursorModel),
|
|
1107
1208
|
);
|
|
1108
1209
|
const msgSummary = messages.map((m: any, i: number) => {
|
|
1109
1210
|
const role = m?.role ?? "?";
|
|
@@ -1123,7 +1224,7 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
1123
1224
|
});
|
|
1124
1225
|
|
|
1125
1226
|
const cmd = [
|
|
1126
|
-
|
|
1227
|
+
resolveCursorAgentBinary(),
|
|
1127
1228
|
"--print",
|
|
1128
1229
|
"--output-format",
|
|
1129
1230
|
"stream-json",
|
|
@@ -1137,7 +1238,10 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
1137
1238
|
cmd.push("--force");
|
|
1138
1239
|
}
|
|
1139
1240
|
|
|
1140
|
-
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
|
+
});
|
|
1141
1245
|
|
|
1142
1246
|
// Write prompt to stdin to avoid E2BIG error
|
|
1143
1247
|
child.stdin.write(prompt);
|
|
@@ -1225,6 +1329,7 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
1225
1329
|
model,
|
|
1226
1330
|
completion.assistantText || stdout || stderr,
|
|
1227
1331
|
completion.reasoningText || undefined,
|
|
1332
|
+
completion.usage,
|
|
1228
1333
|
);
|
|
1229
1334
|
|
|
1230
1335
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -1251,6 +1356,7 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
1251
1356
|
const stderrChunks: Buffer[] = [];
|
|
1252
1357
|
let streamTerminated = false;
|
|
1253
1358
|
let firstTokenReceived = false;
|
|
1359
|
+
let usage: OpenAiUsage | undefined;
|
|
1254
1360
|
child.stderr.on("data", (chunk) => {
|
|
1255
1361
|
stderrChunks.push(Buffer.from(chunk));
|
|
1256
1362
|
});
|
|
@@ -1323,6 +1429,10 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
1323
1429
|
continue;
|
|
1324
1430
|
}
|
|
1325
1431
|
|
|
1432
|
+
if (isResult(event)) {
|
|
1433
|
+
usage = extractOpenAiUsageFromResult(event) ?? usage;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1326
1436
|
if (event.type === "tool_call") {
|
|
1327
1437
|
perf.mark("tool-call");
|
|
1328
1438
|
const result = await handleToolLoopEventWithFallback({
|
|
@@ -1395,6 +1505,10 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
1395
1505
|
continue;
|
|
1396
1506
|
}
|
|
1397
1507
|
|
|
1508
|
+
if (isResult(event)) {
|
|
1509
|
+
usage = extractOpenAiUsageFromResult(event) ?? usage;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1398
1512
|
if (event.type === "tool_call") {
|
|
1399
1513
|
const result = await handleToolLoopEventWithFallback({
|
|
1400
1514
|
event: event as any,
|
|
@@ -1499,6 +1613,10 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
1499
1613
|
],
|
|
1500
1614
|
};
|
|
1501
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
|
+
}
|
|
1502
1620
|
res.write(formatSseDone());
|
|
1503
1621
|
res.end();
|
|
1504
1622
|
});
|
|
@@ -1594,7 +1712,7 @@ function jsonSchemaToZod(jsonSchema: any): any {
|
|
|
1594
1712
|
}
|
|
1595
1713
|
break;
|
|
1596
1714
|
case "object":
|
|
1597
|
-
zodType = z.record(z.any());
|
|
1715
|
+
zodType = z.record(z.string(), z.any());
|
|
1598
1716
|
if (p.description) {
|
|
1599
1717
|
zodType = zodType.describe(p.description);
|
|
1600
1718
|
}
|
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;
|
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
|
+
}
|
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;
|
package/src/tools/defaults.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ToolRegistry } from "./core/registry.js";
|
|
2
|
+
import { createLogger } from "../utils/logger.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Register default OpenCode tools in the registry
|
|
@@ -265,6 +266,10 @@ export function registerDefaultTools(registry: ToolRegistry): void {
|
|
|
265
266
|
const path = args.path as string;
|
|
266
267
|
const include = args.include as string | undefined;
|
|
267
268
|
|
|
269
|
+
if (process.platform === "win32") {
|
|
270
|
+
return nodeFallbackGrep(pattern, path, include);
|
|
271
|
+
}
|
|
272
|
+
|
|
268
273
|
const grepArgs = ["-r", "-n"];
|
|
269
274
|
if (include) {
|
|
270
275
|
grepArgs.push(`--include=${include}`);
|
|
@@ -374,6 +379,11 @@ export function registerDefaultTools(registry: ToolRegistry): void {
|
|
|
374
379
|
const path = resolvePathArg(args, "glob");
|
|
375
380
|
const cwd = path || ".";
|
|
376
381
|
const normalizedPattern = pattern.replace(/\\/g, "/");
|
|
382
|
+
|
|
383
|
+
if (process.platform === "win32") {
|
|
384
|
+
return nodeFallbackGlob(normalizedPattern, cwd);
|
|
385
|
+
}
|
|
386
|
+
|
|
377
387
|
const isPathPattern = normalizedPattern.includes("/");
|
|
378
388
|
const findArgs = [cwd, "-type", "f"];
|
|
379
389
|
if (isPathPattern) {
|
|
@@ -703,3 +713,159 @@ function coerceToString(value: unknown): string | null {
|
|
|
703
713
|
export function getDefaultToolNames(): string[] {
|
|
704
714
|
return ["bash", "read", "write", "edit", "grep", "ls", "glob", "mkdir", "rm", "stat"];
|
|
705
715
|
}
|
|
716
|
+
|
|
717
|
+
const FALLBACK_SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build"]);
|
|
718
|
+
const fallbackLog = createLogger("tools:fallback");
|
|
719
|
+
|
|
720
|
+
export async function nodeFallbackGrep(
|
|
721
|
+
pattern: string,
|
|
722
|
+
searchPath: string,
|
|
723
|
+
include?: string,
|
|
724
|
+
): Promise<string> {
|
|
725
|
+
const fs = await import("fs/promises");
|
|
726
|
+
const path = await import("path");
|
|
727
|
+
|
|
728
|
+
let regex: RegExp;
|
|
729
|
+
try {
|
|
730
|
+
regex = new RegExp(pattern);
|
|
731
|
+
} catch {
|
|
732
|
+
return "Invalid regex pattern";
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
let includeRegex: RegExp | undefined;
|
|
736
|
+
if (include) {
|
|
737
|
+
const incPattern = include.replace(/\./g, "\\.").replace(/\?/g, ".").replace(/\*/g, ".*");
|
|
738
|
+
includeRegex = new RegExp(`^${incPattern}$`);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const results: string[] = [];
|
|
742
|
+
|
|
743
|
+
async function walk(dir: string): Promise<void> {
|
|
744
|
+
if (results.length >= 100) return;
|
|
745
|
+
let entries;
|
|
746
|
+
try {
|
|
747
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
748
|
+
} catch (err: any) {
|
|
749
|
+
if (err?.code !== "ENOENT" && err?.code !== "EACCES") {
|
|
750
|
+
fallbackLog.error("Unexpected error reading directory", { dir, code: err?.code, message: err?.message });
|
|
751
|
+
}
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
for (const entry of entries) {
|
|
755
|
+
if (results.length >= 100) return;
|
|
756
|
+
const fullPath = path.join(dir, entry.name);
|
|
757
|
+
if (entry.isDirectory()) {
|
|
758
|
+
if (!FALLBACK_SKIP_DIRS.has(entry.name)) {
|
|
759
|
+
await walk(fullPath);
|
|
760
|
+
}
|
|
761
|
+
} else if (entry.isFile()) {
|
|
762
|
+
if (includeRegex && !includeRegex.test(entry.name)) continue;
|
|
763
|
+
let content: string;
|
|
764
|
+
try {
|
|
765
|
+
content = await fs.readFile(fullPath, "utf-8");
|
|
766
|
+
} catch (err: any) {
|
|
767
|
+
if (err?.code !== "ENOENT" && err?.code !== "EACCES") {
|
|
768
|
+
fallbackLog.error("Unexpected error reading file", { path: fullPath, code: err?.code, message: err?.message });
|
|
769
|
+
}
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
const lines = content.split("\n");
|
|
773
|
+
for (let i = 0; i < lines.length; i++) {
|
|
774
|
+
if (regex.test(lines[i])) {
|
|
775
|
+
results.push(`${fullPath}:${i + 1}:${lines[i]}`);
|
|
776
|
+
if (results.length >= 100) break;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
let stat;
|
|
784
|
+
try {
|
|
785
|
+
stat = await fs.stat(searchPath);
|
|
786
|
+
} catch {
|
|
787
|
+
return "Path not found";
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (stat.isFile()) {
|
|
791
|
+
let content: string;
|
|
792
|
+
try {
|
|
793
|
+
content = await fs.readFile(searchPath, "utf-8");
|
|
794
|
+
} catch (err: any) {
|
|
795
|
+
if (err?.code !== "ENOENT" && err?.code !== "EACCES") {
|
|
796
|
+
fallbackLog.error("Unexpected error reading file", { path: searchPath, code: err?.code, message: err?.message });
|
|
797
|
+
}
|
|
798
|
+
return "Path not found";
|
|
799
|
+
}
|
|
800
|
+
const lines = content.split("\n");
|
|
801
|
+
for (let i = 0; i < lines.length; i++) {
|
|
802
|
+
if (regex.test(lines[i])) {
|
|
803
|
+
results.push(`${searchPath}:${i + 1}:${lines[i]}`);
|
|
804
|
+
if (results.length >= 100) break;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
} else {
|
|
808
|
+
await walk(searchPath);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
return results.join("\n") || "No matches found";
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
export async function nodeFallbackGlob(
|
|
815
|
+
pattern: string,
|
|
816
|
+
searchPath: string,
|
|
817
|
+
): Promise<string> {
|
|
818
|
+
const fs = await import("fs/promises");
|
|
819
|
+
const path = await import("path");
|
|
820
|
+
|
|
821
|
+
const results: string[] = [];
|
|
822
|
+
const isPathPattern = pattern.includes("/");
|
|
823
|
+
|
|
824
|
+
// Handle ** before * so double-star → .* and single-star → [^/]*
|
|
825
|
+
let regexPattern = pattern
|
|
826
|
+
.replace(/\./g, "\\.")
|
|
827
|
+
.replace(/\*\*/g, "\x00") // placeholder for **
|
|
828
|
+
.replace(/\*/g, "[^/]*")
|
|
829
|
+
.replace(/\x00/g, ".*"); // restore ** as .*
|
|
830
|
+
|
|
831
|
+
let regex: RegExp;
|
|
832
|
+
try {
|
|
833
|
+
regex = isPathPattern
|
|
834
|
+
? new RegExp(`${regexPattern}$`)
|
|
835
|
+
: new RegExp(`^${regexPattern}$`);
|
|
836
|
+
} catch {
|
|
837
|
+
return "No files found";
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
async function walk(dir: string): Promise<void> {
|
|
841
|
+
if (results.length >= 50) return;
|
|
842
|
+
let entries;
|
|
843
|
+
try {
|
|
844
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
845
|
+
} catch (err: any) {
|
|
846
|
+
if (err?.code !== "ENOENT" && err?.code !== "EACCES") {
|
|
847
|
+
fallbackLog.error("Unexpected error reading directory", { dir, code: err?.code, message: err?.message });
|
|
848
|
+
}
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
for (const entry of entries) {
|
|
852
|
+
if (results.length >= 50) return;
|
|
853
|
+
const fullPath = path.join(dir, entry.name);
|
|
854
|
+
if (entry.isDirectory()) {
|
|
855
|
+
if (!FALLBACK_SKIP_DIRS.has(entry.name)) {
|
|
856
|
+
await walk(fullPath);
|
|
857
|
+
}
|
|
858
|
+
} else if (entry.isFile()) {
|
|
859
|
+
const matchTarget = isPathPattern
|
|
860
|
+
? fullPath.replace(/\\/g, "/")
|
|
861
|
+
: entry.name;
|
|
862
|
+
if (regex.test(matchTarget)) {
|
|
863
|
+
results.push(fullPath);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
await walk(searchPath);
|
|
870
|
+
return results.join("\n") || "No files found";
|
|
871
|
+
}
|