patchling 0.2.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/src/text.d.ts ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Pure text utilities ported from gptdiff's Python implementation.
3
+ *
4
+ * These have no I/O and run identically in the browser and Node.
5
+ */
6
+ /**
7
+ * Split a string into lines while keeping the line-ending characters attached,
8
+ * mirroring Python's `str.splitlines(keepends=True)` for `\n`, `\r`, `\r\n`.
9
+ *
10
+ * @param {string} s
11
+ * @returns {string[]}
12
+ */
13
+ export function splitLinesKeepEnds(s: string): string[];
14
+ /**
15
+ * Split a string into lines WITHOUT keeping line endings, mirroring Python's
16
+ * `str.splitlines()` — notably, a single trailing newline does not produce a
17
+ * trailing empty element.
18
+ *
19
+ * @param {string} s
20
+ * @returns {string[]}
21
+ */
22
+ export function splitLines(s: string): string[];
23
+ /**
24
+ * Color-code a diff for terminal output: lines starting with '-' in red and
25
+ * lines starting with '+' in green. Other lines are untouched.
26
+ *
27
+ * @param {string} diffText
28
+ * @returns {string}
29
+ */
30
+ export function colorCodeDiff(diffText: string): string;
31
+ /**
32
+ * Extract and remove the chain-of-thought reasoning block from an LLM response.
33
+ *
34
+ * The reasoning block begins with a line containing "> Reasoning" and ends with
35
+ * a "Reasoned ... seconds" marker. Multiple blocks are supported.
36
+ *
37
+ * @param {string} fullResponse
38
+ * @returns {[string, string]} tuple of [finalContent, reasoning]
39
+ */
40
+ export function swallowReasoning(fullResponse: string): [string, string];
41
+ /**
42
+ * If the LLM wrapped the file content in a Markdown code fence, unwrap it.
43
+ *
44
+ * Uses line-based fence detection (open on the first ``` line, close on the
45
+ * LAST ``` line) so inner triple-backticks inside the file content do not
46
+ * truncate the result. File content — including a trailing newline — is
47
+ * preserved verbatim.
48
+ *
49
+ * @param {string} updated
50
+ * @param {string} original
51
+ * @returns {string}
52
+ */
53
+ export function stripBadOutput(updated: string, original: string): string;
54
+ /**
55
+ * Extract the bodies of all ```diff fenced code blocks from `text`.
56
+ * Mirrors the MarkdownParser + "diff" tool collection in the Python source.
57
+ *
58
+ * @param {string} text
59
+ * @returns {string[]} block bodies in document order
60
+ */
61
+ export function extractDiffBlocks(text: string): string[];
62
+ /**
63
+ * Remove `<think>...</think>` blocks from a response, returning the content
64
+ * outside any think tool. Mirrors call_llm_for_apply_with_think_tool_available.
65
+ *
66
+ * @param {string} response
67
+ * @returns {string}
68
+ */
69
+ export function stripThinkTags(response: string): string;
package/src/text.js ADDED
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Pure text utilities ported from gptdiff's Python implementation.
3
+ *
4
+ * These have no I/O and run identically in the browser and Node.
5
+ */
6
+
7
+ /**
8
+ * Split a string into lines while keeping the line-ending characters attached,
9
+ * mirroring Python's `str.splitlines(keepends=True)` for `\n`, `\r`, `\r\n`.
10
+ *
11
+ * @param {string} s
12
+ * @returns {string[]}
13
+ */
14
+ export function splitLinesKeepEnds(s) {
15
+ if (s === '') return [];
16
+ const lines = [];
17
+ let i = 0;
18
+ let start = 0;
19
+ while (i < s.length) {
20
+ const ch = s[i];
21
+ if (ch === '\n') {
22
+ lines.push(s.slice(start, i + 1));
23
+ i += 1;
24
+ start = i;
25
+ } else if (ch === '\r') {
26
+ if (s[i + 1] === '\n') {
27
+ lines.push(s.slice(start, i + 2));
28
+ i += 2;
29
+ } else {
30
+ lines.push(s.slice(start, i + 1));
31
+ i += 1;
32
+ }
33
+ start = i;
34
+ } else {
35
+ i += 1;
36
+ }
37
+ }
38
+ if (start < s.length) lines.push(s.slice(start));
39
+ return lines;
40
+ }
41
+
42
+ /**
43
+ * Split a string into lines WITHOUT keeping line endings, mirroring Python's
44
+ * `str.splitlines()` — notably, a single trailing newline does not produce a
45
+ * trailing empty element.
46
+ *
47
+ * @param {string} s
48
+ * @returns {string[]}
49
+ */
50
+ export function splitLines(s) {
51
+ if (s === '') return [];
52
+ const parts = s.split(/\r\n|\r|\n/);
53
+ if (parts.length && parts[parts.length - 1] === '') parts.pop();
54
+ return parts;
55
+ }
56
+
57
+ /**
58
+ * Color-code a diff for terminal output: lines starting with '-' in red and
59
+ * lines starting with '+' in green. Other lines are untouched.
60
+ *
61
+ * @param {string} diffText
62
+ * @returns {string}
63
+ */
64
+ export function colorCodeDiff(diffText) {
65
+ const red = '\x1b[31m';
66
+ const green = '\x1b[32m';
67
+ const reset = '\x1b[0m';
68
+
69
+ return diffText
70
+ .split('\n')
71
+ .map((line) => {
72
+ if (line.startsWith('-')) return `${red}${line}${reset}`;
73
+ if (line.startsWith('+')) return `${green}${line}${reset}`;
74
+ return line;
75
+ })
76
+ .join('\n');
77
+ }
78
+
79
+ /**
80
+ * Extract and remove the chain-of-thought reasoning block from an LLM response.
81
+ *
82
+ * The reasoning block begins with a line containing "> Reasoning" and ends with
83
+ * a "Reasoned ... seconds" marker. Multiple blocks are supported.
84
+ *
85
+ * @param {string} fullResponse
86
+ * @returns {[string, string]} tuple of [finalContent, reasoning]
87
+ */
88
+ export function swallowReasoning(fullResponse) {
89
+ const pattern = />\s*Reasoning[\s\S]*?Reasoned[\s\S]*?seconds/g;
90
+ const reasoningList = [];
91
+
92
+ const finalContent = fullResponse.replace(pattern, (match) => {
93
+ const reasoning = match
94
+ .split(/\r\n|\r|\n/)
95
+ .map((line) => line.replace(/^\++/, '').trim())
96
+ .join('\n')
97
+ .trim();
98
+ reasoningList.push(reasoning);
99
+ return '';
100
+ });
101
+
102
+ return [finalContent.trim(), reasoningList.join('\n')];
103
+ }
104
+
105
+ /**
106
+ * If the LLM wrapped the file content in a Markdown code fence, unwrap it.
107
+ *
108
+ * Uses line-based fence detection (open on the first ``` line, close on the
109
+ * LAST ``` line) so inner triple-backticks inside the file content do not
110
+ * truncate the result. File content — including a trailing newline — is
111
+ * preserved verbatim.
112
+ *
113
+ * @param {string} updated
114
+ * @param {string} original
115
+ * @returns {string}
116
+ */
117
+ export function stripBadOutput(updated, original) {
118
+ // If the file itself starts with a code fence, don't try to unwrap.
119
+ if (original.replace(/^\s+/, '').startsWith('```')) {
120
+ return updated;
121
+ }
122
+
123
+ const lines = splitLinesKeepEnds(updated);
124
+
125
+ // Find the first line that opens a fence (possibly after a preamble).
126
+ let openIdx = null;
127
+ for (let i = 0; i < lines.length; i++) {
128
+ if (lines[i].replace(/^\s+/, '').startsWith('```')) {
129
+ openIdx = i;
130
+ break;
131
+ }
132
+ }
133
+ if (openIdx === null) return updated;
134
+
135
+ // Find the LAST line that is exactly a closing fence after the opener.
136
+ let closeIdx = null;
137
+ for (let i = lines.length - 1; i > openIdx; i--) {
138
+ if (lines[i].trim() === '```') {
139
+ closeIdx = i;
140
+ break;
141
+ }
142
+ }
143
+ if (closeIdx === null) return updated;
144
+
145
+ let inner = lines.slice(openIdx + 1, closeIdx).join('');
146
+
147
+ // If the opener was a bare ``` and the model put the language on the next
148
+ // line instead (e.g. "```\npython\n..."), drop that stray language tag.
149
+ if (lines[openIdx].trim() === '```') {
150
+ const innerLines = splitLinesKeepEnds(inner);
151
+ if (innerLines.length > 1) {
152
+ const firstInner = innerLines[0].trim();
153
+ if (firstInner && !firstInner.includes(' ') && firstInner.toLowerCase() !== 'diff') {
154
+ inner = innerLines.slice(1).join('');
155
+ }
156
+ }
157
+ }
158
+
159
+ return inner;
160
+ }
161
+
162
+ /**
163
+ * Extract the bodies of all ```diff fenced code blocks from `text`.
164
+ * Mirrors the MarkdownParser + "diff" tool collection in the Python source.
165
+ *
166
+ * @param {string} text
167
+ * @returns {string[]} block bodies in document order
168
+ */
169
+ export function extractDiffBlocks(text) {
170
+ const blocks = [];
171
+ const lines = text.split(/\r\n|\r|\n/);
172
+ let inBlock = false;
173
+ let current = [];
174
+ const flush = () => {
175
+ // Mirror the Markdown tool parser: the captured block body keeps the
176
+ // newline that precedes the closing fence.
177
+ blocks.push(current.length ? current.join('\n') + '\n' : '');
178
+ };
179
+ for (const line of lines) {
180
+ const stripped = line.trim();
181
+ if (!inBlock) {
182
+ // Only fences whose language is exactly "diff" open a collected block,
183
+ // matching the Python source (which routes solely the "diff" tool).
184
+ if (stripped.startsWith('```') && stripped.slice(3).trim() === 'diff') {
185
+ inBlock = true;
186
+ current = [];
187
+ }
188
+ continue;
189
+ }
190
+ if (stripped.startsWith('```')) {
191
+ flush();
192
+ inBlock = false;
193
+ current = [];
194
+ continue;
195
+ }
196
+ current.push(line);
197
+ }
198
+ // Force-close an unterminated block (the parser keeps the trailing buffer).
199
+ if (inBlock) flush();
200
+ return blocks;
201
+ }
202
+
203
+ /**
204
+ * Remove `<think>...</think>` blocks from a response, returning the content
205
+ * outside any think tool. Mirrors call_llm_for_apply_with_think_tool_available.
206
+ *
207
+ * @param {string} response
208
+ * @returns {string}
209
+ */
210
+ export function stripThinkTags(response) {
211
+ const [withoutReasoning] = swallowReasoning(response);
212
+ let withoutThink = withoutReasoning.replace(/<think>[\s\S]*?<\/think>/g, '');
213
+ // Force-close a dangling, unterminated <think> tag (mirrors the XML parser,
214
+ // which swallows everything after an unclosed tag) so chain-of-thought is not
215
+ // written into the file.
216
+ withoutThink = withoutThink.replace(/<think>[\s\S]*$/, '');
217
+ return withoutThink.trim();
218
+ }