@rama_nigg/open-cursor 2.4.4 → 2.4.5
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/dist/cli/discover.js +9 -0
- package/dist/cli/opencode-cursor.js +9 -0
- package/dist/index.js +171 -218
- package/dist/plugin-entry.js +162 -189
- package/package.json +1 -1
- package/src/auth.ts +2 -2
- package/src/client/simple.ts +3 -3
- package/src/plugin.ts +90 -142
- package/src/streaming/ai-sdk-parts.ts +8 -30
- package/src/streaming/delta-tracker.ts +42 -0
- package/src/streaming/openai-sse.ts +8 -33
- package/src/utils/binary.ts +17 -3
package/src/plugin.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { isAbsolute, join, relative, resolve } from "path";
|
|
|
8
8
|
import { ToolMapper, type ToolUpdate } from "./acp/tools.js";
|
|
9
9
|
import { startCursorOAuth } from "./auth";
|
|
10
10
|
import { LineBuffer } from "./streaming/line-buffer.js";
|
|
11
|
+
import { MixedDeltaTracker } from "./streaming/delta-tracker.js";
|
|
11
12
|
import { StreamToSseConverter, formatSseDone } from "./streaming/openai-sse.js";
|
|
12
13
|
import { parseStreamJsonLine } from "./streaming/parser.js";
|
|
13
14
|
import { extractText, extractThinking, isAssistantText, isResult, isThinking } from "./streaming/types.js";
|
|
@@ -57,7 +58,7 @@ import {
|
|
|
57
58
|
parseToolLoopMaxRepeat,
|
|
58
59
|
type ToolLoopGuard,
|
|
59
60
|
} from "./provider/tool-loop-guard.js";
|
|
60
|
-
import { resolveCursorAgentBinary } from "./utils/binary.js";
|
|
61
|
+
import { formatShellCommandForPlatform, resolveCursorAgentBinary } from "./utils/binary.js";
|
|
61
62
|
|
|
62
63
|
const log = createLogger("plugin");
|
|
63
64
|
|
|
@@ -444,6 +445,7 @@ export function extractCompletionFromStream(output: string): {
|
|
|
444
445
|
let usage: OpenAiUsage | undefined;
|
|
445
446
|
let sawAssistantPartials = false;
|
|
446
447
|
let sawThinkingPartials = false;
|
|
448
|
+
const tracker = new MixedDeltaTracker();
|
|
447
449
|
|
|
448
450
|
for (const line of lines) {
|
|
449
451
|
const event = parseStreamJsonLine(line);
|
|
@@ -457,8 +459,8 @@ export function extractCompletionFromStream(output: string): {
|
|
|
457
459
|
|
|
458
460
|
const isPartial = typeof (event as any).timestamp_ms === "number";
|
|
459
461
|
if (isPartial) {
|
|
460
|
-
assistantText += text;
|
|
461
462
|
sawAssistantPartials = true;
|
|
463
|
+
assistantText += tracker.nextText(text);
|
|
462
464
|
} else if (!sawAssistantPartials) {
|
|
463
465
|
assistantText = text;
|
|
464
466
|
}
|
|
@@ -469,8 +471,8 @@ export function extractCompletionFromStream(output: string): {
|
|
|
469
471
|
if (thinking) {
|
|
470
472
|
const isPartial = typeof (event as any).timestamp_ms === "number";
|
|
471
473
|
if (isPartial) {
|
|
472
|
-
reasoningText += thinking;
|
|
473
474
|
sawThinkingPartials = true;
|
|
475
|
+
reasoningText += tracker.nextThinking(thinking);
|
|
474
476
|
} else if (!sawThinkingPartials) {
|
|
475
477
|
reasoningText = thinking;
|
|
476
478
|
}
|
|
@@ -1238,7 +1240,7 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
1238
1240
|
cmd.push("--force");
|
|
1239
1241
|
}
|
|
1240
1242
|
|
|
1241
|
-
const child = spawn(cmd[0], cmd.slice(1), {
|
|
1243
|
+
const child = spawn(formatShellCommandForPlatform(cmd[0]), cmd.slice(1), {
|
|
1242
1244
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1243
1245
|
shell: process.platform === "win32",
|
|
1244
1246
|
});
|
|
@@ -1415,19 +1417,17 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
1415
1417
|
}
|
|
1416
1418
|
};
|
|
1417
1419
|
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1420
|
+
const chunkQueue: Buffer[] = [];
|
|
1421
|
+
let draining = false;
|
|
1422
|
+
let childClosed = false;
|
|
1423
|
+
let childCloseHandled = false;
|
|
1424
|
+
let childExitCode: number | null = null;
|
|
1425
|
+
|
|
1426
|
+
const processLines = async (lines: string[]) => {
|
|
1427
|
+
for (const line of lines) {
|
|
1428
|
+
if (streamTerminated || res.writableEnded) break;
|
|
1427
1429
|
const event = parseStreamJsonLine(line);
|
|
1428
|
-
if (!event)
|
|
1429
|
-
continue;
|
|
1430
|
-
}
|
|
1430
|
+
if (!event) continue;
|
|
1431
1431
|
|
|
1432
1432
|
if (isResult(event)) {
|
|
1433
1433
|
usage = extractOpenAiUsageFromResult(event) ?? usage;
|
|
@@ -1469,156 +1469,104 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
|
|
|
1469
1469
|
if (!result.terminate.silent) {
|
|
1470
1470
|
emitTerminalAssistantErrorAndTerminate(result.terminate.message);
|
|
1471
1471
|
} else {
|
|
1472
|
-
// Silent termination: just end the stream without an error message
|
|
1473
1472
|
streamTerminated = true;
|
|
1474
1473
|
try { child.kill(); } catch { /* ignore */ }
|
|
1475
1474
|
}
|
|
1476
1475
|
break;
|
|
1477
1476
|
}
|
|
1478
|
-
if (result.intercepted)
|
|
1479
|
-
|
|
1480
|
-
}
|
|
1481
|
-
if (result.skipConverter) {
|
|
1482
|
-
continue;
|
|
1483
|
-
}
|
|
1477
|
+
if (result.intercepted) break;
|
|
1478
|
+
if (result.skipConverter) continue;
|
|
1484
1479
|
}
|
|
1485
1480
|
|
|
1486
|
-
if (streamTerminated || res.writableEnded)
|
|
1487
|
-
break;
|
|
1488
|
-
}
|
|
1481
|
+
if (streamTerminated || res.writableEnded) break;
|
|
1489
1482
|
for (const sse of converter.handleEvent(event)) {
|
|
1490
1483
|
res.write(sse);
|
|
1491
1484
|
}
|
|
1492
1485
|
}
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
child.on("close", async (code) => {
|
|
1496
|
-
if (streamTerminated || res.writableEnded) {
|
|
1497
|
-
return;
|
|
1498
|
-
}
|
|
1499
|
-
for (const line of lineBuffer.flush()) {
|
|
1500
|
-
if (streamTerminated || res.writableEnded) {
|
|
1501
|
-
break;
|
|
1502
|
-
}
|
|
1503
|
-
const event = parseStreamJsonLine(line);
|
|
1504
|
-
if (!event) {
|
|
1505
|
-
continue;
|
|
1506
|
-
}
|
|
1486
|
+
};
|
|
1507
1487
|
|
|
1508
|
-
|
|
1509
|
-
|
|
1488
|
+
const drainQueue = async () => {
|
|
1489
|
+
if (draining) return;
|
|
1490
|
+
draining = true;
|
|
1491
|
+
try {
|
|
1492
|
+
while (chunkQueue.length > 0) {
|
|
1493
|
+
if (streamTerminated || res.writableEnded) break;
|
|
1494
|
+
const chunk = chunkQueue.shift()!;
|
|
1495
|
+
if (!firstTokenReceived) { perf.mark("first-token"); firstTokenReceived = true; }
|
|
1496
|
+
await processLines(lineBuffer.push(chunk));
|
|
1510
1497
|
}
|
|
1511
1498
|
|
|
1512
|
-
if (
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
toolSessionId,
|
|
1524
|
-
shouldEmitToolUpdates: SHOULD_EMIT_TOOL_UPDATES,
|
|
1525
|
-
proxyExecuteToolCalls: PROXY_EXECUTE_TOOL_CALLS,
|
|
1526
|
-
suppressConverterToolEvents: SUPPRESS_CONVERTER_TOOL_EVENTS,
|
|
1527
|
-
toolRouter,
|
|
1528
|
-
responseMeta: { id, created, model },
|
|
1529
|
-
passThroughTracker,
|
|
1530
|
-
onToolUpdate: (update) => {
|
|
1531
|
-
res.write(formatToolUpdateEvent(update));
|
|
1532
|
-
},
|
|
1533
|
-
onToolResult: (toolResult) => {
|
|
1534
|
-
res.write(`data: ${JSON.stringify(toolResult)}\n\n`);
|
|
1535
|
-
},
|
|
1536
|
-
onInterceptedToolCall: (toolCall) => {
|
|
1537
|
-
emitToolCallAndTerminate(toolCall);
|
|
1538
|
-
},
|
|
1539
|
-
onFallbackToLegacy: (error) => {
|
|
1540
|
-
boundaryContext.activateLegacyFallback("handleToolLoopEvent.close", error);
|
|
1541
|
-
},
|
|
1499
|
+
if (childClosed && !childCloseHandled && !streamTerminated && !res.writableEnded) {
|
|
1500
|
+
childCloseHandled = true;
|
|
1501
|
+
await processLines(lineBuffer.flush());
|
|
1502
|
+
if (streamTerminated || res.writableEnded) return;
|
|
1503
|
+
|
|
1504
|
+
perf.mark("request:done");
|
|
1505
|
+
perf.summarize();
|
|
1506
|
+
const stderrText = Buffer.concat(stderrChunks).toString().trim();
|
|
1507
|
+
log.debug("cursor-agent completed (node stream)", {
|
|
1508
|
+
code: childExitCode,
|
|
1509
|
+
stderrChars: stderrText.length,
|
|
1542
1510
|
});
|
|
1543
|
-
if (
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
}
|
|
1551
|
-
|
|
1511
|
+
if (childExitCode !== 0) {
|
|
1512
|
+
const errSource =
|
|
1513
|
+
stderrText
|
|
1514
|
+
|| `cursor-agent exited with code ${String(childExitCode ?? "unknown")} and no output`;
|
|
1515
|
+
const parsed = parseAgentError(errSource);
|
|
1516
|
+
const msg = formatErrorForUser(parsed);
|
|
1517
|
+
const errChunk = createChatCompletionChunk(id, created, model, msg, true);
|
|
1518
|
+
res.write(`data: ${JSON.stringify(errChunk)}\n\n`);
|
|
1519
|
+
res.write(formatSseDone());
|
|
1520
|
+
streamTerminated = true;
|
|
1521
|
+
res.end();
|
|
1522
|
+
return;
|
|
1552
1523
|
}
|
|
1553
|
-
|
|
1554
|
-
|
|
1524
|
+
|
|
1525
|
+
const passThroughSummary = passThroughTracker.getSummary();
|
|
1526
|
+
if (passThroughSummary.hasActivity) {
|
|
1527
|
+
await toastService.showPassThroughSummary(passThroughSummary.tools);
|
|
1555
1528
|
}
|
|
1556
|
-
if (
|
|
1557
|
-
|
|
1529
|
+
if (passThroughSummary.errors.length > 0) {
|
|
1530
|
+
await toastService.showErrorSummary(passThroughSummary.errors);
|
|
1558
1531
|
}
|
|
1559
|
-
}
|
|
1560
1532
|
|
|
1561
|
-
|
|
1562
|
-
|
|
1533
|
+
const doneChunk = {
|
|
1534
|
+
id,
|
|
1535
|
+
object: "chat.completion.chunk",
|
|
1536
|
+
created,
|
|
1537
|
+
model,
|
|
1538
|
+
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
|
|
1539
|
+
};
|
|
1540
|
+
res.write(`data: ${JSON.stringify(doneChunk)}\n\n`);
|
|
1541
|
+
if (usage) {
|
|
1542
|
+
const usageChunk = createChatCompletionUsageChunk(id, created, model, usage);
|
|
1543
|
+
res.write(`data: ${JSON.stringify(usageChunk)}\n\n`);
|
|
1544
|
+
}
|
|
1545
|
+
res.write(formatSseDone());
|
|
1546
|
+
streamTerminated = true;
|
|
1547
|
+
res.end();
|
|
1563
1548
|
}
|
|
1564
|
-
|
|
1565
|
-
|
|
1549
|
+
} finally {
|
|
1550
|
+
draining = false;
|
|
1551
|
+
if (
|
|
1552
|
+
!streamTerminated
|
|
1553
|
+
&& !res.writableEnded
|
|
1554
|
+
&& (chunkQueue.length > 0 || (childClosed && !childCloseHandled))
|
|
1555
|
+
) {
|
|
1556
|
+
drainQueue();
|
|
1566
1557
|
}
|
|
1567
1558
|
}
|
|
1568
|
-
|
|
1569
|
-
return;
|
|
1570
|
-
}
|
|
1571
|
-
|
|
1572
|
-
perf.mark("request:done");
|
|
1573
|
-
perf.summarize();
|
|
1574
|
-
const stderrText = Buffer.concat(stderrChunks).toString().trim();
|
|
1575
|
-
log.debug("cursor-agent completed (node stream)", {
|
|
1576
|
-
code,
|
|
1577
|
-
stderrChars: stderrText.length,
|
|
1578
|
-
});
|
|
1579
|
-
if (code !== 0) {
|
|
1580
|
-
const errSource =
|
|
1581
|
-
stderrText
|
|
1582
|
-
|| `cursor-agent exited with code ${String(code ?? "unknown")} and no output`;
|
|
1583
|
-
const parsed = parseAgentError(errSource);
|
|
1584
|
-
const msg = formatErrorForUser(parsed);
|
|
1585
|
-
const errChunk = createChatCompletionChunk(id, created, model, msg, true);
|
|
1586
|
-
res.write(`data: ${JSON.stringify(errChunk)}\n\n`);
|
|
1587
|
-
res.write(formatSseDone());
|
|
1588
|
-
streamTerminated = true;
|
|
1589
|
-
res.end();
|
|
1590
|
-
return;
|
|
1591
|
-
}
|
|
1559
|
+
};
|
|
1592
1560
|
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
}
|
|
1598
|
-
if (passThroughSummary.errors.length > 0) {
|
|
1599
|
-
await toastService.showErrorSummary(passThroughSummary.errors);
|
|
1600
|
-
}
|
|
1561
|
+
child.stdout.on("data", (chunk) => {
|
|
1562
|
+
chunkQueue.push(Buffer.from(chunk));
|
|
1563
|
+
drainQueue();
|
|
1564
|
+
});
|
|
1601
1565
|
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
model,
|
|
1607
|
-
choices: [
|
|
1608
|
-
{
|
|
1609
|
-
index: 0,
|
|
1610
|
-
delta: {},
|
|
1611
|
-
finish_reason: "stop",
|
|
1612
|
-
},
|
|
1613
|
-
],
|
|
1614
|
-
};
|
|
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
|
-
}
|
|
1620
|
-
res.write(formatSseDone());
|
|
1621
|
-
res.end();
|
|
1566
|
+
child.on("close", (code) => {
|
|
1567
|
+
childClosed = true;
|
|
1568
|
+
childExitCode = code;
|
|
1569
|
+
drainQueue();
|
|
1622
1570
|
});
|
|
1623
1571
|
}
|
|
1624
1572
|
} catch (error) {
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
type StreamJsonEvent,
|
|
9
9
|
type StreamJsonToolCallEvent,
|
|
10
10
|
} from "./types.js";
|
|
11
|
-
import {
|
|
11
|
+
import { MixedDeltaTracker } from "./delta-tracker.js";
|
|
12
12
|
|
|
13
13
|
export type AiSdkStreamPart =
|
|
14
14
|
| {
|
|
@@ -34,44 +34,22 @@ export type AiSdkStreamPart =
|
|
|
34
34
|
};
|
|
35
35
|
|
|
36
36
|
export class StreamToAiSdkParts {
|
|
37
|
-
private readonly tracker = new DeltaTracker();
|
|
38
37
|
private readonly toolArgsById = new Map<string, string>();
|
|
39
38
|
private readonly startedToolIds = new Set<string>();
|
|
40
|
-
private
|
|
41
|
-
private sawThinkingPartials = false;
|
|
39
|
+
private readonly tracker = new MixedDeltaTracker();
|
|
42
40
|
|
|
43
41
|
handleEvent(event: StreamJsonEvent): AiSdkStreamPart[] {
|
|
44
42
|
if (isAssistantText(event)) {
|
|
45
|
-
const
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
if (text) {
|
|
49
|
-
this.sawAssistantPartials = true;
|
|
50
|
-
return [{ type: "text-delta", textDelta: text }];
|
|
51
|
-
}
|
|
52
|
-
return [];
|
|
53
|
-
}
|
|
54
|
-
if (this.sawAssistantPartials) {
|
|
55
|
-
return [];
|
|
56
|
-
}
|
|
57
|
-
const delta = this.tracker.nextText(extractText(event));
|
|
43
|
+
const text = extractText(event);
|
|
44
|
+
if (!text) return [];
|
|
45
|
+
const delta = this.tracker.nextText(text);
|
|
58
46
|
return delta ? [{ type: "text-delta", textDelta: delta }] : [];
|
|
59
47
|
}
|
|
60
48
|
|
|
61
49
|
if (isThinking(event)) {
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
if (text) {
|
|
66
|
-
this.sawThinkingPartials = true;
|
|
67
|
-
return [{ type: "text-delta", textDelta: text }];
|
|
68
|
-
}
|
|
69
|
-
return [];
|
|
70
|
-
}
|
|
71
|
-
if (this.sawThinkingPartials) {
|
|
72
|
-
return [];
|
|
73
|
-
}
|
|
74
|
-
const delta = this.tracker.nextThinking(extractThinking(event));
|
|
50
|
+
const text = extractThinking(event);
|
|
51
|
+
if (!text) return [];
|
|
52
|
+
const delta = this.tracker.nextThinking(text);
|
|
75
53
|
return delta ? [{ type: "text-delta", textDelta: delta }] : [];
|
|
76
54
|
}
|
|
77
55
|
|
|
@@ -45,3 +45,45 @@ export class DeltaTracker {
|
|
|
45
45
|
return current.slice(i);
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
+
|
|
49
|
+
export class MixedDeltaTracker {
|
|
50
|
+
private emittedText = "";
|
|
51
|
+
private emittedThinking = "";
|
|
52
|
+
|
|
53
|
+
nextText(value: string): string {
|
|
54
|
+
const delta = this.diff(this.emittedText, value);
|
|
55
|
+
if (delta) {
|
|
56
|
+
this.emittedText += delta;
|
|
57
|
+
}
|
|
58
|
+
return delta;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
nextThinking(value: string): string {
|
|
62
|
+
const delta = this.diff(this.emittedThinking, value);
|
|
63
|
+
if (delta) {
|
|
64
|
+
this.emittedThinking += delta;
|
|
65
|
+
}
|
|
66
|
+
return delta;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
reset(): void {
|
|
70
|
+
this.emittedText = "";
|
|
71
|
+
this.emittedThinking = "";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private diff(emitted: string, current: string): string {
|
|
75
|
+
if (!emitted) {
|
|
76
|
+
return current;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (current.startsWith(emitted)) {
|
|
80
|
+
return current.slice(emitted.length);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (emitted.startsWith(current)) {
|
|
84
|
+
return "";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return current;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
type StreamJsonEvent,
|
|
9
9
|
type StreamJsonToolCallEvent,
|
|
10
10
|
} from "./types.js";
|
|
11
|
-
import {
|
|
11
|
+
import { MixedDeltaTracker } from "./delta-tracker.js";
|
|
12
12
|
|
|
13
13
|
type OpenAiToolCall = {
|
|
14
14
|
index: number;
|
|
@@ -60,12 +60,7 @@ export class StreamToSseConverter {
|
|
|
60
60
|
private readonly id: string;
|
|
61
61
|
private readonly created: number;
|
|
62
62
|
private readonly model: string;
|
|
63
|
-
private readonly tracker = new
|
|
64
|
-
// Events with timestamp_ms carry delta text; events without carry accumulated text.
|
|
65
|
-
// DeltaTracker handles accumulated text only. When partials (delta) were seen,
|
|
66
|
-
// the final accumulated event must be skipped to prevent 2x duplication.
|
|
67
|
-
private sawAssistantPartials = false;
|
|
68
|
-
private sawThinkingPartials = false;
|
|
63
|
+
private readonly tracker = new MixedDeltaTracker();
|
|
69
64
|
|
|
70
65
|
constructor(model: string, options?: { id?: string; created?: number }) {
|
|
71
66
|
this.model = model;
|
|
@@ -75,36 +70,16 @@ export class StreamToSseConverter {
|
|
|
75
70
|
|
|
76
71
|
handleEvent(event: StreamJsonEvent): string[] {
|
|
77
72
|
if (isAssistantText(event)) {
|
|
78
|
-
const
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
if (text) {
|
|
82
|
-
this.sawAssistantPartials = true;
|
|
83
|
-
return [this.chunkWith({ content: text })];
|
|
84
|
-
}
|
|
85
|
-
return [];
|
|
86
|
-
}
|
|
87
|
-
if (this.sawAssistantPartials) {
|
|
88
|
-
return [];
|
|
89
|
-
}
|
|
90
|
-
const delta = this.tracker.nextText(extractText(event));
|
|
73
|
+
const text = extractText(event);
|
|
74
|
+
if (!text) return [];
|
|
75
|
+
const delta = this.tracker.nextText(text);
|
|
91
76
|
return delta ? [this.chunkWith({ content: delta })] : [];
|
|
92
77
|
}
|
|
93
78
|
|
|
94
79
|
if (isThinking(event)) {
|
|
95
|
-
const
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
if (text) {
|
|
99
|
-
this.sawThinkingPartials = true;
|
|
100
|
-
return [this.chunkWith({ reasoning_content: text })];
|
|
101
|
-
}
|
|
102
|
-
return [];
|
|
103
|
-
}
|
|
104
|
-
if (this.sawThinkingPartials) {
|
|
105
|
-
return [];
|
|
106
|
-
}
|
|
107
|
-
const delta = this.tracker.nextThinking(extractThinking(event));
|
|
80
|
+
const text = extractThinking(event);
|
|
81
|
+
if (!text) return [];
|
|
82
|
+
const delta = this.tracker.nextThinking(text);
|
|
108
83
|
return delta ? [this.chunkWith({ reasoning_content: delta })] : [];
|
|
109
84
|
}
|
|
110
85
|
|
package/src/utils/binary.ts
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
// Resolves the cursor-agent executable path. On Windows the binary is a `.cmd`
|
|
4
4
|
// shim, which Node's spawn cannot execute directly without `shell: true` —
|
|
5
5
|
// callers therefore pair this resolver with `shell: process.platform === "win32"`
|
|
6
|
-
// at every spawn site. That re-enables
|
|
7
|
-
// any user-controlled string passed as an
|
|
8
|
-
// as untrusted; never concatenate user input
|
|
6
|
+
// and `formatShellCommandForPlatform()` at every Node spawn site. That re-enables
|
|
7
|
+
// shell metacharacter interpretation, so any user-controlled string passed as an
|
|
8
|
+
// argument on Windows must be treated as untrusted; never concatenate user input
|
|
9
|
+
// into argv on win32.
|
|
9
10
|
import { existsSync as fsExistsSync } from "fs";
|
|
10
11
|
import * as pathModule from "path";
|
|
11
12
|
import { homedir as osHomedir } from "os";
|
|
@@ -55,3 +56,16 @@ export function resolveCursorAgentBinary(deps: BinaryDeps = {}): string {
|
|
|
55
56
|
log.warn("cursor-agent not found at known paths, falling back to PATH", { checkedPaths: knownPaths });
|
|
56
57
|
return "cursor-agent";
|
|
57
58
|
}
|
|
59
|
+
|
|
60
|
+
export function formatShellCommandForPlatform(
|
|
61
|
+
command: string,
|
|
62
|
+
platform: NodeJS.Platform = process.platform,
|
|
63
|
+
): string {
|
|
64
|
+
if (platform !== "win32") {
|
|
65
|
+
return command;
|
|
66
|
+
}
|
|
67
|
+
if (command.startsWith("\"") && command.endsWith("\"")) {
|
|
68
|
+
return command;
|
|
69
|
+
}
|
|
70
|
+
return `"${command}"`;
|
|
71
|
+
}
|