@oh-my-pi/pi-coding-agent 4.2.3 → 4.3.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/CHANGELOG.md +12 -0
- package/package.json +5 -5
- package/src/core/auth-storage.ts +7 -0
- package/src/core/cursor/exec-bridge.ts +234 -0
- package/src/core/model-resolver.ts +1 -0
- package/src/core/sdk.ts +12 -1
- package/src/core/tools/edit-diff.ts +44 -21
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [4.3.0] - 2026-01-11
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added Cursor provider support with browser-based OAuth authentication
|
|
10
|
+
- Added default model configuration for Cursor provider (claude-sonnet-4-5)
|
|
11
|
+
- Added execution bridge for Cursor tool calls including read, ls, grep, write, delete, shell, diagnostics, and MCP operations
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Improved fuzzy matching accuracy for edit operations when file and target have inconsistent indentation patterns
|
|
16
|
+
|
|
5
17
|
## [4.2.3] - 2026-01-11
|
|
6
18
|
|
|
7
19
|
### Changed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"prepublishOnly": "bun run generate-template && bun run clean && bun run build"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@oh-my-pi/pi-ai": "4.
|
|
43
|
-
"@oh-my-pi/pi-agent-core": "4.
|
|
44
|
-
"@oh-my-pi/pi-git-tool": "4.
|
|
45
|
-
"@oh-my-pi/pi-tui": "4.
|
|
42
|
+
"@oh-my-pi/pi-ai": "4.3.0",
|
|
43
|
+
"@oh-my-pi/pi-agent-core": "4.3.0",
|
|
44
|
+
"@oh-my-pi/pi-git-tool": "4.3.0",
|
|
45
|
+
"@oh-my-pi/pi-tui": "4.3.0",
|
|
46
46
|
"@openai/agents": "^0.3.7",
|
|
47
47
|
"@sinclair/typebox": "^0.34.46",
|
|
48
48
|
"ajv": "^8.17.1",
|
package/src/core/auth-storage.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
getOAuthApiKey,
|
|
10
10
|
loginAnthropic,
|
|
11
11
|
loginAntigravity,
|
|
12
|
+
loginCursor,
|
|
12
13
|
loginGeminiCli,
|
|
13
14
|
loginGitHubCopilot,
|
|
14
15
|
loginOpenAICodex,
|
|
@@ -585,6 +586,12 @@ export class AuthStorage {
|
|
|
585
586
|
onManualCodeInput: callbacks.onManualCodeInput,
|
|
586
587
|
});
|
|
587
588
|
break;
|
|
589
|
+
case "cursor":
|
|
590
|
+
credentials = await loginCursor(
|
|
591
|
+
(url) => callbacks.onAuth({ url }),
|
|
592
|
+
callbacks.onProgress ? () => callbacks.onProgress?.("Waiting for browser authentication...") : undefined,
|
|
593
|
+
);
|
|
594
|
+
break;
|
|
588
595
|
default:
|
|
589
596
|
throw new Error(`Unknown OAuth provider: ${provider}`);
|
|
590
597
|
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { rmSync, statSync } from "node:fs";
|
|
3
|
+
import type {
|
|
4
|
+
AgentEvent,
|
|
5
|
+
AgentTool,
|
|
6
|
+
AgentToolContext,
|
|
7
|
+
AgentToolResult,
|
|
8
|
+
AgentToolUpdateCallback,
|
|
9
|
+
} from "@oh-my-pi/pi-agent-core";
|
|
10
|
+
import type { CursorExecHandlers, CursorMcpCall, ToolResultMessage } from "@oh-my-pi/pi-ai";
|
|
11
|
+
import { resolveToCwd } from "../tools/path-utils";
|
|
12
|
+
|
|
13
|
+
interface CursorExecBridgeOptions {
|
|
14
|
+
cwd: string;
|
|
15
|
+
tools: Map<string, AgentTool>;
|
|
16
|
+
getToolContext?: () => AgentToolContext | undefined;
|
|
17
|
+
emitEvent?: (event: AgentEvent) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function createToolResultMessage(
|
|
21
|
+
toolCallId: string,
|
|
22
|
+
toolName: string,
|
|
23
|
+
result: AgentToolResult<unknown>,
|
|
24
|
+
isError: boolean,
|
|
25
|
+
): ToolResultMessage {
|
|
26
|
+
return {
|
|
27
|
+
role: "toolResult",
|
|
28
|
+
toolCallId,
|
|
29
|
+
toolName,
|
|
30
|
+
content: result.content,
|
|
31
|
+
details: result.details,
|
|
32
|
+
isError,
|
|
33
|
+
timestamp: Date.now(),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function buildToolErrorResult(message: string): AgentToolResult<unknown> {
|
|
38
|
+
return {
|
|
39
|
+
content: [{ type: "text", text: message }],
|
|
40
|
+
details: {},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function executeTool(
|
|
45
|
+
options: CursorExecBridgeOptions,
|
|
46
|
+
toolName: string,
|
|
47
|
+
toolCallId: string,
|
|
48
|
+
args: Record<string, unknown>,
|
|
49
|
+
): Promise<ToolResultMessage> {
|
|
50
|
+
const tool = options.tools.get(toolName);
|
|
51
|
+
if (!tool) {
|
|
52
|
+
const result = buildToolErrorResult(`Tool "${toolName}" not available`);
|
|
53
|
+
return createToolResultMessage(toolCallId, toolName, result, true);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
options.emitEvent?.({ type: "tool_execution_start", toolCallId, toolName, args });
|
|
57
|
+
|
|
58
|
+
let result: AgentToolResult<unknown>;
|
|
59
|
+
let isError = false;
|
|
60
|
+
|
|
61
|
+
const onUpdate: AgentToolUpdateCallback<unknown> | undefined = options.emitEvent
|
|
62
|
+
? (partialResult) => {
|
|
63
|
+
options.emitEvent?.({
|
|
64
|
+
type: "tool_execution_update",
|
|
65
|
+
toolCallId,
|
|
66
|
+
toolName,
|
|
67
|
+
args,
|
|
68
|
+
partialResult,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
: undefined;
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
result = await tool.execute(
|
|
75
|
+
toolCallId,
|
|
76
|
+
args as Record<string, unknown>,
|
|
77
|
+
undefined,
|
|
78
|
+
onUpdate,
|
|
79
|
+
options.getToolContext?.(),
|
|
80
|
+
);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
83
|
+
result = buildToolErrorResult(message);
|
|
84
|
+
isError = true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
options.emitEvent?.({ type: "tool_execution_end", toolCallId, toolName, result, isError });
|
|
88
|
+
|
|
89
|
+
return createToolResultMessage(toolCallId, toolName, result, isError);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function executeDelete(options: CursorExecBridgeOptions, pathArg: string, toolCallId: string) {
|
|
93
|
+
const toolName = "delete";
|
|
94
|
+
options.emitEvent?.({ type: "tool_execution_start", toolCallId, toolName, args: { path: pathArg } });
|
|
95
|
+
|
|
96
|
+
const absolutePath = resolveToCwd(pathArg, options.cwd);
|
|
97
|
+
let isError = false;
|
|
98
|
+
let result: AgentToolResult<unknown>;
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const stat = statSync(absolutePath, { throwIfNoEntry: false });
|
|
102
|
+
if (!stat) {
|
|
103
|
+
throw new Error(`File not found: ${pathArg}`);
|
|
104
|
+
}
|
|
105
|
+
if (!stat.isFile()) {
|
|
106
|
+
throw new Error(`Path is not a file: ${pathArg}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
rmSync(absolutePath);
|
|
110
|
+
|
|
111
|
+
const sizeText = stat.size ? ` (${stat.size} bytes)` : "";
|
|
112
|
+
const message = `Deleted ${pathArg}${sizeText}`;
|
|
113
|
+
result = { content: [{ type: "text", text: message }], details: {} };
|
|
114
|
+
} catch (error) {
|
|
115
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
116
|
+
result = buildToolErrorResult(message);
|
|
117
|
+
isError = true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
options.emitEvent?.({ type: "tool_execution_end", toolCallId, toolName, result, isError });
|
|
121
|
+
return createToolResultMessage(toolCallId, toolName, result, isError);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function decodeToolCallId(toolCallId?: string): string {
|
|
125
|
+
return toolCallId && toolCallId.length > 0 ? toolCallId : randomUUID();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function decodeMcpArgs(rawArgs: Record<string, Uint8Array>): Record<string, unknown> {
|
|
129
|
+
const decoded: Record<string, unknown> = {};
|
|
130
|
+
for (const [key, value] of Object.entries(rawArgs)) {
|
|
131
|
+
const text = new TextDecoder().decode(value);
|
|
132
|
+
try {
|
|
133
|
+
decoded[key] = JSON.parse(text);
|
|
134
|
+
} catch {
|
|
135
|
+
decoded[key] = text;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return decoded;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function formatMcpToolErrorMessage(toolName: string, availableTools: string[]): string {
|
|
142
|
+
const list = availableTools.length > 0 ? availableTools.join(", ") : "none";
|
|
143
|
+
return `MCP tool "${toolName}" not found. Available tools: ${list}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function createCursorExecHandlers(options: CursorExecBridgeOptions): CursorExecHandlers {
|
|
147
|
+
return {
|
|
148
|
+
read: async (args) => {
|
|
149
|
+
const toolCallId = decodeToolCallId(args.toolCallId);
|
|
150
|
+
const toolResultMessage = await executeTool(options, "read", toolCallId, { path: args.path });
|
|
151
|
+
return toolResultMessage;
|
|
152
|
+
},
|
|
153
|
+
ls: async (args) => {
|
|
154
|
+
const toolCallId = decodeToolCallId(args.toolCallId);
|
|
155
|
+
const toolResultMessage = await executeTool(options, "ls", toolCallId, { path: args.path });
|
|
156
|
+
return toolResultMessage;
|
|
157
|
+
},
|
|
158
|
+
grep: async (args) => {
|
|
159
|
+
const toolCallId = decodeToolCallId(args.toolCallId);
|
|
160
|
+
const toolResultMessage = await executeTool(options, "grep", toolCallId, {
|
|
161
|
+
pattern: args.pattern,
|
|
162
|
+
path: args.path || undefined,
|
|
163
|
+
glob: args.glob || undefined,
|
|
164
|
+
outputMode: args.outputMode || undefined,
|
|
165
|
+
context: args.context ?? args.contextBefore ?? args.contextAfter ?? undefined,
|
|
166
|
+
ignoreCase: args.caseInsensitive || undefined,
|
|
167
|
+
type: args.type || undefined,
|
|
168
|
+
headLimit: args.headLimit ?? undefined,
|
|
169
|
+
multiline: args.multiline || undefined,
|
|
170
|
+
});
|
|
171
|
+
return toolResultMessage;
|
|
172
|
+
},
|
|
173
|
+
write: async (args) => {
|
|
174
|
+
const toolCallId = decodeToolCallId(args.toolCallId);
|
|
175
|
+
const content = args.fileText ?? new TextDecoder().decode(args.fileBytes ?? new Uint8Array());
|
|
176
|
+
const toolResultMessage = await executeTool(options, "write", toolCallId, {
|
|
177
|
+
path: args.path,
|
|
178
|
+
content,
|
|
179
|
+
});
|
|
180
|
+
return toolResultMessage;
|
|
181
|
+
},
|
|
182
|
+
delete: async (args) => {
|
|
183
|
+
const toolCallId = decodeToolCallId(args.toolCallId);
|
|
184
|
+
const toolResultMessage = await executeDelete(options, args.path, toolCallId);
|
|
185
|
+
return toolResultMessage;
|
|
186
|
+
},
|
|
187
|
+
shell: async (args) => {
|
|
188
|
+
const toolCallId = decodeToolCallId(args.toolCallId);
|
|
189
|
+
const timeoutSeconds =
|
|
190
|
+
args.timeout && args.timeout > 0
|
|
191
|
+
? args.timeout > 1000
|
|
192
|
+
? Math.ceil(args.timeout / 1000)
|
|
193
|
+
: args.timeout
|
|
194
|
+
: undefined;
|
|
195
|
+
const toolResultMessage = await executeTool(options, "bash", toolCallId, {
|
|
196
|
+
command: args.command,
|
|
197
|
+
workdir: args.workingDirectory || undefined,
|
|
198
|
+
timeout: timeoutSeconds,
|
|
199
|
+
});
|
|
200
|
+
return toolResultMessage;
|
|
201
|
+
},
|
|
202
|
+
diagnostics: async (args) => {
|
|
203
|
+
const toolCallId = decodeToolCallId(args.toolCallId);
|
|
204
|
+
const toolResultMessage = await executeTool(options, "lsp", toolCallId, {
|
|
205
|
+
action: "diagnostics",
|
|
206
|
+
file: args.path,
|
|
207
|
+
});
|
|
208
|
+
return toolResultMessage;
|
|
209
|
+
},
|
|
210
|
+
mcp: async (call: CursorMcpCall) => {
|
|
211
|
+
const toolName = call.toolName || call.name;
|
|
212
|
+
const toolCallId = decodeToolCallId(call.toolCallId);
|
|
213
|
+
const tool = options.tools.get(toolName);
|
|
214
|
+
if (!tool) {
|
|
215
|
+
const availableTools = Array.from(options.tools.keys()).filter((name) => name.startsWith("mcp_"));
|
|
216
|
+
const message = formatMcpToolErrorMessage(toolName, availableTools);
|
|
217
|
+
const toolResult: ToolResultMessage = {
|
|
218
|
+
role: "toolResult",
|
|
219
|
+
toolCallId,
|
|
220
|
+
toolName,
|
|
221
|
+
content: [{ type: "text", text: message }],
|
|
222
|
+
details: {},
|
|
223
|
+
isError: true,
|
|
224
|
+
timestamp: Date.now(),
|
|
225
|
+
};
|
|
226
|
+
return toolResult;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const args = Object.keys(call.args ?? {}).length > 0 ? call.args : decodeMcpArgs(call.rawArgs ?? {});
|
|
230
|
+
const toolResultMessage = await executeTool(options, toolName, toolCallId, args);
|
|
231
|
+
return toolResultMessage;
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
}
|
|
@@ -19,6 +19,7 @@ export const defaultModelPerProvider: Record<KnownProvider, string> = {
|
|
|
19
19
|
"google-antigravity": "gemini-3-pro-high",
|
|
20
20
|
"google-vertex": "gemini-2.5-pro",
|
|
21
21
|
"github-copilot": "gpt-4o",
|
|
22
|
+
cursor: "claude-sonnet-4-5",
|
|
22
23
|
openrouter: "openai/gpt-5.1-codex",
|
|
23
24
|
xai: "grok-4-fast-non-reasoning",
|
|
24
25
|
groq: "openai/gpt-oss-120b",
|
package/src/core/sdk.ts
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
29
|
import { join } from "node:path";
|
|
30
|
-
import { Agent, type AgentMessage, type AgentTool, type ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
30
|
+
import { Agent, type AgentEvent, type AgentMessage, type AgentTool, type ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
31
31
|
import type { Message, Model } from "@oh-my-pi/pi-ai";
|
|
32
32
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
33
33
|
import chalk from "chalk";
|
|
@@ -40,6 +40,7 @@ import { initializeWithSettings } from "../discovery";
|
|
|
40
40
|
import { registerAsyncCleanup } from "../modes/cleanup";
|
|
41
41
|
import { AgentSession } from "./agent-session";
|
|
42
42
|
import { AuthStorage } from "./auth-storage";
|
|
43
|
+
import { createCursorExecHandlers } from "./cursor/exec-bridge";
|
|
43
44
|
import {
|
|
44
45
|
type CustomCommandsLoadResult,
|
|
45
46
|
loadCustomCommands as loadCustomCommandsInternal,
|
|
@@ -854,6 +855,14 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
854
855
|
}
|
|
855
856
|
time("combineTools");
|
|
856
857
|
|
|
858
|
+
let cursorEventEmitter: ((event: AgentEvent) => void) | undefined;
|
|
859
|
+
const cursorExecHandlers = createCursorExecHandlers({
|
|
860
|
+
cwd,
|
|
861
|
+
tools: toolRegistry,
|
|
862
|
+
getToolContext: toolContextStore.getContext,
|
|
863
|
+
emitEvent: (event) => cursorEventEmitter?.(event),
|
|
864
|
+
});
|
|
865
|
+
|
|
857
866
|
const rebuildSystemPrompt = async (toolNames: string[], tools: Map<string, AgentTool>): Promise<string> => {
|
|
858
867
|
toolContextStore.setToolNames(toolNames);
|
|
859
868
|
const defaultPrompt = await buildSystemPromptInternal({
|
|
@@ -964,7 +973,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
964
973
|
}
|
|
965
974
|
return key;
|
|
966
975
|
},
|
|
976
|
+
cursorExecHandlers,
|
|
967
977
|
});
|
|
978
|
+
cursorEventEmitter = (event) => agent.emitExternalEvent(event);
|
|
968
979
|
time("createAgent");
|
|
969
980
|
|
|
970
981
|
// Restore messages if session has existing data
|
|
@@ -49,9 +49,9 @@ function countLeadingWhitespace(line: string): number {
|
|
|
49
49
|
const char = line[i];
|
|
50
50
|
if (char === " " || char === "\t") {
|
|
51
51
|
count++;
|
|
52
|
-
|
|
52
|
+
} else {
|
|
53
|
+
break;
|
|
53
54
|
}
|
|
54
|
-
break;
|
|
55
55
|
}
|
|
56
56
|
return count;
|
|
57
57
|
}
|
|
@@ -80,15 +80,16 @@ function computeRelativeIndentDepths(lines: string[]): number[] {
|
|
|
80
80
|
});
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
function normalizeLinesForMatch(lines: string[]): string[] {
|
|
84
|
-
const indentDepths = computeRelativeIndentDepths(lines);
|
|
83
|
+
function normalizeLinesForMatch(lines: string[], includeDepth = true): string[] {
|
|
84
|
+
const indentDepths = includeDepth ? computeRelativeIndentDepths(lines) : null;
|
|
85
85
|
return lines.map((line, index) => {
|
|
86
86
|
const trimmed = line.trim();
|
|
87
|
+
const prefix = indentDepths ? `${indentDepths[index]}|` : "|";
|
|
87
88
|
if (trimmed.length === 0) {
|
|
88
|
-
return
|
|
89
|
+
return prefix;
|
|
89
90
|
}
|
|
90
91
|
const collapsed = trimmed.replace(/[ \t]+/g, " ");
|
|
91
|
-
return `${
|
|
92
|
+
return `${prefix}${collapsed}`;
|
|
92
93
|
});
|
|
93
94
|
}
|
|
94
95
|
|
|
@@ -148,22 +149,14 @@ function computeLineOffsets(lines: string[]): number[] {
|
|
|
148
149
|
return offsets;
|
|
149
150
|
}
|
|
150
151
|
|
|
151
|
-
function
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
function findBestFuzzyMatchCore(
|
|
153
|
+
contentLines: string[],
|
|
154
|
+
targetLines: string[],
|
|
155
|
+
offsets: number[],
|
|
154
156
|
threshold: number,
|
|
157
|
+
includeDepth: boolean,
|
|
155
158
|
): { best?: EditMatch; aboveThresholdCount: number } {
|
|
156
|
-
const
|
|
157
|
-
const targetLines = target.split("\n");
|
|
158
|
-
if (targetLines.length === 0 || target.length === 0) {
|
|
159
|
-
return { aboveThresholdCount: 0 };
|
|
160
|
-
}
|
|
161
|
-
if (targetLines.length > contentLines.length) {
|
|
162
|
-
return { aboveThresholdCount: 0 };
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const targetNormalized = normalizeLinesForMatch(targetLines);
|
|
166
|
-
const offsets = computeLineOffsets(contentLines);
|
|
159
|
+
const targetNormalized = normalizeLinesForMatch(targetLines, includeDepth);
|
|
167
160
|
|
|
168
161
|
let best: EditMatch | undefined;
|
|
169
162
|
let bestScore = -1;
|
|
@@ -171,7 +164,7 @@ function findBestFuzzyMatch(
|
|
|
171
164
|
|
|
172
165
|
for (let start = 0; start <= contentLines.length - targetLines.length; start++) {
|
|
173
166
|
const windowLines = contentLines.slice(start, start + targetLines.length);
|
|
174
|
-
const windowNormalized = normalizeLinesForMatch(windowLines);
|
|
167
|
+
const windowNormalized = normalizeLinesForMatch(windowLines, includeDepth);
|
|
175
168
|
let score = 0;
|
|
176
169
|
for (let i = 0; i < targetLines.length; i++) {
|
|
177
170
|
score += similarityScore(targetNormalized[i], windowNormalized[i]);
|
|
@@ -196,6 +189,36 @@ function findBestFuzzyMatch(
|
|
|
196
189
|
return { best, aboveThresholdCount };
|
|
197
190
|
}
|
|
198
191
|
|
|
192
|
+
const FALLBACK_THRESHOLD = 0.8;
|
|
193
|
+
|
|
194
|
+
function findBestFuzzyMatch(
|
|
195
|
+
content: string,
|
|
196
|
+
target: string,
|
|
197
|
+
threshold: number,
|
|
198
|
+
): { best?: EditMatch; aboveThresholdCount: number } {
|
|
199
|
+
const contentLines = content.split("\n");
|
|
200
|
+
const targetLines = target.split("\n");
|
|
201
|
+
if (targetLines.length === 0 || target.length === 0) {
|
|
202
|
+
return { aboveThresholdCount: 0 };
|
|
203
|
+
}
|
|
204
|
+
if (targetLines.length > contentLines.length) {
|
|
205
|
+
return { aboveThresholdCount: 0 };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const offsets = computeLineOffsets(contentLines);
|
|
209
|
+
|
|
210
|
+
let result = findBestFuzzyMatchCore(contentLines, targetLines, offsets, threshold, true);
|
|
211
|
+
|
|
212
|
+
if (result.best && result.best.confidence < threshold && result.best.confidence >= FALLBACK_THRESHOLD) {
|
|
213
|
+
const noDepthResult = findBestFuzzyMatchCore(contentLines, targetLines, offsets, threshold, false);
|
|
214
|
+
if (noDepthResult.best && noDepthResult.best.confidence > result.best.confidence) {
|
|
215
|
+
result = noDepthResult;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
|
|
199
222
|
export function findEditMatch(
|
|
200
223
|
content: string,
|
|
201
224
|
target: string,
|