@rama_nigg/open-cursor 2.4.5 → 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/index.js +164 -83
- package/dist/plugin-entry.js +156 -75
- package/package.json +1 -1
- package/src/mcp/tool-bridge.ts +4 -2
- package/src/plugin.ts +28 -39
- 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/tools/defaults.ts +0 -4
|
@@ -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
|
+
}
|
package/src/tools/defaults.ts
CHANGED