clanka 0.2.10 → 0.2.12

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.
@@ -0,0 +1,420 @@
1
+ const isIdentifierChar = (char: string | undefined): boolean =>
2
+ char !== undefined && /[A-Za-z0-9_$]/.test(char)
3
+
4
+ const isIdentifierStartChar = (char: string | undefined): boolean =>
5
+ char !== undefined && /[A-Za-z_$]/.test(char)
6
+
7
+ const hasIdentifierBoundary = (
8
+ text: string,
9
+ index: number,
10
+ length: number,
11
+ ): boolean =>
12
+ !isIdentifierChar(text[index - 1]) && !isIdentifierChar(text[index + length])
13
+
14
+ const findNextIdentifier = (
15
+ text: string,
16
+ identifier: string,
17
+ from: number,
18
+ ): number => {
19
+ let index = text.indexOf(identifier, from)
20
+ while (index !== -1) {
21
+ if (hasIdentifierBoundary(text, index, identifier.length)) {
22
+ return index
23
+ }
24
+ index = text.indexOf(identifier, index + identifier.length)
25
+ }
26
+ return -1
27
+ }
28
+
29
+ const skipWhitespace = (text: string, start: number): number => {
30
+ let i = start
31
+ while (i < text.length && /\s/.test(text[i]!)) {
32
+ i++
33
+ }
34
+ return i
35
+ }
36
+
37
+ const parseIdentifier = (
38
+ text: string,
39
+ start: number,
40
+ ): { readonly name: string; readonly end: number } | undefined => {
41
+ if (!isIdentifierStartChar(text[start])) {
42
+ return undefined
43
+ }
44
+ let end = start + 1
45
+ while (end < text.length && isIdentifierChar(text[end])) {
46
+ end++
47
+ }
48
+ return {
49
+ name: text.slice(start, end),
50
+ end,
51
+ }
52
+ }
53
+
54
+ const isEscaped = (text: string, index: number): boolean => {
55
+ let slashCount = 0
56
+ let i = index - 1
57
+ while (i >= 0 && text[i] === "\\") {
58
+ slashCount++
59
+ i--
60
+ }
61
+ return slashCount % 2 === 1
62
+ }
63
+
64
+ const needsTemplateEscaping = (text: string): boolean => {
65
+ for (let i = 0; i < text.length; i++) {
66
+ const char = text[i]!
67
+ if (char === "`" && !isEscaped(text, i)) {
68
+ return true
69
+ }
70
+ if (char === "$" && text[i + 1] === "{" && !isEscaped(text, i)) {
71
+ return true
72
+ }
73
+ }
74
+ return false
75
+ }
76
+
77
+ const escapeTemplateLiteralContent = (text: string): string => {
78
+ if (!needsTemplateEscaping(text)) {
79
+ return text
80
+ }
81
+
82
+ let out = ""
83
+ for (let i = 0; i < text.length; i++) {
84
+ const char = text[i]!
85
+ if (char === "\\") {
86
+ out += "\\\\"
87
+ continue
88
+ }
89
+ if (char === "`" && !isEscaped(text, i)) {
90
+ out += "\\`"
91
+ continue
92
+ }
93
+ if (char === "$" && text[i + 1] === "{" && !isEscaped(text, i)) {
94
+ out += "\\$"
95
+ continue
96
+ }
97
+ out += char
98
+ }
99
+ return out
100
+ }
101
+
102
+ const findTemplateEnd = (
103
+ text: string,
104
+ start: number,
105
+ isTerminator: (char: string | undefined) => boolean,
106
+ ): number => {
107
+ for (let i = start + 1; i < text.length; i++) {
108
+ if (text[i] !== "`" || isEscaped(text, i)) {
109
+ continue
110
+ }
111
+ const next = skipWhitespace(text, i + 1)
112
+ if (isTerminator(text[next])) {
113
+ return i
114
+ }
115
+ }
116
+ return -1
117
+ }
118
+
119
+ const findTypeAnnotationAssignment = (text: string, start: number): number => {
120
+ let i = start
121
+ while (i < text.length) {
122
+ const char = text[i]!
123
+ if (char === "=") {
124
+ return i
125
+ }
126
+ if (char === "\n" || char === ";") {
127
+ return -1
128
+ }
129
+ i++
130
+ }
131
+ return -1
132
+ }
133
+
134
+ const fixCallTemplateArgument = (
135
+ script: string,
136
+ functionName: string,
137
+ isTerminator: (char: string | undefined) => boolean,
138
+ ): string => {
139
+ let out = script
140
+ let cursor = 0
141
+
142
+ while (cursor < out.length) {
143
+ const callStart = findNextIdentifier(out, functionName, cursor)
144
+ if (callStart === -1) {
145
+ break
146
+ }
147
+
148
+ const openParen = skipWhitespace(out, callStart + functionName.length)
149
+ if (out[openParen] !== "(") {
150
+ cursor = callStart + functionName.length
151
+ continue
152
+ }
153
+
154
+ const templateStart = skipWhitespace(out, openParen + 1)
155
+ if (out[templateStart] !== "`") {
156
+ cursor = openParen + 1
157
+ continue
158
+ }
159
+
160
+ const templateEnd = findTemplateEnd(out, templateStart, isTerminator)
161
+ if (templateEnd === -1) {
162
+ cursor = templateStart + 1
163
+ continue
164
+ }
165
+
166
+ const original = out.slice(templateStart + 1, templateEnd)
167
+ const escaped = escapeTemplateLiteralContent(original)
168
+ if (escaped !== original) {
169
+ out = `${out.slice(0, templateStart + 1)}${escaped}${out.slice(templateEnd)}`
170
+ cursor = templateEnd + (escaped.length - original.length) + 1
171
+ continue
172
+ }
173
+
174
+ cursor = templateEnd + 1
175
+ }
176
+
177
+ return out
178
+ }
179
+
180
+ const collectCallArgumentIdentifiers = (
181
+ script: string,
182
+ functionName: string,
183
+ ): ReadonlySet<string> => {
184
+ const out = new Set<string>()
185
+ let cursor = 0
186
+
187
+ while (cursor < script.length) {
188
+ const callStart = findNextIdentifier(script, functionName, cursor)
189
+ if (callStart === -1) {
190
+ break
191
+ }
192
+
193
+ const openParen = skipWhitespace(script, callStart + functionName.length)
194
+ if (script[openParen] !== "(") {
195
+ cursor = callStart + functionName.length
196
+ continue
197
+ }
198
+
199
+ const argumentStart = skipWhitespace(script, openParen + 1)
200
+ const identifier = parseIdentifier(script, argumentStart)
201
+ if (identifier === undefined) {
202
+ cursor = openParen + 1
203
+ continue
204
+ }
205
+
206
+ const argumentEnd = skipWhitespace(script, identifier.end)
207
+ if (script[argumentEnd] === ")" || script[argumentEnd] === ",") {
208
+ out.add(identifier.name)
209
+ }
210
+
211
+ cursor = identifier.end
212
+ }
213
+
214
+ return out
215
+ }
216
+
217
+ const fixWriteFileContentTemplates = (script: string): string => {
218
+ let out = script
219
+ let cursor = 0
220
+
221
+ while (cursor < out.length) {
222
+ const callStart = findNextIdentifier(out, "writeFile", cursor)
223
+ if (callStart === -1) {
224
+ break
225
+ }
226
+
227
+ const openParen = skipWhitespace(out, callStart + "writeFile".length)
228
+ if (out[openParen] !== "(") {
229
+ cursor = callStart + "writeFile".length
230
+ continue
231
+ }
232
+
233
+ const contentKey = findNextIdentifier(out, "content", openParen + 1)
234
+ if (contentKey === -1) {
235
+ cursor = openParen + 1
236
+ continue
237
+ }
238
+
239
+ const colon = skipWhitespace(out, contentKey + "content".length)
240
+ if (out[colon] !== ":") {
241
+ cursor = contentKey + "content".length
242
+ continue
243
+ }
244
+
245
+ const templateStart = skipWhitespace(out, colon + 1)
246
+ if (out[templateStart] !== "`") {
247
+ cursor = templateStart + 1
248
+ continue
249
+ }
250
+
251
+ const templateEnd = findTemplateEnd(
252
+ out,
253
+ templateStart,
254
+ (char) => char === "}" || char === ",",
255
+ )
256
+ if (templateEnd === -1) {
257
+ cursor = templateStart + 1
258
+ continue
259
+ }
260
+
261
+ const original = out.slice(templateStart + 1, templateEnd)
262
+ const escaped = escapeTemplateLiteralContent(original)
263
+ if (escaped !== original) {
264
+ out = `${out.slice(0, templateStart + 1)}${escaped}${out.slice(templateEnd)}`
265
+ cursor = templateEnd + (escaped.length - original.length) + 1
266
+ continue
267
+ }
268
+
269
+ cursor = templateEnd + 1
270
+ }
271
+
272
+ return out
273
+ }
274
+
275
+ const collectWriteFileContentIdentifiers = (
276
+ script: string,
277
+ ): ReadonlySet<string> => {
278
+ const out = new Set<string>()
279
+ let cursor = 0
280
+
281
+ while (cursor < script.length) {
282
+ const callStart = findNextIdentifier(script, "writeFile", cursor)
283
+ if (callStart === -1) {
284
+ break
285
+ }
286
+
287
+ const openParen = skipWhitespace(script, callStart + "writeFile".length)
288
+ if (script[openParen] !== "(") {
289
+ cursor = callStart + "writeFile".length
290
+ continue
291
+ }
292
+
293
+ const contentKey = findNextIdentifier(script, "content", openParen + 1)
294
+ if (contentKey === -1) {
295
+ cursor = openParen + 1
296
+ continue
297
+ }
298
+
299
+ const afterContent = skipWhitespace(script, contentKey + "content".length)
300
+ if (script[afterContent] === ":") {
301
+ const valueStart = skipWhitespace(script, afterContent + 1)
302
+ const identifier = parseIdentifier(script, valueStart)
303
+ if (identifier !== undefined) {
304
+ const valueEnd = skipWhitespace(script, identifier.end)
305
+ if (script[valueEnd] === "}" || script[valueEnd] === ",") {
306
+ out.add(identifier.name)
307
+ }
308
+ }
309
+ cursor = valueStart + 1
310
+ continue
311
+ }
312
+
313
+ if (script[afterContent] === "}" || script[afterContent] === ",") {
314
+ out.add("content")
315
+ cursor = afterContent + 1
316
+ continue
317
+ }
318
+
319
+ cursor = afterContent + 1
320
+ }
321
+
322
+ return out
323
+ }
324
+
325
+ const fixAssignedTemplate = (script: string, variableName: string): string => {
326
+ let out = script
327
+ let cursor = 0
328
+
329
+ while (cursor < out.length) {
330
+ const variableStart = findNextIdentifier(out, variableName, cursor)
331
+ if (variableStart === -1) {
332
+ break
333
+ }
334
+
335
+ let assignmentStart = skipWhitespace(
336
+ out,
337
+ variableStart + variableName.length,
338
+ )
339
+ if (out[assignmentStart] === ":") {
340
+ assignmentStart = findTypeAnnotationAssignment(out, assignmentStart + 1)
341
+ if (assignmentStart === -1) {
342
+ cursor = variableStart + variableName.length
343
+ continue
344
+ }
345
+ }
346
+
347
+ if (
348
+ out[assignmentStart] !== "=" ||
349
+ out[assignmentStart + 1] === "=" ||
350
+ out[assignmentStart + 1] === ">"
351
+ ) {
352
+ cursor = variableStart + variableName.length
353
+ continue
354
+ }
355
+
356
+ const templateStart = skipWhitespace(out, assignmentStart + 1)
357
+ if (out[templateStart] !== "`") {
358
+ cursor = templateStart + 1
359
+ continue
360
+ }
361
+
362
+ const templateEnd = findTemplateEnd(
363
+ out,
364
+ templateStart,
365
+ (char) =>
366
+ char === undefined ||
367
+ char === ";" ||
368
+ char === "," ||
369
+ char === ")" ||
370
+ char === "}" ||
371
+ char === "]",
372
+ )
373
+ if (templateEnd === -1) {
374
+ cursor = templateStart + 1
375
+ continue
376
+ }
377
+
378
+ const original = out.slice(templateStart + 1, templateEnd)
379
+ const escaped = escapeTemplateLiteralContent(original)
380
+ if (escaped !== original) {
381
+ out = `${out.slice(0, templateStart + 1)}${escaped}${out.slice(templateEnd)}`
382
+ cursor = templateEnd + (escaped.length - original.length) + 1
383
+ continue
384
+ }
385
+
386
+ cursor = templateEnd + 1
387
+ }
388
+
389
+ return out
390
+ }
391
+
392
+ const fixAssignedTemplatesForToolCalls = (script: string): string => {
393
+ const identifiers = new Set<string>()
394
+ for (const functionName of ["applyPatch", "taskComplete"] as const) {
395
+ for (const identifier of collectCallArgumentIdentifiers(
396
+ script,
397
+ functionName,
398
+ )) {
399
+ identifiers.add(identifier)
400
+ }
401
+ }
402
+ for (const identifier of collectWriteFileContentIdentifiers(script)) {
403
+ identifiers.add(identifier)
404
+ }
405
+
406
+ let out = script
407
+ for (const identifier of identifiers) {
408
+ out = fixAssignedTemplate(out, identifier)
409
+ }
410
+ return out
411
+ }
412
+
413
+ export const preprocessScript = (script: string): string =>
414
+ fixAssignedTemplatesForToolCalls(
415
+ ["applyPatch", "taskComplete"].reduce(
416
+ (current, functionName) =>
417
+ fixCallTemplateArgument(current, functionName, (char) => char === ")"),
418
+ fixWriteFileContentTemplates(script),
419
+ ),
420
+ )
@@ -0,0 +1,187 @@
1
+ const patch = `*** Begin Patch
2
+ *** Add File: src/ScriptPreprocessing.ts
3
+ +const isIdentifierChar = (char: string | undefined): boolean =>
4
+ + char !== undefined && /[A-Za-z0-9_$]/.test(char)
5
+ +
6
+ +const hasIdentifierBoundary = (
7
+ + text: string,
8
+ + index: number,
9
+ + length: number,
10
+ +): boolean =>
11
+ + !isIdentifierChar(text[index - 1]) && !isIdentifierChar(text[index + length])
12
+ +
13
+ +const findNextIdentifier = (
14
+ + text: string,
15
+ + identifier: string,
16
+ + from: number,
17
+ +): number => {
18
+ + let index = text.indexOf(identifier, from)
19
+ + while (index !== -1) {
20
+ + if (hasIdentifierBoundary(text, index, identifier.length)) {
21
+ + return index
22
+ + }
23
+ + index = text.indexOf(identifier, index + identifier.length)
24
+ + }
25
+ + return -1
26
+ +}
27
+ +
28
+ +const skipWhitespace = (text: string, start: number): number => {
29
+ + let i = start
30
+ + while (i < text.length && /\s/.test(text[i]!)) {
31
+ + i++
32
+ + }
33
+ + return i
34
+ +}
35
+ +
36
+ +const isEscaped = (text: string, index: number): boolean => {
37
+ + let slashCount = 0
38
+ + let i = index - 1
39
+ + while (i >= 0 && text[i] === "\\") {
40
+ + slashCount++
41
+ + i--
42
+ + }
43
+ + return slashCount % 2 === 1
44
+ +}
45
+ +
46
+ +const escapeUnescapedBackticks = (text: string): string => {
47
+ + let out = ""
48
+ + for (let i = 0; i < text.length; i++) {
49
+ + const char = text[i]!
50
+ + if (char === "`" && !isEscaped(text, i)) {
51
+ + out += "\\`"
52
+ + continue
53
+ + }
54
+ + out += char
55
+ + }
56
+ + return out
57
+ +}
58
+ +
59
+ +const findTemplateEnd = (
60
+ + text: string,
61
+ + start: number,
62
+ + isTerminator: (char: string | undefined) => boolean,
63
+ +): number => {
64
+ + for (let i = start + 1; i < text.length; i++) {
65
+ + if (text[i] !== "`" || isEscaped(text, i)) {
66
+ + continue
67
+ + }
68
+ + const next = skipWhitespace(text, i + 1)
69
+ + if (isTerminator(text[next])) {
70
+ + return i
71
+ + }
72
+ + }
73
+ + return -1
74
+ +}
75
+ +
76
+ +const fixCallTemplateArgument = (
77
+ + script: string,
78
+ + functionName: string,
79
+ + isTerminator: (char: string | undefined) => boolean,
80
+ +): string => {
81
+ + let out = script
82
+ + let cursor = 0
83
+ +
84
+ + while (cursor < out.length) {
85
+ + const callStart = findNextIdentifier(out, functionName, cursor)
86
+ + if (callStart === -1) {
87
+ + break
88
+ + }
89
+ +
90
+ + const openParen = skipWhitespace(out, callStart + functionName.length)
91
+ + if (out[openParen] !== "(") {
92
+ + cursor = callStart + functionName.length
93
+ + continue
94
+ + }
95
+ +
96
+ + const templateStart = skipWhitespace(out, openParen + 1)
97
+ + if (out[templateStart] !== "`") {
98
+ + cursor = openParen + 1
99
+ + continue
100
+ + }
101
+ +
102
+ + const templateEnd = findTemplateEnd(out, templateStart, isTerminator)
103
+ + if (templateEnd === -1) {
104
+ + cursor = templateStart + 1
105
+ + continue
106
+ + }
107
+ +
108
+ + const original = out.slice(templateStart + 1, templateEnd)
109
+ + const escaped = escapeUnescapedBackticks(original)
110
+ + if (escaped !== original) {
111
+ + out = `${out.slice(0, templateStart + 1)}${escaped}${out.slice(templateEnd)}`
112
+ + cursor = templateEnd + (escaped.length - original.length) + 1
113
+ + continue
114
+ + }
115
+ +
116
+ + cursor = templateEnd + 1
117
+ + }
118
+ +
119
+ + return out
120
+ +}
121
+ +
122
+ +const fixWriteFileContentTemplates = (script: string): string => {
123
+ + let out = script
124
+ + let cursor = 0
125
+ +
126
+ + while (cursor < out.length) {
127
+ + const callStart = findNextIdentifier(out, "writeFile", cursor)
128
+ + if (callStart === -1) {
129
+ + break
130
+ + }
131
+ +
132
+ + const openParen = skipWhitespace(out, callStart + "writeFile".length)
133
+ + if (out[openParen] !== "(") {
134
+ + cursor = callStart + "writeFile".length
135
+ + continue
136
+ + }
137
+ +
138
+ + const contentKey = findNextIdentifier(out, "content", openParen + 1)
139
+ + if (contentKey === -1) {
140
+ + cursor = openParen + 1
141
+ + continue
142
+ + }
143
+ +
144
+ + const colon = skipWhitespace(out, contentKey + "content".length)
145
+ + if (out[colon] !== ":") {
146
+ + cursor = contentKey + "content".length
147
+ + continue
148
+ + }
149
+ +
150
+ + const templateStart = skipWhitespace(out, colon + 1)
151
+ + if (out[templateStart] !== "`") {
152
+ + cursor = templateStart + 1
153
+ + continue
154
+ + }
155
+ +
156
+ + const templateEnd = findTemplateEnd(
157
+ + out,
158
+ + templateStart,
159
+ + (char) => char === "}" || char === ",",
160
+ + )
161
+ + if (templateEnd === -1) {
162
+ + cursor = templateStart + 1
163
+ + continue
164
+ + }
165
+ +
166
+ + const original = out.slice(templateStart + 1, templateEnd)
167
+ + const escaped = escapeUnescapedBackticks(original)
168
+ + if (escaped !== original) {
169
+ + out = `${out.slice(0, templateStart + 1)}${escaped}${out.slice(templateEnd)}`
170
+ + cursor = templateEnd + (escaped.length - original.length) + 1
171
+ + continue
172
+ + }
173
+ +
174
+ + cursor = templateEnd + 1
175
+ + }
176
+ +
177
+ + return out
178
+ +}
179
+ +
180
+ +export const preprocessScript = (script: string): string =>
181
+ + ["applyPatch", "taskComplete"].reduce(
182
+ + (current, functionName) =>
183
+ + fixCallTemplateArgument(current, functionName, (char) => char === ")"),
184
+ + fixWriteFileContentTemplates(script),
185
+ + )
186
+ *** End Patch`;
187
+ console.log(await applyPatch(patch));