@rama_nigg/open-cursor 2.4.4 → 2.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/cli/discover.js +9 -0
- package/dist/cli/opencode-cursor.js +9 -0
- package/dist/index.js +335 -301
- package/dist/plugin-entry.js +318 -264
- package/package.json +1 -1
- package/src/auth.ts +2 -2
- package/src/client/simple.ts +3 -3
- package/src/mcp/tool-bridge.ts +4 -2
- package/src/plugin.ts +118 -181
- package/src/provider/runtime-interception.ts +40 -5
- package/src/provider/tool-schema-compat.ts +180 -2
- package/src/proxy/prompt-builder.ts +4 -35
- 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/tools/defaults.ts +0 -4
- package/src/utils/binary.ts +17 -3
|
@@ -49,6 +49,7 @@ export interface ToolSchemaValidationResult {
|
|
|
49
49
|
export interface ToolSchemaCompatResult {
|
|
50
50
|
toolCall: OpenAiToolCall;
|
|
51
51
|
normalizedArgs: JsonRecord;
|
|
52
|
+
originalArgs: JsonRecord;
|
|
52
53
|
originalArgKeys: string[];
|
|
53
54
|
normalizedArgKeys: string[];
|
|
54
55
|
collisionKeys: string[];
|
|
@@ -89,6 +90,10 @@ export function applyToolSchemaCompat(
|
|
|
89
90
|
sanitization.args,
|
|
90
91
|
schema,
|
|
91
92
|
sanitization.unexpected,
|
|
93
|
+
{
|
|
94
|
+
originalArgs: parsedArgs,
|
|
95
|
+
writeSchema: toolSchemaMap.get("write"),
|
|
96
|
+
},
|
|
92
97
|
);
|
|
93
98
|
|
|
94
99
|
const normalizedToolCall: OpenAiToolCall = {
|
|
@@ -102,6 +107,7 @@ export function applyToolSchemaCompat(
|
|
|
102
107
|
return {
|
|
103
108
|
toolCall: normalizedToolCall,
|
|
104
109
|
normalizedArgs: sanitization.args,
|
|
110
|
+
originalArgs: parsedArgs,
|
|
105
111
|
originalArgKeys,
|
|
106
112
|
normalizedArgKeys: Object.keys(sanitization.args),
|
|
107
113
|
collisionKeys,
|
|
@@ -109,6 +115,96 @@ export function applyToolSchemaCompat(
|
|
|
109
115
|
};
|
|
110
116
|
}
|
|
111
117
|
|
|
118
|
+
export function isFullFileShapedEditValidationFailure(
|
|
119
|
+
toolName: string,
|
|
120
|
+
args: JsonRecord,
|
|
121
|
+
validation: ToolSchemaValidationResult,
|
|
122
|
+
originalArgs: JsonRecord,
|
|
123
|
+
writeSchema?: unknown,
|
|
124
|
+
): boolean {
|
|
125
|
+
if (toolName.toLowerCase() !== "edit" || validation.ok) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
return buildEditFullFileHint(args, validation.missing, validation.typeErrors, {
|
|
129
|
+
originalArgs,
|
|
130
|
+
writeSchema,
|
|
131
|
+
}) !== null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function buildWriteArguments(
|
|
135
|
+
filePath: string,
|
|
136
|
+
content: string,
|
|
137
|
+
writeSchema: unknown,
|
|
138
|
+
): JsonRecord {
|
|
139
|
+
if (!isRecord(writeSchema)) {
|
|
140
|
+
return { path: filePath, content };
|
|
141
|
+
}
|
|
142
|
+
const required = Array.isArray(writeSchema.required)
|
|
143
|
+
? writeSchema.required.filter((value): value is string => typeof value === "string")
|
|
144
|
+
: [];
|
|
145
|
+
if (required.includes("filePath")) {
|
|
146
|
+
return { filePath, content };
|
|
147
|
+
}
|
|
148
|
+
return { path: filePath, content };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Malformed full-file edit (path + body, no old_string) → write tool call when write is available. */
|
|
152
|
+
export function tryRerouteEditToWrite(
|
|
153
|
+
toolCall: OpenAiToolCall,
|
|
154
|
+
compat: ToolSchemaCompatResult,
|
|
155
|
+
allowedToolNames: Set<string>,
|
|
156
|
+
toolSchemaMap: Map<string, unknown>,
|
|
157
|
+
): OpenAiToolCall | null {
|
|
158
|
+
if (toolCall.function.name.toLowerCase() !== "edit") {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
if (!allowedToolNames.has("write") || !toolSchemaMap.has("write")) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const writeSchema = toolSchemaMap.get("write");
|
|
166
|
+
if (
|
|
167
|
+
!isFullFileShapedEditValidationFailure(
|
|
168
|
+
toolCall.function.name,
|
|
169
|
+
compat.normalizedArgs,
|
|
170
|
+
compat.validation,
|
|
171
|
+
compat.originalArgs,
|
|
172
|
+
writeSchema,
|
|
173
|
+
)
|
|
174
|
+
) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const filePath = typeof compat.normalizedArgs.path === "string" && compat.normalizedArgs.path.length > 0
|
|
179
|
+
? compat.normalizedArgs.path
|
|
180
|
+
: typeof compat.normalizedArgs.filePath === "string" && compat.normalizedArgs.filePath.length > 0
|
|
181
|
+
? compat.normalizedArgs.filePath
|
|
182
|
+
: null;
|
|
183
|
+
if (!filePath) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const content =
|
|
188
|
+
typeof compat.normalizedArgs.new_string === "string"
|
|
189
|
+
? compat.normalizedArgs.new_string
|
|
190
|
+
: typeof compat.normalizedArgs.newString === "string"
|
|
191
|
+
? compat.normalizedArgs.newString
|
|
192
|
+
: typeof compat.normalizedArgs.content === "string"
|
|
193
|
+
? compat.normalizedArgs.content
|
|
194
|
+
: null;
|
|
195
|
+
if (content === null) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
...toolCall,
|
|
201
|
+
function: {
|
|
202
|
+
name: "write",
|
|
203
|
+
arguments: JSON.stringify(buildWriteArguments(filePath, content, writeSchema)),
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
112
208
|
function parseArguments(rawArguments: string): JsonRecord {
|
|
113
209
|
try {
|
|
114
210
|
const parsed = JSON.parse(rawArguments);
|
|
@@ -350,11 +446,17 @@ function sanitizeArgumentsForSchema(
|
|
|
350
446
|
return { args: sanitized, unexpected };
|
|
351
447
|
}
|
|
352
448
|
|
|
449
|
+
type ValidateToolArgumentsContext = {
|
|
450
|
+
originalArgs?: JsonRecord;
|
|
451
|
+
writeSchema?: unknown;
|
|
452
|
+
};
|
|
453
|
+
|
|
353
454
|
function validateToolArguments(
|
|
354
455
|
toolName: string,
|
|
355
456
|
args: JsonRecord,
|
|
356
457
|
schema: unknown,
|
|
357
458
|
unexpected: string[],
|
|
459
|
+
context: ValidateToolArgumentsContext = {},
|
|
358
460
|
): ToolSchemaValidationResult {
|
|
359
461
|
if (!isRecord(schema)) {
|
|
360
462
|
return {
|
|
@@ -399,16 +501,90 @@ function validateToolArguments(
|
|
|
399
501
|
missing,
|
|
400
502
|
unexpected,
|
|
401
503
|
typeErrors,
|
|
402
|
-
repairHint: ok
|
|
504
|
+
repairHint: ok
|
|
505
|
+
? undefined
|
|
506
|
+
: buildRepairHint(toolName, args, missing, unexpected, typeErrors, context),
|
|
403
507
|
};
|
|
404
508
|
}
|
|
405
509
|
|
|
510
|
+
function hadOldStringPropertyInPayload(args: JsonRecord): boolean {
|
|
511
|
+
for (const key of Object.keys(args)) {
|
|
512
|
+
const token = key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
513
|
+
if (token === "oldstring") {
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function hasEditFilePath(args: JsonRecord): boolean {
|
|
521
|
+
const pathValue = args.path ?? args.filePath;
|
|
522
|
+
return typeof pathValue === "string" && pathValue.trim().length > 0;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function hasEditBody(args: JsonRecord): boolean {
|
|
526
|
+
const body = args.new_string ?? args.newString ?? args.content;
|
|
527
|
+
return typeof body === "string" && body.length > 0;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function writeToolExample(writeSchema: unknown): string {
|
|
531
|
+
if (!isRecord(writeSchema)) {
|
|
532
|
+
return "write with path and content";
|
|
533
|
+
}
|
|
534
|
+
const required = Array.isArray(writeSchema.required)
|
|
535
|
+
? writeSchema.required.filter((value): value is string => typeof value === "string")
|
|
536
|
+
: [];
|
|
537
|
+
if (required.includes("filePath")) {
|
|
538
|
+
return "write with filePath and content";
|
|
539
|
+
}
|
|
540
|
+
return "write with path and content";
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function buildEditFullFileHint(
|
|
544
|
+
args: JsonRecord,
|
|
545
|
+
missing: string[],
|
|
546
|
+
typeErrors: string[],
|
|
547
|
+
context: ValidateToolArgumentsContext,
|
|
548
|
+
): string | null {
|
|
549
|
+
if (typeErrors.length > 0) {
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const missingOldStringOnly =
|
|
554
|
+
(missing.includes("old_string") || missing.includes("oldString"))
|
|
555
|
+
&& missing.every((key) => key === "old_string" || key === "oldString");
|
|
556
|
+
if (!missingOldStringOnly) {
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const originalArgs = context.originalArgs ?? {};
|
|
561
|
+
if (hadOldStringPropertyInPayload(originalArgs)) {
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (!hasEditFilePath(args) || !hasEditBody(args)) {
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const example = writeToolExample(context.writeSchema);
|
|
570
|
+
return `For a full file body, use ${example} instead of edit without old_string`;
|
|
571
|
+
}
|
|
572
|
+
|
|
406
573
|
function buildRepairHint(
|
|
407
574
|
toolName: string,
|
|
575
|
+
args: JsonRecord,
|
|
408
576
|
missing: string[],
|
|
409
577
|
unexpected: string[],
|
|
410
578
|
typeErrors: string[],
|
|
579
|
+
context: ValidateToolArgumentsContext = {},
|
|
411
580
|
): string {
|
|
581
|
+
const fullFileHint = toolName.toLowerCase() === "edit"
|
|
582
|
+
? buildEditFullFileHint(args, missing, typeErrors, context)
|
|
583
|
+
: null;
|
|
584
|
+
if (fullFileHint) {
|
|
585
|
+
return fullFileHint;
|
|
586
|
+
}
|
|
587
|
+
|
|
412
588
|
const hints: string[] = [];
|
|
413
589
|
if (missing.length > 0) {
|
|
414
590
|
hints.push(`missing required: ${missing.join(", ")}`);
|
|
@@ -419,12 +595,14 @@ function buildRepairHint(
|
|
|
419
595
|
if (typeErrors.length > 0) {
|
|
420
596
|
hints.push(`fix type errors: ${typeErrors.join("; ")}`);
|
|
421
597
|
}
|
|
598
|
+
|
|
422
599
|
if (
|
|
423
600
|
toolName.toLowerCase() === "edit"
|
|
424
|
-
&& (missing.includes("old_string") || missing.includes("new_string"))
|
|
601
|
+
&& (missing.includes("old_string") || missing.includes("oldString") || missing.includes("new_string") || missing.includes("newString"))
|
|
425
602
|
) {
|
|
426
603
|
hints.push("edit requires path, old_string, and new_string");
|
|
427
604
|
}
|
|
605
|
+
|
|
428
606
|
return hints.join(" | ");
|
|
429
607
|
}
|
|
430
608
|
|
|
@@ -1,43 +1,13 @@
|
|
|
1
|
-
import { appendFileSync, existsSync, mkdirSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
1
|
import { createLogger } from "../utils/logger.js";
|
|
5
2
|
|
|
6
3
|
const log = createLogger("proxy:prompt-builder");
|
|
7
4
|
|
|
8
|
-
// Debug log file for tool-loop investigation
|
|
9
|
-
const DEBUG_LOG_DIR = join(homedir(), ".config", "opencode", "logs");
|
|
10
|
-
const DEBUG_LOG_FILE = join(DEBUG_LOG_DIR, "tool-loop-debug.log");
|
|
11
|
-
|
|
12
|
-
function ensureLogDir(): void {
|
|
13
|
-
try {
|
|
14
|
-
if (!existsSync(DEBUG_LOG_DIR)) {
|
|
15
|
-
mkdirSync(DEBUG_LOG_DIR, { recursive: true });
|
|
16
|
-
}
|
|
17
|
-
} catch {
|
|
18
|
-
// Ignore errors creating log directory
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function debugLogToFile(message: string, data: any): void {
|
|
23
|
-
try {
|
|
24
|
-
ensureLogDir();
|
|
25
|
-
const timestamp = new Date().toISOString();
|
|
26
|
-
const logLine = `[${timestamp}] ${message}: ${JSON.stringify(data, null, 2)}\n`;
|
|
27
|
-
appendFileSync(DEBUG_LOG_FILE, logLine);
|
|
28
|
-
} catch (err) {
|
|
29
|
-
// Fall back to regular debug log if file writing fails
|
|
30
|
-
log.debug(message, data);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
5
|
/**
|
|
35
6
|
* Build a text prompt from OpenAI chat messages + tool definitions.
|
|
36
7
|
* Handles role:"tool" result messages and assistant tool_calls that
|
|
37
8
|
* plain text flattening would silently drop.
|
|
38
9
|
*/
|
|
39
10
|
export function buildPromptFromMessages(messages: Array<any>, tools: Array<any>, subagentNames: string[] = []): string {
|
|
40
|
-
// DEBUG: Log incoming message structure to file for root cause analysis
|
|
41
11
|
const messageSummary = messages.map((m: any, i: number) => {
|
|
42
12
|
const role = m?.role ?? "?";
|
|
43
13
|
const hasToolCalls = Array.isArray(m?.tool_calls) ? m.tool_calls.length : 0;
|
|
@@ -52,7 +22,7 @@ export function buildPromptFromMessages(messages: Array<any>, tools: Array<any>,
|
|
|
52
22
|
const assistantEmpty = messages.filter((m: any) => m?.role === "assistant" && (!m?.tool_calls || m.tool_calls.length === 0) && (!m?.content || m.content === "" || m.content === null));
|
|
53
23
|
const toolResults = messages.filter((m: any) => m?.role === "tool");
|
|
54
24
|
|
|
55
|
-
|
|
25
|
+
log.debug("buildPromptFromMessages", {
|
|
56
26
|
totalMessages: messages.length,
|
|
57
27
|
totalTools: tools.length,
|
|
58
28
|
messageSummary,
|
|
@@ -165,16 +135,15 @@ export function buildPromptFromMessages(messages: Array<any>, tools: Array<any>,
|
|
|
165
135
|
);
|
|
166
136
|
}
|
|
167
137
|
|
|
168
|
-
// DEBUG: Log the final prompt structure
|
|
169
138
|
const finalPrompt = lines.join("\n\n");
|
|
170
|
-
|
|
139
|
+
log.debug("buildPromptFromMessages: final prompt", {
|
|
171
140
|
lineCount: lines.length,
|
|
172
141
|
promptLength: finalPrompt.length,
|
|
173
142
|
promptPreview: finalPrompt.slice(0, 500),
|
|
174
143
|
hasToolResultFormat: finalPrompt.includes("TOOL_RESULT"),
|
|
175
144
|
hasAssistantToolCallFormat: finalPrompt.includes("tool_call(id:"),
|
|
176
|
-
hasCompletionSignal: finalPrompt.includes("
|
|
145
|
+
hasCompletionSignal: finalPrompt.includes("The above tool calls have been executed"),
|
|
177
146
|
});
|
|
178
147
|
|
|
179
148
|
return finalPrompt;
|
|
180
|
-
}
|
|
149
|
+
}
|
|
@@ -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/tools/defaults.ts
CHANGED
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
|
+
}
|