clanka 0.0.21 → 0.0.22

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.
@@ -36,8 +36,9 @@ describe("AgentTools", () => {
36
36
  expect(output).toContain("readonly path: string;")
37
37
  expect(output).toContain("readonly startLine?: number | undefined;")
38
38
  expect(output).toContain("readonly endLine?: number | undefined;")
39
+ expect(output).toContain("readonly noIgnore?: boolean | undefined;")
39
40
  expect(output).toContain(
40
- "/** Apply a git diff / unified diff patch across one or more files. */",
41
+ "/** Apply a git diff / unified diff patch, or a wrapped apply_patch patch, across one or more files. */",
41
42
  )
42
43
  expect(output).toContain(
43
44
  "declare function applyPatch(patch: string): Promise<string>",
@@ -192,6 +193,621 @@ describe("AgentTools", () => {
192
193
  ),
193
194
  )
194
195
 
196
+ it.effect("applies wrapped apply_patch patches", () =>
197
+ Effect.gen(function* () {
198
+ const fs = yield* FileSystem.FileSystem
199
+ const tempRoot = yield* makeTempRoot("clanka-apply-patch-wrapped-")
200
+ yield* fs.makeDirectory(join(tempRoot, "src"), { recursive: true })
201
+ yield* fs.writeFileString(join(tempRoot, "src", "app.txt"), "old\n")
202
+ yield* fs.writeFileString(join(tempRoot, "obsolete.txt"), "remove me\n")
203
+
204
+ const executor = yield* Executor
205
+ const tools = yield* AgentTools
206
+ const output = yield* executor
207
+ .execute({
208
+ tools,
209
+ script: [
210
+ "const output = await applyPatch(`",
211
+ "*** Begin Patch",
212
+ "*** Update File: src/app.txt",
213
+ "*** Move to: src/main.txt",
214
+ "@@",
215
+ "-old",
216
+ "+new",
217
+ "*** Delete File: obsolete.txt",
218
+ "*** Add File: notes/hello.txt",
219
+ "+hello",
220
+ "*** End Patch",
221
+ "`)",
222
+ "console.log(output)",
223
+ ].join("\n"),
224
+ })
225
+ .pipe(
226
+ Stream.mkString,
227
+ Effect.provideServices(makeContextNoop(tempRoot)),
228
+ )
229
+
230
+ expect(output).toContain("A notes/hello.txt")
231
+ expect(output).toContain("M src/main.txt")
232
+ expect(output).toContain("D obsolete.txt")
233
+ expect(
234
+ yield* fs.readFileString(join(tempRoot, "notes", "hello.txt")),
235
+ ).toBe("hello\n")
236
+ expect(yield* fs.readFileString(join(tempRoot, "src", "main.txt"))).toBe(
237
+ "new\n",
238
+ )
239
+ yield* Effect.flip(fs.readFileString(join(tempRoot, "obsolete.txt")))
240
+ yield* Effect.flip(fs.readFileString(join(tempRoot, "src", "app.txt")))
241
+ }).pipe(
242
+ Effect.provide([
243
+ AgentToolHandlers,
244
+ Executor.layer,
245
+ ToolkitRenderer.layer,
246
+ ]),
247
+ Effect.provide(NodeServices.layer),
248
+ ),
249
+ )
250
+
251
+ it.effect(
252
+ "applies larger wrapped apply_patch patches across multiple files",
253
+ () =>
254
+ Effect.gen(function* () {
255
+ const fs = yield* FileSystem.FileSystem
256
+ const tempRoot = yield* makeTempRoot(
257
+ "clanka-apply-patch-wrapped-large-",
258
+ )
259
+ yield* fs.makeDirectory(join(tempRoot, "src"), { recursive: true })
260
+ yield* fs.makeDirectory(join(tempRoot, "docs"), { recursive: true })
261
+ yield* fs.writeFileString(
262
+ join(tempRoot, "src", "app.txt"),
263
+ "alpha\nbeta\n",
264
+ )
265
+ yield* fs.writeFileString(
266
+ join(tempRoot, "src", "config.json"),
267
+ '{"enabled":false}\n',
268
+ )
269
+ yield* fs.writeFileString(join(tempRoot, "docs", "old.md"), "legacy\n")
270
+ yield* fs.writeFileString(
271
+ join(tempRoot, "README.md"),
272
+ "# Title\nOld intro\n",
273
+ )
274
+
275
+ const executor = yield* Executor
276
+ const tools = yield* AgentTools
277
+ const output = yield* executor
278
+ .execute({
279
+ tools,
280
+ script: [
281
+ "const output = await applyPatch(`",
282
+ "*** Begin Patch",
283
+ "",
284
+ "*** Update File: src/app.txt",
285
+ "*** Move to: src/main.txt",
286
+ "@@",
287
+ " alpha",
288
+ "-beta",
289
+ "+gamma",
290
+ "",
291
+ "*** Update File: src/config.json",
292
+ "@@",
293
+ '-{"enabled":false}',
294
+ '+{"enabled":true}',
295
+ "",
296
+ "*** Update File: README.md",
297
+ "@@",
298
+ " # Title",
299
+ "-Old intro",
300
+ "+New intro",
301
+ "+More details",
302
+ "",
303
+ "*** Delete File: docs/old.md",
304
+ "",
305
+ "*** Add File: docs/new.md",
306
+ "+# Docs",
307
+ "+",
308
+ "+Updated",
309
+ "",
310
+ "*** Add File: notes/todo.txt",
311
+ "+one",
312
+ "+two",
313
+ "*** End Patch",
314
+ "`)",
315
+ "console.log(output)",
316
+ ].join("\n"),
317
+ })
318
+ .pipe(
319
+ Stream.mkString,
320
+ Effect.provideServices(makeContextNoop(tempRoot)),
321
+ )
322
+
323
+ expect(output).toContain("M src/main.txt")
324
+ expect(output).toContain("M src/config.json")
325
+ expect(output).toContain("M README.md")
326
+ expect(output).toContain("D docs/old.md")
327
+ expect(output).toContain("A docs/new.md")
328
+ expect(output).toContain("A notes/todo.txt")
329
+ expect(
330
+ yield* fs.readFileString(join(tempRoot, "src", "main.txt")),
331
+ ).toBe("alpha\ngamma\n")
332
+ expect(
333
+ yield* fs.readFileString(join(tempRoot, "src", "config.json")),
334
+ ).toBe('{"enabled":true}\n')
335
+ expect(yield* fs.readFileString(join(tempRoot, "README.md"))).toBe(
336
+ "# Title\nNew intro\nMore details\n",
337
+ )
338
+ expect(yield* fs.readFileString(join(tempRoot, "docs", "new.md"))).toBe(
339
+ "# Docs\n\nUpdated\n",
340
+ )
341
+ expect(
342
+ yield* fs.readFileString(join(tempRoot, "notes", "todo.txt")),
343
+ ).toBe("one\ntwo\n")
344
+ yield* Effect.flip(fs.readFileString(join(tempRoot, "docs", "old.md")))
345
+ yield* Effect.flip(fs.readFileString(join(tempRoot, "src", "app.txt")))
346
+ }).pipe(
347
+ Effect.provide([
348
+ AgentToolHandlers,
349
+ Executor.layer,
350
+ ToolkitRenderer.layer,
351
+ ]),
352
+ Effect.provide(NodeServices.layer),
353
+ ),
354
+ )
355
+
356
+ it.effect(
357
+ "chains wrapped apply_patch updates through in-memory renamed state",
358
+ () =>
359
+ Effect.gen(function* () {
360
+ const fs = yield* FileSystem.FileSystem
361
+ const tempRoot = yield* makeTempRoot(
362
+ "clanka-apply-patch-wrapped-state-",
363
+ )
364
+ yield* fs.makeDirectory(join(tempRoot, "src"), { recursive: true })
365
+ yield* fs.writeFileString(join(tempRoot, "src", "app.txt"), "old\n")
366
+
367
+ const executor = yield* Executor
368
+ const tools = yield* AgentTools
369
+ const output = yield* executor
370
+ .execute({
371
+ tools,
372
+ script: [
373
+ "const output = await applyPatch(`",
374
+ "*** Begin Patch",
375
+ "*** Update File: src/app.txt",
376
+ "*** Move to: src/main.txt",
377
+ "@@",
378
+ "-old",
379
+ "+new",
380
+ "*** Update File: src/main.txt",
381
+ "@@",
382
+ "-new",
383
+ "+newer",
384
+ "*** Add File: notes/hello.txt",
385
+ "+hello",
386
+ "*** Update File: notes/hello.txt",
387
+ "@@",
388
+ "-hello",
389
+ "+hello again",
390
+ "*** End Patch",
391
+ "`)",
392
+ "console.log(output)",
393
+ ].join("\n"),
394
+ })
395
+ .pipe(
396
+ Stream.mkString,
397
+ Effect.provideServices(makeContextNoop(tempRoot)),
398
+ )
399
+
400
+ expect(output).toContain("M src/main.txt")
401
+ expect(output).toContain("A notes/hello.txt")
402
+ expect(output).toContain("M notes/hello.txt")
403
+ expect(
404
+ yield* fs.readFileString(join(tempRoot, "src", "main.txt")),
405
+ ).toBe("newer\n")
406
+ expect(
407
+ yield* fs.readFileString(join(tempRoot, "notes", "hello.txt")),
408
+ ).toBe("hello again\n")
409
+ yield* Effect.flip(fs.readFileString(join(tempRoot, "src", "app.txt")))
410
+ }).pipe(
411
+ Effect.provide([
412
+ AgentToolHandlers,
413
+ Executor.layer,
414
+ ToolkitRenderer.layer,
415
+ ]),
416
+ Effect.provide(NodeServices.layer),
417
+ ),
418
+ )
419
+
420
+ it.effect("applies wrapped apply_patch patches with multiple hunks", () =>
421
+ Effect.gen(function* () {
422
+ const fs = yield* FileSystem.FileSystem
423
+ const tempRoot = yield* makeTempRoot("clanka-apply-patch-wrapped-hunks-")
424
+ yield* fs.writeFileString(
425
+ join(tempRoot, "multi.txt"),
426
+ "line1\nline2\nline3\nline4\n",
427
+ )
428
+
429
+ const executor = yield* Executor
430
+ const tools = yield* AgentTools
431
+ const output = yield* executor
432
+ .execute({
433
+ tools,
434
+ script: [
435
+ "const output = await applyPatch(`",
436
+ "*** Begin Patch",
437
+ "*** Update File: multi.txt",
438
+ "@@",
439
+ "-line2",
440
+ "+changed2",
441
+ "@@",
442
+ "-line4",
443
+ "+changed4",
444
+ "*** End Patch",
445
+ "`)",
446
+ "console.log(output)",
447
+ ].join("\n"),
448
+ })
449
+ .pipe(
450
+ Stream.mkString,
451
+ Effect.provideServices(makeContextNoop(tempRoot)),
452
+ )
453
+
454
+ expect(output).toContain("M multi.txt")
455
+ expect(yield* fs.readFileString(join(tempRoot, "multi.txt"))).toBe(
456
+ "line1\nchanged2\nline3\nchanged4\n",
457
+ )
458
+ }).pipe(
459
+ Effect.provide([
460
+ AgentToolHandlers,
461
+ Executor.layer,
462
+ ToolkitRenderer.layer,
463
+ ]),
464
+ Effect.provide(NodeServices.layer),
465
+ ),
466
+ )
467
+
468
+ it.effect(
469
+ "applies realistic multi-file git patches with repeated multi-hunk updates",
470
+ () =>
471
+ Effect.gen(function* () {
472
+ const fs = yield* FileSystem.FileSystem
473
+ const tempRoot = yield* makeTempRoot("clanka-apply-patch-realistic-")
474
+ yield* fs.makeDirectory(join(tempRoot, "dist", "internal"), {
475
+ recursive: true,
476
+ })
477
+
478
+ const initial = [
479
+ "if (reasoningStarted && !textStarted) {",
480
+ " controller.enqueue({",
481
+ ' type: "reasoning-end",',
482
+ " id: reasoningId || generateId()",
483
+ " });",
484
+ "}",
485
+ "",
486
+ "separator",
487
+ "",
488
+ "if (reasoningStarted) {",
489
+ " controller.enqueue({",
490
+ ' type: "reasoning-end",',
491
+ " id: reasoningId || generateId()",
492
+ " });",
493
+ "}",
494
+ "",
495
+ ].join("\n")
496
+
497
+ for (const path of [
498
+ join(tempRoot, "dist", "index.js"),
499
+ join(tempRoot, "dist", "index.mjs"),
500
+ join(tempRoot, "dist", "internal", "index.js"),
501
+ join(tempRoot, "dist", "internal", "index.mjs"),
502
+ ]) {
503
+ yield* fs.writeFileString(path, initial)
504
+ }
505
+
506
+ const executor = yield* Executor
507
+ const tools = yield* AgentTools
508
+ const output = yield* executor
509
+ .execute({
510
+ tools,
511
+ script: [
512
+ "const output = await applyPatch(`",
513
+ "diff --git a/dist/index.js b/dist/index.js",
514
+ "index f33510a..e887a60 100644",
515
+ "--- a/dist/index.js",
516
+ "+++ b/dist/index.js",
517
+ "@@ -1,7 +1,12 @@",
518
+ " if (reasoningStarted && !textStarted) {",
519
+ " controller.enqueue({",
520
+ ' type: "reasoning-end",',
521
+ "- id: reasoningId || generateId()",
522
+ "+ id: reasoningId || generateId(),",
523
+ "+ providerMetadata: accumulatedReasoningDetails.length > 0 ? {",
524
+ "+ openrouter: {",
525
+ "+ reasoning_details: accumulatedReasoningDetails",
526
+ "+ }",
527
+ "+ } : undefined",
528
+ " });",
529
+ " }",
530
+ "@@ -10,7 +15,12 @@",
531
+ " if (reasoningStarted) {",
532
+ " controller.enqueue({",
533
+ ' type: "reasoning-end",',
534
+ "- id: reasoningId || generateId()",
535
+ "+ id: reasoningId || generateId(),",
536
+ "+ providerMetadata: accumulatedReasoningDetails.length > 0 ? {",
537
+ "+ openrouter: {",
538
+ "+ reasoning_details: accumulatedReasoningDetails",
539
+ "+ }",
540
+ "+ } : undefined",
541
+ " });",
542
+ " }",
543
+ "diff --git a/dist/index.mjs b/dist/index.mjs",
544
+ "index 8a68833..6310cb8 100644",
545
+ "--- a/dist/index.mjs",
546
+ "+++ b/dist/index.mjs",
547
+ "@@ -1,7 +1,12 @@",
548
+ " if (reasoningStarted && !textStarted) {",
549
+ " controller.enqueue({",
550
+ ' type: "reasoning-end",',
551
+ "- id: reasoningId || generateId()",
552
+ "+ id: reasoningId || generateId(),",
553
+ "+ providerMetadata: accumulatedReasoningDetails.length > 0 ? {",
554
+ "+ openrouter: {",
555
+ "+ reasoning_details: accumulatedReasoningDetails",
556
+ "+ }",
557
+ "+ } : undefined",
558
+ " });",
559
+ " }",
560
+ "@@ -10,7 +15,12 @@",
561
+ " if (reasoningStarted) {",
562
+ " controller.enqueue({",
563
+ ' type: "reasoning-end",',
564
+ "- id: reasoningId || generateId()",
565
+ "+ id: reasoningId || generateId(),",
566
+ "+ providerMetadata: accumulatedReasoningDetails.length > 0 ? {",
567
+ "+ openrouter: {",
568
+ "+ reasoning_details: accumulatedReasoningDetails",
569
+ "+ }",
570
+ "+ } : undefined",
571
+ " });",
572
+ " }",
573
+ "diff --git a/dist/internal/index.js b/dist/internal/index.js",
574
+ "index d40fa66..8dd86d1 100644",
575
+ "--- a/dist/internal/index.js",
576
+ "+++ b/dist/internal/index.js",
577
+ "@@ -1,7 +1,12 @@",
578
+ " if (reasoningStarted && !textStarted) {",
579
+ " controller.enqueue({",
580
+ ' type: "reasoning-end",',
581
+ "- id: reasoningId || generateId()",
582
+ "+ id: reasoningId || generateId(),",
583
+ "+ providerMetadata: accumulatedReasoningDetails.length > 0 ? {",
584
+ "+ openrouter: {",
585
+ "+ reasoning_details: accumulatedReasoningDetails",
586
+ "+ }",
587
+ "+ } : undefined",
588
+ " });",
589
+ " }",
590
+ "@@ -10,7 +15,12 @@",
591
+ " if (reasoningStarted) {",
592
+ " controller.enqueue({",
593
+ ' type: "reasoning-end",',
594
+ "- id: reasoningId || generateId()",
595
+ "+ id: reasoningId || generateId(),",
596
+ "+ providerMetadata: accumulatedReasoningDetails.length > 0 ? {",
597
+ "+ openrouter: {",
598
+ "+ reasoning_details: accumulatedReasoningDetails",
599
+ "+ }",
600
+ "+ } : undefined",
601
+ " });",
602
+ " }",
603
+ "diff --git a/dist/internal/index.mjs b/dist/internal/index.mjs",
604
+ "index b0ed9d1..5695930 100644",
605
+ "--- a/dist/internal/index.mjs",
606
+ "+++ b/dist/internal/index.mjs",
607
+ "@@ -1,7 +1,12 @@",
608
+ " if (reasoningStarted && !textStarted) {",
609
+ " controller.enqueue({",
610
+ ' type: "reasoning-end",',
611
+ "- id: reasoningId || generateId()",
612
+ "+ id: reasoningId || generateId(),",
613
+ "+ providerMetadata: accumulatedReasoningDetails.length > 0 ? {",
614
+ "+ openrouter: {",
615
+ "+ reasoning_details: accumulatedReasoningDetails",
616
+ "+ }",
617
+ "+ } : undefined",
618
+ " });",
619
+ " }",
620
+ "@@ -10,7 +15,12 @@",
621
+ " if (reasoningStarted) {",
622
+ " controller.enqueue({",
623
+ ' type: "reasoning-end",',
624
+ "- id: reasoningId || generateId()",
625
+ "+ id: reasoningId || generateId(),",
626
+ "+ providerMetadata: accumulatedReasoningDetails.length > 0 ? {",
627
+ "+ openrouter: {",
628
+ "+ reasoning_details: accumulatedReasoningDetails",
629
+ "+ }",
630
+ "+ } : undefined",
631
+ " });",
632
+ " }",
633
+ "`)",
634
+ "console.log(output)",
635
+ ].join("\n"),
636
+ })
637
+ .pipe(
638
+ Stream.mkString,
639
+ Effect.provideServices(makeContextNoop(tempRoot)),
640
+ )
641
+
642
+ expect(output).toContain("M dist/index.js")
643
+ expect(output).toContain("M dist/index.mjs")
644
+ expect(output).toContain("M dist/internal/index.js")
645
+ expect(output).toContain("M dist/internal/index.mjs")
646
+
647
+ const expected = [
648
+ "if (reasoningStarted && !textStarted) {",
649
+ " controller.enqueue({",
650
+ ' type: "reasoning-end",',
651
+ " id: reasoningId || generateId(),",
652
+ " providerMetadata: accumulatedReasoningDetails.length > 0 ? {",
653
+ " openrouter: {",
654
+ " reasoning_details: accumulatedReasoningDetails",
655
+ " }",
656
+ " } : undefined",
657
+ " });",
658
+ "}",
659
+ "",
660
+ "separator",
661
+ "",
662
+ "if (reasoningStarted) {",
663
+ " controller.enqueue({",
664
+ ' type: "reasoning-end",',
665
+ " id: reasoningId || generateId(),",
666
+ " providerMetadata: accumulatedReasoningDetails.length > 0 ? {",
667
+ " openrouter: {",
668
+ " reasoning_details: accumulatedReasoningDetails",
669
+ " }",
670
+ " } : undefined",
671
+ " });",
672
+ "}",
673
+ "",
674
+ ].join("\n")
675
+
676
+ for (const path of [
677
+ join(tempRoot, "dist", "index.js"),
678
+ join(tempRoot, "dist", "index.mjs"),
679
+ join(tempRoot, "dist", "internal", "index.js"),
680
+ join(tempRoot, "dist", "internal", "index.mjs"),
681
+ ]) {
682
+ expect(yield* fs.readFileString(path)).toBe(expected)
683
+ }
684
+ }).pipe(
685
+ Effect.provide([
686
+ AgentToolHandlers,
687
+ Executor.layer,
688
+ ToolkitRenderer.layer,
689
+ ]),
690
+ Effect.provide(NodeServices.layer),
691
+ ),
692
+ )
693
+
694
+ it.effect("fails multi-file git patches atomically", () =>
695
+ Effect.gen(function* () {
696
+ const fs = yield* FileSystem.FileSystem
697
+ const tempRoot = yield* makeTempRoot("clanka-apply-patch-git-fail-")
698
+ yield* fs.makeDirectory(join(tempRoot, "src"), { recursive: true })
699
+ yield* fs.writeFileString(join(tempRoot, "src", "app.txt"), "old\n")
700
+ yield* fs.writeFileString(join(tempRoot, "keep.txt"), "keep\n")
701
+
702
+ const executor = yield* Executor
703
+ const tools = yield* AgentTools
704
+ const output = yield* executor
705
+ .execute({
706
+ tools,
707
+ script: [
708
+ "await applyPatch(`",
709
+ "diff --git a/src/app.txt b/src/main.txt",
710
+ "similarity index 100%",
711
+ "rename from src/app.txt",
712
+ "rename to src/main.txt",
713
+ "--- a/src/app.txt",
714
+ "+++ b/src/main.txt",
715
+ "@@ -1 +1 @@",
716
+ "-missing",
717
+ "+new",
718
+ "diff --git a/keep.txt b/keep.txt",
719
+ "deleted file mode 100644",
720
+ "--- a/keep.txt",
721
+ "+++ /dev/null",
722
+ "diff --git a/dev/null b/notes/hello.txt",
723
+ "new file mode 100644",
724
+ "--- /dev/null",
725
+ "+++ b/notes/hello.txt",
726
+ "@@ -0,0 +1 @@",
727
+ "+hello",
728
+ "`)",
729
+ ].join("\n"),
730
+ })
731
+ .pipe(
732
+ Stream.mkString,
733
+ Effect.provideServices(makeContextNoop(tempRoot)),
734
+ )
735
+
736
+ expect(output).toContain("applyPatch verification failed")
737
+ expect(output).toContain("Failed to find expected lines")
738
+ expect(yield* fs.readFileString(join(tempRoot, "src", "app.txt"))).toBe(
739
+ "old\n",
740
+ )
741
+ expect(yield* fs.readFileString(join(tempRoot, "keep.txt"))).toBe(
742
+ "keep\n",
743
+ )
744
+ yield* Effect.flip(fs.readFileString(join(tempRoot, "src", "main.txt")))
745
+ yield* Effect.flip(
746
+ fs.readFileString(join(tempRoot, "notes", "hello.txt")),
747
+ )
748
+ }).pipe(
749
+ Effect.provide([
750
+ AgentToolHandlers,
751
+ Executor.layer,
752
+ ToolkitRenderer.layer,
753
+ ]),
754
+ Effect.provide(NodeServices.layer),
755
+ ),
756
+ )
757
+
758
+ it.effect("fails wrapped apply_patch patches atomically", () =>
759
+ Effect.gen(function* () {
760
+ const fs = yield* FileSystem.FileSystem
761
+ const tempRoot = yield* makeTempRoot("clanka-apply-patch-wrapped-fail-")
762
+ yield* fs.makeDirectory(join(tempRoot, "src"), { recursive: true })
763
+ yield* fs.writeFileString(join(tempRoot, "src", "app.txt"), "old\n")
764
+ yield* fs.writeFileString(join(tempRoot, "keep.txt"), "keep\n")
765
+
766
+ const executor = yield* Executor
767
+ const tools = yield* AgentTools
768
+ const output = yield* executor
769
+ .execute({
770
+ tools,
771
+ script: [
772
+ "await applyPatch(`",
773
+ "*** Begin Patch",
774
+ "*** Update File: src/app.txt",
775
+ "@@",
776
+ "-missing",
777
+ "+new",
778
+ "*** Delete File: keep.txt",
779
+ "*** Add File: notes/hello.txt",
780
+ "+hello",
781
+ "*** End Patch",
782
+ "`)",
783
+ ].join("\n"),
784
+ })
785
+ .pipe(
786
+ Stream.mkString,
787
+ Effect.provideServices(makeContextNoop(tempRoot)),
788
+ )
789
+
790
+ expect(output).toContain("applyPatch verification failed")
791
+ expect(output).toContain("Failed to find expected lines")
792
+ expect(yield* fs.readFileString(join(tempRoot, "src", "app.txt"))).toBe(
793
+ "old\n",
794
+ )
795
+ expect(yield* fs.readFileString(join(tempRoot, "keep.txt"))).toBe(
796
+ "keep\n",
797
+ )
798
+ yield* Effect.flip(
799
+ fs.readFileString(join(tempRoot, "notes", "hello.txt")),
800
+ )
801
+ }).pipe(
802
+ Effect.provide([
803
+ AgentToolHandlers,
804
+ Executor.layer,
805
+ ToolkitRenderer.layer,
806
+ ]),
807
+ Effect.provide(NodeServices.layer),
808
+ ),
809
+ )
810
+
195
811
  it.effect("renames a file", () =>
196
812
  Effect.gen(function* () {
197
813
  const fs = yield* FileSystem.FileSystem
@@ -231,4 +847,108 @@ describe("AgentTools", () => {
231
847
  Effect.provide(NodeServices.layer),
232
848
  ),
233
849
  )
850
+
851
+ it.effect("rg respects ignore files by default and can disable them", () =>
852
+ Effect.gen(function* () {
853
+ const fs = yield* FileSystem.FileSystem
854
+ const tempRoot = yield* makeTempRoot("clanka-rg-ignore-")
855
+ yield* fs.writeFileString(join(tempRoot, ".ignore"), "ignored.txt\n")
856
+ yield* fs.writeFileString(
857
+ join(tempRoot, "visible.txt"),
858
+ "match visible\n",
859
+ )
860
+ yield* fs.writeFileString(
861
+ join(tempRoot, "ignored.txt"),
862
+ "match ignored\n",
863
+ )
864
+
865
+ const executor = yield* Executor
866
+ const tools = yield* AgentTools
867
+
868
+ const defaultOutput = yield* executor
869
+ .execute({
870
+ tools,
871
+ script: [
872
+ 'const output = await rg({ pattern: "match" })',
873
+ "console.log(output)",
874
+ ].join("\n"),
875
+ })
876
+ .pipe(
877
+ Stream.mkString,
878
+ Effect.provideServices(makeContextNoop(tempRoot)),
879
+ )
880
+
881
+ expect(defaultOutput).toContain("visible.txt:1:match visible")
882
+ expect(defaultOutput).not.toContain("ignored.txt:1:match ignored")
883
+
884
+ const noIgnoreOutput = yield* executor
885
+ .execute({
886
+ tools,
887
+ script: [
888
+ 'const output = await rg({ pattern: "match", noIgnore: true })',
889
+ "console.log(output)",
890
+ ].join("\n"),
891
+ })
892
+ .pipe(
893
+ Stream.mkString,
894
+ Effect.provideServices(makeContextNoop(tempRoot)),
895
+ )
896
+
897
+ expect(noIgnoreOutput).toContain("visible.txt:1:match visible")
898
+ expect(noIgnoreOutput).toContain("ignored.txt:1:match ignored")
899
+ }).pipe(
900
+ Effect.provide([
901
+ AgentToolHandlers,
902
+ Executor.layer,
903
+ ToolkitRenderer.layer,
904
+ ]),
905
+ Effect.provide(NodeServices.layer),
906
+ ),
907
+ )
908
+
909
+ it.effect("rg combines noIgnore with glob and maxLines", () =>
910
+ Effect.gen(function* () {
911
+ const fs = yield* FileSystem.FileSystem
912
+ const tempRoot = yield* makeTempRoot("clanka-rg-no-ignore-glob-")
913
+ yield* fs.writeFileString(join(tempRoot, ".ignore"), "ignored-*.txt\n")
914
+ yield* fs.writeFileString(join(tempRoot, "ignored-a.txt"), "needle one\n")
915
+ yield* fs.writeFileString(join(tempRoot, "ignored-b.txt"), "needle two\n")
916
+ yield* fs.writeFileString(
917
+ join(tempRoot, "visible.txt"),
918
+ "needle visible\n",
919
+ )
920
+
921
+ const executor = yield* Executor
922
+ const tools = yield* AgentTools
923
+ const output = yield* executor
924
+ .execute({
925
+ tools,
926
+ script: [
927
+ "const output = await rg({",
928
+ ' pattern: "needle",',
929
+ ' glob: "ignored-*.txt",',
930
+ " noIgnore: true,",
931
+ " maxLines: 1,",
932
+ "})",
933
+ "console.log(output)",
934
+ ].join("\n"),
935
+ })
936
+ .pipe(
937
+ Stream.mkString,
938
+ Effect.provideServices(makeContextNoop(tempRoot)),
939
+ )
940
+
941
+ const lines = output.trimEnd().split("\n")
942
+ expect(lines).toHaveLength(1)
943
+ expect(lines[0]).toMatch(/ignored-[ab]\.txt:1:needle (one|two)/)
944
+ expect(output).not.toContain("visible.txt:1:needle visible")
945
+ }).pipe(
946
+ Effect.provide([
947
+ AgentToolHandlers,
948
+ Executor.layer,
949
+ ToolkitRenderer.layer,
950
+ ]),
951
+ Effect.provide(NodeServices.layer),
952
+ ),
953
+ )
234
954
  })