clanka 0.2.28 → 0.2.29

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.
@@ -1,3 +1,10 @@
1
+ const callTemplateTargets = ["applyPatch", "taskComplete"] as const
2
+
3
+ const objectPropertyTargets = [
4
+ { functionName: "writeFile", propertyName: "content" },
5
+ { functionName: "updateTask", propertyName: "description" },
6
+ ] as const
7
+
1
8
  const isIdentifierChar = (char: string | undefined): boolean =>
2
9
  char !== undefined && /[A-Za-z0-9_$]/.test(char)
3
10
 
@@ -27,11 +34,11 @@ const findNextIdentifier = (
27
34
  }
28
35
 
29
36
  const skipWhitespace = (text: string, start: number): number => {
30
- let i = start
31
- while (i < text.length && /\s/.test(text[i]!)) {
32
- i++
37
+ let index = start
38
+ while (index < text.length && /\s/.test(text[index]!)) {
39
+ index++
33
40
  }
34
- return i
41
+ return index
35
42
  }
36
43
 
37
44
  const parseIdentifier = (
@@ -41,10 +48,12 @@ const parseIdentifier = (
41
48
  if (!isIdentifierStartChar(text[start])) {
42
49
  return undefined
43
50
  }
51
+
44
52
  let end = start + 1
45
53
  while (end < text.length && isIdentifierChar(text[end])) {
46
54
  end++
47
55
  }
56
+
48
57
  return {
49
58
  name: text.slice(start, end),
50
59
  end,
@@ -52,19 +61,134 @@ const parseIdentifier = (
52
61
  }
53
62
 
54
63
  const findPreviousNonWhitespace = (text: string, from: number): number => {
55
- let i = from
56
- while (i >= 0 && /\s/.test(text[i]!)) {
57
- i--
64
+ let index = from
65
+ while (index >= 0 && /\s/.test(text[index]!)) {
66
+ index--
58
67
  }
59
- return i
68
+ return index
60
69
  }
61
70
 
62
71
  const findNextNonWhitespace = (text: string, from: number): number => {
63
- let i = from
64
- while (i < text.length && /\s/.test(text[i]!)) {
65
- i++
72
+ let index = from
73
+ while (index < text.length && /\s/.test(text[index]!)) {
74
+ index++
75
+ }
76
+ return index
77
+ }
78
+
79
+ const isEscaped = (text: string, index: number): boolean => {
80
+ let slashCount = 0
81
+ let cursor = index - 1
82
+ while (cursor >= 0 && text[cursor] === "\\") {
83
+ slashCount++
84
+ cursor--
85
+ }
86
+ return slashCount % 2 === 1
87
+ }
88
+
89
+ const needsTemplateEscaping = (text: string): boolean => {
90
+ for (let index = 0; index < text.length; index++) {
91
+ const char = text[index]!
92
+ if (char === "`" && !isEscaped(text, index)) {
93
+ return true
94
+ }
95
+ if (char === "$" && text[index + 1] === "{" && !isEscaped(text, index)) {
96
+ return true
97
+ }
98
+ }
99
+ return false
100
+ }
101
+
102
+ const findTemplateEnd = (
103
+ text: string,
104
+ start: number,
105
+ isTerminator: (char: string | undefined) => boolean,
106
+ ): number => {
107
+ let end = -1
108
+ for (let index = start + 1; index < text.length; index++) {
109
+ if (text[index] !== "`" || isEscaped(text, index)) {
110
+ continue
111
+ }
112
+
113
+ if (isTerminator(text[index + 1])) {
114
+ end = index
115
+ continue
116
+ }
117
+
118
+ const next = skipWhitespace(text, index + 1)
119
+ if (isTerminator(text[next])) {
120
+ end = index
121
+ }
122
+ }
123
+ return end
124
+ }
125
+
126
+ const findTypeAnnotationAssignment = (text: string, start: number): number => {
127
+ let index = start
128
+ while (index < text.length) {
129
+ const char = text[index]!
130
+ if (char === "=") {
131
+ return index
132
+ }
133
+ if (char === "\n" || char === ";") {
134
+ return -1
135
+ }
136
+ index++
137
+ }
138
+ return -1
139
+ }
140
+
141
+ const findClosingParen = (text: string, openParen: number): number => {
142
+ let depth = 1
143
+ for (let index = openParen + 1; index < text.length; index++) {
144
+ const char = text[index]!
145
+ if (char === "(") {
146
+ depth++
147
+ continue
148
+ }
149
+ if (char === ")") {
150
+ depth--
151
+ if (depth === 0) {
152
+ return index
153
+ }
154
+ }
155
+ }
156
+ return -1
157
+ }
158
+
159
+ const findClosingBrace = (text: string, openBrace: number): number => {
160
+ let depth = 1
161
+ let stringDelimiter: '"' | "'" | "`" | undefined
162
+
163
+ for (let index = openBrace + 1; index < text.length; index++) {
164
+ const char = text[index]!
165
+
166
+ if (stringDelimiter !== undefined) {
167
+ if (char === stringDelimiter && !isEscaped(text, index)) {
168
+ stringDelimiter = undefined
169
+ }
170
+ continue
171
+ }
172
+
173
+ if (char === '"' || char === "'" || char === "`") {
174
+ stringDelimiter = char
175
+ continue
176
+ }
177
+
178
+ if (char === "{") {
179
+ depth++
180
+ continue
181
+ }
182
+
183
+ if (char === "}") {
184
+ depth--
185
+ if (depth === 0) {
186
+ return index
187
+ }
188
+ }
66
189
  }
67
- return i
190
+
191
+ return -1
68
192
  }
69
193
 
70
194
  const findObjectValueTerminator = (text: string, start: number): number => {
@@ -73,11 +197,11 @@ const findObjectValueTerminator = (text: string, start: number): number => {
73
197
  let braceDepth = 0
74
198
  let stringDelimiter: '"' | "'" | "`" | undefined
75
199
 
76
- for (let i = start; i < text.length; i++) {
77
- const char = text[i]!
200
+ for (let index = start; index < text.length; index++) {
201
+ const char = text[index]!
78
202
 
79
203
  if (stringDelimiter !== undefined) {
80
- if (char === stringDelimiter && !isEscaped(text, i)) {
204
+ if (char === stringDelimiter && !isEscaped(text, index)) {
81
205
  stringDelimiter = undefined
82
206
  }
83
207
  continue
@@ -114,7 +238,7 @@ const findObjectValueTerminator = (text: string, start: number): number => {
114
238
  }
115
239
  if (char === "}") {
116
240
  if (parenDepth === 0 && bracketDepth === 0 && braceDepth === 0) {
117
- return i
241
+ return index
118
242
  }
119
243
  if (braceDepth > 0) {
120
244
  braceDepth--
@@ -128,7 +252,7 @@ const findObjectValueTerminator = (text: string, start: number): number => {
128
252
  bracketDepth === 0 &&
129
253
  braceDepth === 0
130
254
  ) {
131
- return i
255
+ return index
132
256
  }
133
257
  }
134
258
 
@@ -140,7 +264,7 @@ const collectExpressionIdentifiers = (
140
264
  start: number,
141
265
  end: number,
142
266
  ): ReadonlySet<string> => {
143
- const out = new Set<string>()
267
+ const identifiers = new Set<string>()
144
268
  let cursor = start
145
269
 
146
270
  while (cursor < end) {
@@ -150,43 +274,16 @@ const collectExpressionIdentifiers = (
150
274
  continue
151
275
  }
152
276
 
153
- const previousNonWhitespace = findPreviousNonWhitespace(text, cursor - 1)
154
- const nextNonWhitespace = findNextNonWhitespace(text, identifier.end)
155
- if (
156
- text[previousNonWhitespace] !== "." &&
157
- text[nextNonWhitespace] !== "." &&
158
- text[nextNonWhitespace] !== "("
159
- ) {
160
- out.add(identifier.name)
277
+ const previous = findPreviousNonWhitespace(text, cursor - 1)
278
+ const next = findNextNonWhitespace(text, identifier.end)
279
+ if (text[previous] !== "." && text[next] !== "." && text[next] !== "(") {
280
+ identifiers.add(identifier.name)
161
281
  }
162
282
 
163
283
  cursor = identifier.end
164
284
  }
165
285
 
166
- return out
167
- }
168
-
169
- const isEscaped = (text: string, index: number): boolean => {
170
- let slashCount = 0
171
- let i = index - 1
172
- while (i >= 0 && text[i] === "\\") {
173
- slashCount++
174
- i--
175
- }
176
- return slashCount % 2 === 1
177
- }
178
-
179
- const needsTemplateEscaping = (text: string): boolean => {
180
- for (let i = 0; i < text.length; i++) {
181
- const char = text[i]!
182
- if (char === "`" && !isEscaped(text, i)) {
183
- return true
184
- }
185
- if (char === "$" && text[i + 1] === "{" && !isEscaped(text, i)) {
186
- return true
187
- }
188
- }
189
- return false
286
+ return identifiers
190
287
  }
191
288
 
192
289
  const normalizePatchEscapedQuotes = (text: string): string =>
@@ -217,26 +314,29 @@ const normalizeNonPatchEscapedTemplateMarkers = (text: string): string =>
217
314
  .replace(/(^|\s)\\+(?=\.[A-Za-z0-9_-]+\/)/g, "$1")
218
315
 
219
316
  const escapeTemplateLiteralContent = (text: string): string => {
220
- const normalizedPatchQuotes = normalizePatchEscapedQuotes(text)
221
- const isPatchContent = normalizedPatchQuotes.includes("*** Begin Patch")
317
+ const patchNormalized = normalizePatchEscapedQuotes(text)
318
+ const isPatchContent = patchNormalized.includes("*** Begin Patch")
222
319
  const normalized = isPatchContent
223
- ? normalizedPatchQuotes
224
- : normalizeNonPatchEscapedTemplateMarkers(normalizedPatchQuotes)
320
+ ? patchNormalized
321
+ : normalizeNonPatchEscapedTemplateMarkers(patchNormalized)
322
+
225
323
  if (
226
324
  !needsTemplateEscaping(normalized) &&
227
- !(isPatchContent && normalized.includes("\\"))
325
+ !(isPatchContent && normalized.includes('\\"'))
228
326
  ) {
229
327
  return normalized
230
328
  }
231
329
 
232
330
  let out = ""
233
- for (let i = 0; i < normalized.length; i++) {
234
- const char = normalized[i]!
331
+ for (let index = 0; index < normalized.length; index++) {
332
+ const char = normalized[index]!
333
+
235
334
  if (char === "\\") {
236
335
  if (
237
- !isPatchContent &&
238
- (normalized[i + 1] === "`" ||
239
- (normalized[i + 1] === "$" && normalized[i + 2] === "{"))
336
+ (normalized[index + 1] === "`" && isEscaped(normalized, index + 1)) ||
337
+ (normalized[index + 1] === "$" &&
338
+ normalized[index + 2] === "{" &&
339
+ isEscaped(normalized, index + 1))
240
340
  ) {
241
341
  out += "\\"
242
342
  continue
@@ -244,303 +344,196 @@ const escapeTemplateLiteralContent = (text: string): string => {
244
344
  out += "\\\\"
245
345
  continue
246
346
  }
247
- if (char === "`" && !isEscaped(normalized, i)) {
347
+
348
+ if (char === "`" && !isEscaped(normalized, index)) {
248
349
  out += "\\`"
249
350
  continue
250
351
  }
352
+
251
353
  if (
252
354
  char === "$" &&
253
- normalized[i + 1] === "{" &&
254
- !isEscaped(normalized, i)
355
+ normalized[index + 1] === "{" &&
356
+ !isEscaped(normalized, index)
255
357
  ) {
256
358
  out += "\\$"
257
359
  continue
258
360
  }
361
+
259
362
  out += char
260
363
  }
364
+
261
365
  return out
262
366
  }
263
367
 
264
- const findTemplateEnd = (
368
+ const normalizeObjectLiteralTemplateMarkers = (text: string): string =>
369
+ text.replace(/\\{2,}(?=`|\$\{)/g, "\\")
370
+
371
+ const replaceSlice = (
265
372
  text: string,
266
373
  start: number,
267
- isTerminator: (char: string | undefined) => boolean,
268
- ): number => {
269
- let end = -1
270
- for (let i = start + 1; i < text.length; i++) {
271
- if (text[i] !== "`" || isEscaped(text, i)) {
272
- continue
273
- }
274
- if (isTerminator(text[i + 1])) {
275
- end = i
276
- continue
277
- }
278
- const next = skipWhitespace(text, i + 1)
279
- if (isTerminator(text[next])) {
280
- end = i
281
- }
282
- }
283
- return end
284
- }
285
-
286
- const findTypeAnnotationAssignment = (text: string, start: number): number => {
287
- let i = start
288
- while (i < text.length) {
289
- const char = text[i]!
290
- if (char === "=") {
291
- return i
292
- }
293
- if (char === "\n" || char === ";") {
294
- return -1
295
- }
296
- i++
297
- }
298
- return -1
299
- }
300
-
301
- const findClosingParen = (text: string, openParen: number): number => {
302
- let depth = 1
303
- for (let i = openParen + 1; i < text.length; i++) {
304
- const char = text[i]!
305
- if (char === "(") {
306
- depth++
307
- continue
308
- }
309
- if (char === ")") {
310
- depth--
311
- if (depth === 0) {
312
- return i
313
- }
314
- }
315
- }
316
- return -1
317
- }
318
-
319
- const findClosingBrace = (text: string, openBrace: number): number => {
320
- let depth = 1
321
- let stringDelimiter: '"' | "'" | "`" | undefined
322
-
323
- for (let i = openBrace + 1; i < text.length; i++) {
324
- const char = text[i]!
325
-
326
- if (stringDelimiter !== undefined) {
327
- if (char === stringDelimiter && !isEscaped(text, i)) {
328
- stringDelimiter = undefined
329
- }
330
- continue
331
- }
332
-
333
- if (char === '"' || char === "'" || char === "`") {
334
- stringDelimiter = char
335
- continue
336
- }
337
-
338
- if (char === "{") {
339
- depth++
340
- continue
341
- }
342
-
343
- if (char === "}") {
344
- depth--
345
- if (depth === 0) {
346
- return i
347
- }
348
- }
349
- }
350
-
351
- return -1
352
- }
353
-
354
- const fixObjectLiteralTemplateValues = (text: string): string =>
355
- text.replace(/\\{2,}(?=`|\$\{)/g, "\\")
374
+ end: number,
375
+ replacement: string,
376
+ ): string => `${text.slice(0, start)}${replacement}${text.slice(end)}`
356
377
 
357
- const fixAssignedObjectTemplateValues = (
378
+ const rewriteTemplateContents = (
358
379
  script: string,
359
- variableName: string,
380
+ findNext: (
381
+ text: string,
382
+ from: number,
383
+ ) =>
384
+ | {
385
+ readonly contentStart: number
386
+ readonly contentEnd: number
387
+ readonly nextCursor: number
388
+ }
389
+ | undefined,
390
+ rewrite: (content: string) => string,
360
391
  ): string => {
361
392
  let out = script
362
393
  let cursor = 0
363
394
 
364
395
  while (cursor < out.length) {
365
- const variableStart = findNextIdentifier(out, variableName, cursor)
366
- if (variableStart === -1) {
396
+ const range = findNext(out, cursor)
397
+ if (range === undefined) {
367
398
  break
368
399
  }
369
400
 
370
- let assignmentStart = skipWhitespace(
371
- out,
372
- variableStart + variableName.length,
373
- )
374
- if (out[assignmentStart] === ":") {
375
- assignmentStart = findTypeAnnotationAssignment(out, assignmentStart + 1)
376
- if (assignmentStart === -1) {
377
- cursor = variableStart + variableName.length
378
- continue
379
- }
380
- }
381
-
382
- if (
383
- out[assignmentStart] !== "=" ||
384
- out[assignmentStart + 1] === "=" ||
385
- out[assignmentStart + 1] === ">"
386
- ) {
387
- cursor = variableStart + variableName.length
388
- continue
389
- }
390
-
391
- const objectStart = skipWhitespace(out, assignmentStart + 1)
392
- if (out[objectStart] !== "{") {
393
- cursor = objectStart + 1
394
- continue
395
- }
396
-
397
- const objectEnd = findClosingBrace(out, objectStart)
398
- if (objectEnd === -1) {
399
- cursor = objectStart + 1
401
+ const original = out.slice(range.contentStart, range.contentEnd)
402
+ const updated = rewrite(original)
403
+ if (updated !== original) {
404
+ out = replaceSlice(out, range.contentStart, range.contentEnd, updated)
405
+ cursor = range.nextCursor + (updated.length - original.length)
400
406
  continue
401
407
  }
402
408
 
403
- const original = out.slice(objectStart, objectEnd + 1)
404
- const escaped = fixObjectLiteralTemplateValues(original)
405
- if (escaped !== original) {
406
- out = `${out.slice(0, objectStart)}${escaped}${out.slice(objectEnd + 1)}`
407
- cursor = objectEnd + (escaped.length - original.length) + 1
408
- continue
409
- }
410
-
411
- cursor = objectEnd + 1
409
+ cursor = range.nextCursor
412
410
  }
413
411
 
414
412
  return out
415
413
  }
416
414
 
417
- const escapeRegExp = (text: string): string =>
418
- text.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")
419
-
420
- const collectObjectEntryMapSources = (
421
- script: string,
422
- valueIdentifier: string,
423
- ): ReadonlySet<string> => {
424
- const out = new Set<string>()
425
- const pattern = new RegExp(
426
- `Object\\.entries\\(\\s*([A-Za-z_$][A-Za-z0-9_$]*)\\s*\\)\\s*\\.map\\(\\s*(?:async\\s*)?\\(\\s*\\[\\s*[A-Za-z_$][A-Za-z0-9_$]*\\s*,\\s*${escapeRegExp(valueIdentifier)}\\s*\\]\\s*\\)\\s*=>`,
427
- "g",
428
- )
429
-
430
- for (const match of script.matchAll(pattern)) {
431
- const sourceIdentifier = match[1]
432
- if (sourceIdentifier !== undefined) {
433
- out.add(sourceIdentifier)
434
- }
435
- }
436
-
437
- return out
438
- }
439
-
440
- const findCallTemplateEnd = (
415
+ const findDirectCallTemplate = (
441
416
  text: string,
442
- templateStart: number,
443
- openParen: number,
444
- ): number => {
445
- const closeParen = findClosingParen(text, openParen)
446
- if (closeParen === -1) {
447
- return -1
448
- }
449
-
450
- for (let i = closeParen - 1; i > templateStart; i--) {
451
- if (text[i] === "`" && !isEscaped(text, i)) {
452
- return i
453
- }
454
- }
455
-
456
- return -1
457
- }
458
-
459
- const fixCallTemplateArgument = (
460
- script: string,
461
417
  functionName: string,
462
- ): string => {
463
- let out = script
464
- let cursor = 0
418
+ from: number,
419
+ ):
420
+ | {
421
+ readonly contentStart: number
422
+ readonly contentEnd: number
423
+ readonly nextCursor: number
424
+ }
425
+ | undefined => {
426
+ let cursor = from
465
427
 
466
- while (cursor < out.length) {
467
- const callStart = findNextIdentifier(out, functionName, cursor)
428
+ while (cursor < text.length) {
429
+ const callStart = findNextIdentifier(text, functionName, cursor)
468
430
  if (callStart === -1) {
469
- break
431
+ return undefined
470
432
  }
471
433
 
472
- const openParen = skipWhitespace(out, callStart + functionName.length)
473
- if (out[openParen] !== "(") {
434
+ const openParen = skipWhitespace(text, callStart + functionName.length)
435
+ if (text[openParen] !== "(") {
474
436
  cursor = callStart + functionName.length
475
437
  continue
476
438
  }
477
439
 
478
- const templateStart = skipWhitespace(out, openParen + 1)
479
- if (out[templateStart] !== "`") {
440
+ const templateStart = skipWhitespace(text, openParen + 1)
441
+ if (text[templateStart] !== "`") {
480
442
  cursor = openParen + 1
481
443
  continue
482
444
  }
483
445
 
484
- const templateEnd = findCallTemplateEnd(out, templateStart, openParen)
446
+ const closeParen = findClosingParen(text, openParen)
447
+ let templateEnd = -1
448
+ if (closeParen !== -1) {
449
+ for (let index = closeParen - 1; index > templateStart; index--) {
450
+ if (text[index] === "`" && !isEscaped(text, index)) {
451
+ templateEnd = index
452
+ break
453
+ }
454
+ }
455
+ } else {
456
+ const patchEnd = text.indexOf("*** End Patch", templateStart)
457
+ const searchStart = patchEnd === -1 ? templateStart + 1 : patchEnd + 1
458
+ for (let index = searchStart; index < text.length; index++) {
459
+ if (text[index] !== "`" || isEscaped(text, index)) {
460
+ continue
461
+ }
462
+ const candidate = skipWhitespace(text, index + 1)
463
+ if (text[candidate] === ")") {
464
+ templateEnd = index
465
+ break
466
+ }
467
+ }
468
+ }
469
+
485
470
  if (templateEnd === -1) {
486
471
  cursor = templateStart + 1
487
472
  continue
488
473
  }
489
474
 
490
- const original = out.slice(templateStart + 1, templateEnd)
491
- const escaped = escapeTemplateLiteralContent(original)
492
- if (escaped !== original) {
493
- out = `${out.slice(0, templateStart + 1)}${escaped}${out.slice(templateEnd)}`
494
- cursor = templateEnd + (escaped.length - original.length) + 1
495
- continue
475
+ return {
476
+ contentStart: templateStart + 1,
477
+ contentEnd: templateEnd,
478
+ nextCursor: templateEnd + 1,
496
479
  }
497
-
498
- cursor = templateEnd + 1
499
480
  }
500
481
 
501
- return out
482
+ return undefined
502
483
  }
503
484
 
504
- const fixCallObjectPropertyTemplate = (
505
- script: string,
506
- functionName: string,
507
- propertyName: string,
508
- ): string => {
509
- let out = script
510
- let cursor = 0
485
+ const findObjectPropertyTemplate = (
486
+ text: string,
487
+ target: (typeof objectPropertyTargets)[number],
488
+ from: number,
489
+ ):
490
+ | {
491
+ readonly contentStart: number
492
+ readonly contentEnd: number
493
+ readonly nextCursor: number
494
+ }
495
+ | undefined => {
496
+ let cursor = from
511
497
 
512
- while (cursor < out.length) {
513
- const callStart = findNextIdentifier(out, functionName, cursor)
498
+ while (cursor < text.length) {
499
+ const callStart = findNextIdentifier(text, target.functionName, cursor)
514
500
  if (callStart === -1) {
515
- break
501
+ return undefined
516
502
  }
517
503
 
518
- const openParen = skipWhitespace(out, callStart + functionName.length)
519
- if (out[openParen] !== "(") {
520
- cursor = callStart + functionName.length
504
+ const openParen = skipWhitespace(
505
+ text,
506
+ callStart + target.functionName.length,
507
+ )
508
+ if (text[openParen] !== "(") {
509
+ cursor = callStart + target.functionName.length
521
510
  continue
522
511
  }
523
512
 
524
- const propertyKey = findNextIdentifier(out, propertyName, openParen + 1)
513
+ const propertyKey = findNextIdentifier(
514
+ text,
515
+ target.propertyName,
516
+ openParen + 1,
517
+ )
525
518
  if (propertyKey === -1) {
526
519
  cursor = openParen + 1
527
520
  continue
528
521
  }
529
522
 
530
- const colon = skipWhitespace(out, propertyKey + propertyName.length)
531
- if (out[colon] !== ":") {
532
- cursor = propertyKey + propertyName.length
523
+ const colon = skipWhitespace(text, propertyKey + target.propertyName.length)
524
+ if (text[colon] !== ":") {
525
+ cursor = propertyKey + target.propertyName.length
533
526
  continue
534
527
  }
535
528
 
536
- const templateStart = skipWhitespace(out, colon + 1)
537
- if (out[templateStart] !== "`") {
529
+ const templateStart = skipWhitespace(text, colon + 1)
530
+ if (text[templateStart] !== "`") {
538
531
  cursor = templateStart + 1
539
532
  continue
540
533
  }
541
534
 
542
535
  const templateEnd = findTemplateEnd(
543
- out,
536
+ text,
544
537
  templateStart,
545
538
  (char) => char === "}" || char === ",",
546
539
  )
@@ -549,25 +542,21 @@ const fixCallObjectPropertyTemplate = (
549
542
  continue
550
543
  }
551
544
 
552
- const original = out.slice(templateStart + 1, templateEnd)
553
- const escaped = escapeTemplateLiteralContent(original)
554
- if (escaped !== original) {
555
- out = `${out.slice(0, templateStart + 1)}${escaped}${out.slice(templateEnd)}`
556
- cursor = templateEnd + (escaped.length - original.length) + 1
557
- continue
545
+ return {
546
+ contentStart: templateStart + 1,
547
+ contentEnd: templateEnd,
548
+ nextCursor: templateEnd + 1,
558
549
  }
559
-
560
- cursor = templateEnd + 1
561
550
  }
562
551
 
563
- return out
552
+ return undefined
564
553
  }
565
554
 
566
555
  const collectCallArgumentIdentifiers = (
567
556
  script: string,
568
557
  functionName: string,
569
558
  ): ReadonlySet<string> => {
570
- const out = new Set<string>()
559
+ const identifiers = new Set<string>()
571
560
  let cursor = 0
572
561
 
573
562
  while (cursor < script.length) {
@@ -591,36 +580,42 @@ const collectCallArgumentIdentifiers = (
591
580
 
592
581
  const argumentEnd = skipWhitespace(script, identifier.end)
593
582
  if (script[argumentEnd] === ")" || script[argumentEnd] === ",") {
594
- out.add(identifier.name)
583
+ identifiers.add(identifier.name)
595
584
  }
596
585
 
597
586
  cursor = identifier.end
598
587
  }
599
588
 
600
- return out
589
+ return identifiers
601
590
  }
602
591
 
603
- const collectCallObjectPropertyIdentifiers = (
592
+ const collectObjectPropertyIdentifiers = (
604
593
  script: string,
605
- functionName: string,
606
- propertyName: string,
594
+ target: (typeof objectPropertyTargets)[number],
607
595
  ): ReadonlySet<string> => {
608
- const out = new Set<string>()
596
+ const identifiers = new Set<string>()
609
597
  let cursor = 0
610
598
 
611
599
  while (cursor < script.length) {
612
- const callStart = findNextIdentifier(script, functionName, cursor)
600
+ const callStart = findNextIdentifier(script, target.functionName, cursor)
613
601
  if (callStart === -1) {
614
602
  break
615
603
  }
616
604
 
617
- const openParen = skipWhitespace(script, callStart + functionName.length)
605
+ const openParen = skipWhitespace(
606
+ script,
607
+ callStart + target.functionName.length,
608
+ )
618
609
  if (script[openParen] !== "(") {
619
- cursor = callStart + functionName.length
610
+ cursor = callStart + target.functionName.length
620
611
  continue
621
612
  }
622
613
 
623
- const propertyKey = findNextIdentifier(script, propertyName, openParen + 1)
614
+ const propertyKey = findNextIdentifier(
615
+ script,
616
+ target.propertyName,
617
+ openParen + 1,
618
+ )
624
619
  if (propertyKey === -1) {
625
620
  cursor = openParen + 1
626
621
  continue
@@ -628,7 +623,7 @@ const collectCallObjectPropertyIdentifiers = (
628
623
 
629
624
  const afterProperty = skipWhitespace(
630
625
  script,
631
- propertyKey + propertyName.length,
626
+ propertyKey + target.propertyName.length,
632
627
  )
633
628
  if (script[afterProperty] === ":") {
634
629
  const valueStart = skipWhitespace(script, afterProperty + 1)
@@ -639,7 +634,7 @@ const collectCallObjectPropertyIdentifiers = (
639
634
  valueStart,
640
635
  valueEnd,
641
636
  )) {
642
- out.add(identifier)
637
+ identifiers.add(identifier)
643
638
  }
644
639
  }
645
640
  cursor = valueStart + 1
@@ -647,7 +642,7 @@ const collectCallObjectPropertyIdentifiers = (
647
642
  }
648
643
 
649
644
  if (script[afterProperty] === "}" || script[afterProperty] === ",") {
650
- out.add(propertyName)
645
+ identifiers.add(target.propertyName)
651
646
  cursor = afterProperty + 1
652
647
  continue
653
648
  }
@@ -655,22 +650,88 @@ const collectCallObjectPropertyIdentifiers = (
655
650
  cursor = afterProperty + 1
656
651
  }
657
652
 
658
- return out
653
+ return identifiers
659
654
  }
660
655
 
661
- const callObjectPropertyTargets = [
662
- ["writeFile", "content"],
663
- ["updateTask", "description"],
664
- ] as const
665
-
666
- const fixTargetCallObjectPropertyTemplates = (script: string): string =>
667
- callObjectPropertyTargets.reduce(
668
- (current, [functionName, propertyName]) =>
669
- fixCallObjectPropertyTemplate(current, functionName, propertyName),
656
+ const rewriteAssignedTemplate = (
657
+ script: string,
658
+ variableName: string,
659
+ ): string =>
660
+ rewriteTemplateContents(
670
661
  script,
662
+ (text, from) => {
663
+ let cursor = from
664
+
665
+ while (cursor < text.length) {
666
+ const variableStart = findNextIdentifier(text, variableName, cursor)
667
+ if (variableStart === -1) {
668
+ return undefined
669
+ }
670
+
671
+ let assignmentStart = skipWhitespace(
672
+ text,
673
+ variableStart + variableName.length,
674
+ )
675
+ if (text[assignmentStart] === ":") {
676
+ assignmentStart = findTypeAnnotationAssignment(
677
+ text,
678
+ assignmentStart + 1,
679
+ )
680
+ if (assignmentStart === -1) {
681
+ cursor = variableStart + variableName.length
682
+ continue
683
+ }
684
+ }
685
+
686
+ if (
687
+ text[assignmentStart] !== "=" ||
688
+ text[assignmentStart + 1] === "=" ||
689
+ text[assignmentStart + 1] === ">"
690
+ ) {
691
+ cursor = variableStart + variableName.length
692
+ continue
693
+ }
694
+
695
+ const templateStart = skipWhitespace(text, assignmentStart + 1)
696
+ if (text[templateStart] !== "`") {
697
+ cursor = templateStart + 1
698
+ continue
699
+ }
700
+
701
+ const templateEnd = findTemplateEnd(
702
+ text,
703
+ templateStart,
704
+ (char) =>
705
+ char === undefined ||
706
+ char === "\n" ||
707
+ char === "\r" ||
708
+ char === ";" ||
709
+ char === "," ||
710
+ char === ")" ||
711
+ char === "}" ||
712
+ char === "]",
713
+ )
714
+ if (templateEnd === -1) {
715
+ cursor = templateStart + 1
716
+ continue
717
+ }
718
+
719
+ return {
720
+ contentStart: templateStart + 1,
721
+ contentEnd: templateEnd,
722
+ nextCursor: templateEnd + 1,
723
+ }
724
+ }
725
+
726
+ return undefined
727
+ },
728
+ escapeTemplateLiteralContent,
671
729
  )
672
730
 
673
- const fixAssignedTemplate = (script: string, variableName: string): string => {
731
+ const rewriteAssignedObjectLiteral = (
732
+ script: string,
733
+ variableName: string,
734
+ ): string => {
674
735
  let out = script
675
736
  let cursor = 0
676
737
 
@@ -701,91 +762,129 @@ const fixAssignedTemplate = (script: string, variableName: string): string => {
701
762
  continue
702
763
  }
703
764
 
704
- const templateStart = skipWhitespace(out, assignmentStart + 1)
705
- if (out[templateStart] !== "`") {
706
- cursor = templateStart + 1
765
+ const objectStart = skipWhitespace(out, assignmentStart + 1)
766
+ if (out[objectStart] !== "{") {
767
+ cursor = objectStart + 1
707
768
  continue
708
769
  }
709
770
 
710
- const templateEnd = findTemplateEnd(
711
- out,
712
- templateStart,
713
- (char) =>
714
- char === undefined ||
715
- char === "\n" ||
716
- char === "\r" ||
717
- char === ";" ||
718
- char === "," ||
719
- char === ")" ||
720
- char === "}" ||
721
- char === "]",
722
- )
723
- if (templateEnd === -1) {
724
- cursor = templateStart + 1
771
+ const objectEnd = findClosingBrace(out, objectStart)
772
+ if (objectEnd === -1) {
773
+ cursor = objectStart + 1
725
774
  continue
726
775
  }
727
776
 
728
- const original = out.slice(templateStart + 1, templateEnd)
729
- const escaped = escapeTemplateLiteralContent(original)
730
- if (escaped !== original) {
731
- out = `${out.slice(0, templateStart + 1)}${escaped}${out.slice(templateEnd)}`
732
- cursor = templateEnd + (escaped.length - original.length) + 1
777
+ const original = out.slice(objectStart, objectEnd + 1)
778
+ const updated = normalizeObjectLiteralTemplateMarkers(original)
779
+ if (updated !== original) {
780
+ out = replaceSlice(out, objectStart, objectEnd + 1, updated)
781
+ cursor = objectEnd + 1 + (updated.length - original.length)
733
782
  continue
734
783
  }
735
784
 
736
- cursor = templateEnd + 1
785
+ cursor = objectEnd + 1
737
786
  }
738
787
 
739
788
  return out
740
789
  }
741
790
 
742
- const fixAssignedTemplatesForToolCalls = (script: string): string => {
791
+ const escapeRegExp = (text: string): string =>
792
+ text.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")
793
+
794
+ const collectObjectEntryMapSources = (
795
+ script: string,
796
+ valueIdentifier: string,
797
+ ): ReadonlySet<string> => {
743
798
  const identifiers = new Set<string>()
744
- for (const functionName of ["applyPatch", "taskComplete"] as const) {
799
+ const pattern = new RegExp(
800
+ `Object\\.entries\\(\\s*([A-Za-z_$][A-Za-z0-9_$]*)\\s*\\)\\s*\\.map\\(\\s*(?:async\\s*)?\\(\\s*\\[\\s*[A-Za-z_$][A-Za-z0-9_$]*\\s*,\\s*${escapeRegExp(valueIdentifier)}\\s*\\]\\s*\\)\\s*=>`,
801
+ "g",
802
+ )
803
+
804
+ for (const match of script.matchAll(pattern)) {
805
+ if (match[1] !== undefined) {
806
+ identifiers.add(match[1])
807
+ }
808
+ }
809
+
810
+ return identifiers
811
+ }
812
+
813
+ const rewriteDirectTemplates = (script: string): string => {
814
+ let out = script
815
+
816
+ for (const target of objectPropertyTargets) {
817
+ out = rewriteTemplateContents(
818
+ out,
819
+ (text, from) => findObjectPropertyTemplate(text, target, from),
820
+ escapeTemplateLiteralContent,
821
+ )
822
+ }
823
+
824
+ for (const functionName of callTemplateTargets) {
825
+ out = rewriteTemplateContents(
826
+ out,
827
+ (text, from) => findDirectCallTemplate(text, functionName, from),
828
+ escapeTemplateLiteralContent,
829
+ )
830
+ }
831
+
832
+ return out
833
+ }
834
+
835
+ const collectReferencedTemplateIdentifiers = (
836
+ script: string,
837
+ ): {
838
+ readonly templateIdentifiers: ReadonlySet<string>
839
+ readonly objectIdentifiers: ReadonlySet<string>
840
+ } => {
841
+ const templateIdentifiers = new Set<string>()
842
+
843
+ for (const functionName of callTemplateTargets) {
745
844
  for (const identifier of collectCallArgumentIdentifiers(
746
845
  script,
747
846
  functionName,
748
847
  )) {
749
- identifiers.add(identifier)
848
+ templateIdentifiers.add(identifier)
750
849
  }
751
850
  }
752
- for (const [functionName, propertyName] of callObjectPropertyTargets) {
753
- for (const identifier of collectCallObjectPropertyIdentifiers(
754
- script,
755
- functionName,
756
- propertyName,
757
- )) {
758
- identifiers.add(identifier)
851
+
852
+ for (const target of objectPropertyTargets) {
853
+ for (const identifier of collectObjectPropertyIdentifiers(script, target)) {
854
+ templateIdentifiers.add(identifier)
759
855
  }
760
856
  }
857
+
761
858
  if (script.includes("*** Begin Patch")) {
762
- identifiers.add("patch")
859
+ templateIdentifiers.add("patch")
763
860
  }
764
861
 
765
- const objectTemplateIdentifiers = new Set<string>()
766
- for (const identifier of identifiers) {
767
- for (const sourceIdentifier of collectObjectEntryMapSources(
768
- script,
769
- identifier,
770
- )) {
771
- objectTemplateIdentifiers.add(sourceIdentifier)
862
+ const objectIdentifiers = new Set<string>()
863
+ for (const identifier of templateIdentifiers) {
864
+ for (const source of collectObjectEntryMapSources(script, identifier)) {
865
+ objectIdentifiers.add(source)
772
866
  }
773
867
  }
774
868
 
869
+ return {
870
+ templateIdentifiers,
871
+ objectIdentifiers,
872
+ }
873
+ }
874
+
875
+ const rewriteAssignedTargets = (script: string): string => {
876
+ const { templateIdentifiers, objectIdentifiers } =
877
+ collectReferencedTemplateIdentifiers(script)
878
+
775
879
  let out = script
776
- for (const identifier of identifiers) {
777
- out = fixAssignedTemplate(out, identifier)
880
+ for (const identifier of templateIdentifiers) {
881
+ out = rewriteAssignedTemplate(out, identifier)
778
882
  }
779
- for (const identifier of objectTemplateIdentifiers) {
780
- out = fixAssignedObjectTemplateValues(out, identifier)
883
+ for (const identifier of objectIdentifiers) {
884
+ out = rewriteAssignedObjectLiteral(out, identifier)
781
885
  }
886
+
782
887
  return out
783
888
  }
784
-
785
889
  export const preprocessScript = (script: string): string =>
786
- fixAssignedTemplatesForToolCalls(
787
- ["applyPatch", "taskComplete"].reduce(
788
- (current, functionName) => fixCallTemplateArgument(current, functionName),
789
- fixTargetCallObjectPropertyTemplates(script),
790
- ),
791
- )
890
+ rewriteAssignedTargets(rewriteDirectTemplates(script))