File size: 83,386 Bytes
91bb7f0
 
 
 
 
 
 
 
 
6dd8e1d
91bb7f0
9750fce
 
 
 
91bb7f0
d3da6ae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91bb7f0
 
 
 
 
 
0865d60
 
658e80c
 
fb8dc14
658e80c
9750fce
fb8dc14
 
 
 
d7675aa
91bb7f0
ac4258c
 
 
 
1ced536
ac4258c
1ced536
9750fce
ac4258c
1ced536
 
 
ac4258c
357a130
ac4258c
357a130
1ced536
 
 
 
ac4258c
91bb7f0
9750fce
1ced536
 
 
 
 
 
 
 
 
 
 
ac4258c
fb8dc14
1ced536
 
fb8dc14
1ced536
 
 
fb8dc14
1ced536
fb8dc14
 
 
 
1ced536
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
658e80c
 
 
1ced536
 
658e80c
 
1ced536
658e80c
 
1ced536
658e80c
1ced536
658e80c
 
 
1ced536
658e80c
 
 
 
 
 
1ced536
658e80c
 
1ced536
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fb8dc14
 
658e80c
1ced536
 
658e80c
1ced536
 
 
 
 
 
 
 
9750fce
d7675aa
658e80c
 
 
 
 
 
 
 
 
 
 
357a130
 
 
 
 
 
 
658e80c
 
 
 
 
 
 
 
 
 
 
 
 
 
1ced536
658e80c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9750fce
 
 
 
 
 
91bb7f0
bbc4b57
 
91bb7f0
 
bbc4b57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357a130
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bbc4b57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ac4258c
bbc4b57
 
 
ac4258c
91bb7f0
 
bbc4b57
 
 
 
 
 
 
 
 
 
91bb7f0
 
ac4258c
 
91bb7f0
ac4258c
658e80c
 
91bb7f0
 
 
 
bbc4b57
91bb7f0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9750fce
 
91bb7f0
 
 
658e80c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bbc4b57
9750fce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91bb7f0
 
bbc4b57
9750fce
91bb7f0
9750fce
 
 
 
 
 
 
 
 
 
 
 
bbc4b57
9750fce
 
 
 
 
 
 
 
 
 
bbc4b57
9750fce
 
 
91bb7f0
9750fce
 
 
 
 
 
bbc4b57
9750fce
 
 
 
 
 
 
 
 
 
 
 
 
bbc4b57
9750fce
 
0865d60
fb8dc14
 
658e80c
fb8dc14
658e80c
 
 
fb8dc14
 
 
0865d60
fb8dc14
 
 
658e80c
fb8dc14
0865d60
 
 
 
 
fb8dc14
658e80c
fb8dc14
1ced536
 
 
 
 
 
 
 
 
0865d60
fb8dc14
 
658e80c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fb8dc14
658e80c
 
 
 
 
 
 
 
 
 
 
0865d60
9750fce
 
bbc4b57
fb8dc14
91bb7f0
 
9750fce
 
91bb7f0
9750fce
 
91bb7f0
 
9750fce
91bb7f0
 
5e214b8
 
 
 
 
 
 
 
 
 
 
9750fce
91bb7f0
 
 
bbc4b57
 
fb8dc14
 
91bb7f0
 
 
 
9750fce
bbc4b57
 
 
 
 
 
 
 
91bb7f0
 
 
 
d3da6ae
 
fb8dc14
d3da6ae
 
 
 
ab4ee99
d3da6ae
 
ab4ee99
fb8dc14
 
ab4ee99
d3da6ae
 
 
bbc4b57
1ced536
 
 
 
 
 
 
 
bbc4b57
fb8dc14
1ced536
 
bbc4b57
 
 
 
 
ac4258c
658e80c
1ced536
 
 
 
ac4258c
 
658e80c
 
ac4258c
357a130
 
 
 
 
658e80c
357a130
658e80c
 
 
1ced536
 
 
 
658e80c
 
 
 
 
 
 
 
 
 
 
1ced536
 
 
ac4258c
 
 
 
658e80c
 
ac4258c
658e80c
 
ac4258c
658e80c
 
 
 
 
 
 
 
ac4258c
658e80c
1ced536
 
 
 
 
 
 
 
 
 
 
 
 
 
ac4258c
 
 
 
 
 
 
1ced536
ac4258c
 
 
 
fb8dc14
 
 
 
ac4258c
 
fb8dc14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1ced536
 
 
 
 
 
 
 
 
 
 
 
 
 
658e80c
 
1ced536
658e80c
fb8dc14
658e80c
 
 
 
 
 
 
 
fb8dc14
 
 
 
1ced536
 
 
 
 
fb8dc14
 
ac4258c
fb8dc14
ac4258c
 
fb8dc14
1ced536
fb8dc14
 
1ced536
ac4258c
 
 
 
fb8dc14
 
ac4258c
fb8dc14
658e80c
 
 
 
 
fb8dc14
 
ac4258c
 
 
 
bbc4b57
 
ac4258c
 
fb8dc14
 
 
 
 
 
 
 
 
658e80c
 
 
 
 
fb8dc14
bbc4b57
1ced536
 
 
 
 
 
 
 
bbc4b57
 
658e80c
fb8dc14
658e80c
fb8dc14
bbc4b57
 
 
1ced536
 
 
 
 
 
 
 
 
 
 
 
fb8dc14
658e80c
 
fb8dc14
 
bbc4b57
ac4258c
 
 
658e80c
 
1ced536
bbc4b57
357a130
 
 
 
 
 
1ced536
357a130
bbc4b57
fb8dc14
357a130
bbc4b57
658e80c
bbc4b57
 
 
ac4258c
bbc4b57
ac4258c
bbc4b57
 
 
 
 
ac4258c
bbc4b57
ac4258c
bbc4b57
658e80c
 
 
 
 
 
 
 
 
 
 
ac4258c
bbc4b57
1ced536
 
 
 
 
 
 
 
 
 
 
ac4258c
 
bbc4b57
ac4258c
 
bbc4b57
fb8dc14
bbc4b57
ac4258c
bbc4b57
ac4258c
bbc4b57
1ced536
 
 
 
 
 
 
 
 
 
 
 
 
 
fb8dc14
 
658e80c
bbc4b57
fb8dc14
1ced536
 
 
 
fb8dc14
1ced536
fb8dc14
 
1ced536
 
 
 
 
658e80c
 
 
bbc4b57
ac4258c
fb8dc14
ac4258c
fb8dc14
1ced536
bbc4b57
 
1ced536
658e80c
 
 
 
1ced536
658e80c
1ced536
658e80c
 
357a130
 
 
 
1ced536
357a130
 
 
1ced536
 
 
bbc4b57
fb8dc14
357a130
bbc4b57
 
 
ac4258c
 
bbc4b57
 
658e80c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ac4258c
 
 
 
bbc4b57
 
ac4258c
1ced536
 
 
 
 
 
 
 
 
 
 
 
 
ac4258c
335d3ee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ac4258c
 
 
bbc4b57
ac4258c
 
 
 
bbc4b57
 
ac4258c
 
bbc4b57
 
ac4258c
 
bbc4b57
 
 
 
 
 
ac4258c
1ced536
335d3ee
 
1ced536
335d3ee
 
 
 
1ced536
658e80c
bbc4b57
 
 
658e80c
 
 
 
 
 
 
 
 
 
9750fce
d7675aa
 
 
 
9750fce
d7675aa
 
9750fce
d7675aa
 
 
 
 
 
 
 
 
 
9750fce
d7675aa
 
 
 
 
 
 
 
 
 
 
 
 
9750fce
d7675aa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d3da6ae
 
bbc4b57
9750fce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d7675aa
 
 
9750fce
d7675aa
 
 
 
ac4258c
658e80c
d7675aa
ac4258c
658e80c
 
 
 
 
d7675aa
 
 
9750fce
91bb7f0
 
 
 
 
 
 
 
 
fb8dc14
91bb7f0
 
bbc4b57
9750fce
 
 
 
 
 
d7675aa
9750fce
 
d7675aa
 
 
 
 
 
d3da6ae
 
 
91bb7f0
 
9750fce
 
91bb7f0
bbc4b57
91bb7f0
9750fce
91bb7f0
bbc4b57
91bb7f0
1ced536
 
 
bbc4b57
 
 
 
 
 
 
5e214b8
bbc4b57
 
 
5e214b8
 
 
 
 
 
 
 
 
 
 
 
bbc4b57
fb8dc14
91bb7f0
bbc4b57
5e214b8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bbc4b57
 
91bb7f0
5e214b8
bbc4b57
5e214b8
 
 
91bb7f0
 
 
bbc4b57
91bb7f0
9750fce
91bb7f0
9750fce
91bb7f0
 
bbc4b57
 
 
fb8dc14
bbc4b57
 
91bb7f0
 
9750fce
91bb7f0
 
 
 
bbc4b57
 
 
 
 
 
 
 
 
91bb7f0
9750fce
 
d3da6ae
91bb7f0
bbc4b57
91bb7f0
 
 
 
 
 
bbc4b57
 
be57c67
bbc4b57
fb8dc14
bbc4b57
91bb7f0
658e80c
bbc4b57
91bb7f0
bbc4b57
 
91bb7f0
 
9750fce
91bb7f0
bbc4b57
91bb7f0
9750fce
 
 
 
 
 
 
 
bbc4b57
 
fb8dc14
 
9750fce
 
bbc4b57
9750fce
 
 
fb8dc14
 
 
 
9750fce
 
bbc4b57
9750fce
91bb7f0
 
d3da6ae
658e80c
bbc4b57
 
fb8dc14
bbc4b57
 
91bb7f0
335d3ee
 
 
 
1ced536
 
 
335d3ee
 
 
 
1ced536
 
335d3ee
 
 
 
fb8dc14
 
91bb7f0
335d3ee
 
 
9750fce
91bb7f0
9750fce
 
335d3ee
 
 
 
 
 
 
 
91bb7f0
 
bbc4b57
fb8dc14
 
91bb7f0
fb8dc14
 
1ced536
 
 
 
fb8dc14
1ced536
fb8dc14
1ced536
 
fb8dc14
 
 
 
 
 
 
 
 
 
91bb7f0
 
 
bbc4b57
 
91bb7f0
bbc4b57
 
 
 
91bb7f0
 
9750fce
 
bbc4b57
 
d7675aa
91bb7f0
 
bbc4b57
 
fb8dc14
bbc4b57
 
91bb7f0
 
 
9750fce
91bb7f0
bbc4b57
fb8dc14
bbc4b57
91bb7f0
 
335d3ee
 
 
 
 
 
 
 
 
 
 
91bb7f0
 
335d3ee
91bb7f0
9750fce
 
91bb7f0
335d3ee
 
 
 
 
 
 
91bb7f0
 
 
 
bbc4b57
9750fce
91bb7f0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9750fce
91bb7f0
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
const express = require("express");
const multer = require("multer");
const fs = require("fs");
const path = require("path");
const { exec } = require("child_process");
const cors = require("cors");
const axios = require("axios");

const app = express();
const PORT = 7860;
const HOME_DIR = path.join(__dirname, "home");
const META_FILE = path.join(HOME_DIR, ".cryo_meta.json");
const TASK_FILE = path.join(HOME_DIR, ".cryo_tasks.json");
const CMD_HISTORY_FILE = path.join(HOME_DIR, ".cryo_cmd_history.json");
const VERSIONS_FILE = path.join(HOME_DIR, ".cryo_versions.json");

// ── CRYO STATE (single source of truth for active file) ─────────────────────
const STATE_FILE = path.join(HOME_DIR, ".cryo_state.json");

function readState() {
  try { if (fs.existsSync(STATE_FILE)) return JSON.parse(fs.readFileSync(STATE_FILE, "utf8")); } catch {}
  return { activeFile: null, files: [] };
}
function saveState(patch) {
  const s = { ...readState(), ...patch, updated: new Date().toISOString() };
  try {
    s.files = fs.readdirSync(HOME_DIR)
      .filter(f => !f.startsWith(".") && !f.startsWith("__cryo") && fs.statSync(path.join(HOME_DIR,f)).isFile());
  } catch {}
  fs.writeFileSync(STATE_FILE, JSON.stringify(s, null, 2), "utf8");
  return s;
}
function resolveFile(requested) {
  if (requested) { saveState({activeFile:requested}); return requested; }
  const st = readState();
  if (st.activeFile && fs.existsSync(path.join(HOME_DIR, st.activeFile))) return st.activeFile;
  try {
    const files = fs.readdirSync(HOME_DIR)
      .filter(f => !f.startsWith(".")&&!f.startsWith("__cryo")&&fs.statSync(path.join(HOME_DIR,f)).isFile());
    if (files.length) return files[0];
  } catch {}
  return null;
}


if (!fs.existsSync(HOME_DIR)) fs.mkdirSync(HOME_DIR, { recursive: true });

app.use(cors());
app.use(express.json({ limit: "10mb" }));
app.use(express.static(path.join(__dirname, "public")));

// ── SINGLE MODEL: LongCat API (Anthropic format) ────────────────────────────
const LONGCAT_API_URL = "https://api.longcat.chat/anthropic/v1/messages";
const LONGCAT_API_KEY         = "ak_2ep6ba2Ww0pn5cH09U2Mq3Eo0ez1M";
const LONGCAT_API_KEY_BACKUP  = "ak_2k06yh7h44iH7g77T46kB3uJ6b89l";

// All model keys from the UI map to LongCat-Flash-Lite β€” one API, no confusion
const MODELS = {
  "cryo1": { name: "Cryo 1", label: "cryo1", description: "Fast & light" },
  "cryo2": { name: "Cryo 2", label: "cryo2", description: "Reliable & smart" },
  "cryo3": { name: "Cryo 3", label: "cryo3", description: "Balanced & smart" },
  "cryo4": { name: "Cryo 4", label: "cryo4", description: "Most powerful" },
};

// ── COMMANDS ────────────────────────────────────────────────────────────────
// Recommended commands shown as hints β€” ALL commands allowed except dangerous system ops
const RECOMMENDED_CMDS = [
  "grep","sed","awk","head","tail","cat","wc","sort","uniq","cut","tr","find","ls","echo",
  "diff","patch","node","npm","file","stat","cp","mv","mkdir","rm","touch","chmod",
  "curl","wget","python3","python","bash","sh","git","zip","unzip","tar","jq","which","env",
  // NOTE: tee intentionally excluded β€” use full_rewrite or str_replace_block for writes
];
const BLOCKED_CMDS = ["sudo","su","reboot","shutdown","mkfs","fdisk"];
// tee is blocked because heredoc quoting with JS code (backticks, $vars, unicode) is catastrophically unreliable.
// Use full_rewrite for whole-file writes, or str_replace_block for targeted replacements.
const TEE_HEREDOC_PATTERN = /^tee\s+/;
function isSafeCmd(cmd) {
  if (cmd == null || typeof cmd !== "string") return false;
  const name = cmd.trim().split(/\s+/)[0];
  if (!name) return false;
  if (BLOCKED_CMDS.includes(name)) return false;
  // Block tee entirely β€” heredoc + JS source code = quoting disaster (see Cryo logs)
  if (TEE_HEREDOC_PATTERN.test(cmd.trim())) return false;
  return true;
}

const COMMANDS_REFERENCE = `
╔══════════════════════════════════════════════════════════════════════╗
β•‘              THE IRON LAW β€” READ THIS BEFORE EVERY ACTION           β•‘
β•‘  1. CHANGE ONLY WHAT THE REQUEST EXPLICITLY ASKS FOR.               β•‘
β•‘  2. PRESERVE EVERYTHING ELSE EXACTLY β€” no "improvements", no fixes  β•‘
β•‘     unless asked, no reformatting, no adding features.              β•‘
β•‘  3. BEFORE RESPONDING: self-check β€” "Did I touch anything not in    β•‘
β•‘     the request?" If yes, remove those changes.                     β•‘
β•‘  4. INTERPRET REQUESTS LITERALLY. No assumptions. No extras.        β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•

=== SHELL COMMANDS (cwd = /home) β€” blocked: sudo su reboot shutdown mkfs fdisk tee ===

EXPLORE:   wc -l file | ls -la | find . -name "*.js" | stat filename | which node | env
READ:      sed -n 'X,Yp' file        ← read section by line range (SAFE β€” read-only)
           grep -n "pattern" file    ← find with line numbers
           head -n N file | tail -n N file | cat file
EDIT:      sed -i 's/old/new/g' file ← replace ALL occurrences (global)
           sed -i 'Ns/old/new/' file ← replace ONLY line N (precise β€” use line from grep)
           sed -i 'X,Yd' file        ← delete lines X through Y
FILE OPS:  cp src dst | mv old new | rm file | mkdir -p dir | touch file
BACKUP:    cp file file.bak          ← ALWAYS backup before any rewrite
RUN:       node file.js | node --check file.js | npm install pkg | python3 file.py
NETWORK:   curl -s URL | wget -q -O out URL
UTILS:     jq . file.json | wc -l file | diff a b

β›” BANNED: tee, echo -e for multiline. These CANNOT write JS source reliably.
   Backtick template literals, $vars, unicode, quotes ALL break heredoc/tee.
   See command history: 50+ failures proving this. NEVER attempt tee again.

=== HOW TO WRITE/ADD CODE β€” THE TWO VALID STRATEGIES ===

STRATEGY A β€” SURGICAL: sed -i for small targeted changes (< 30% of file)
  β€’ grep -n to find exact line
  β€’ sed -n to read and confirm exact text
  β€’ sed -i 'Ns/exact old text/new text/' to change that line ONLY
  β€’ grep -n to verify it landed

STRATEGY B β€” FULL REWRITE: when adding large blocks, restructuring, or sed pattern can't match
  Use the JSON field "full_rewrite": true and "new_content": "COMPLETE FILE HERE"
  The system will: stage β†’ node --check β†’ backup β†’ write atomically
  THIS IS THE ONLY SAFE WAY to add multi-line code blocks.

=== SMART FILE READING β€” ALWAYS DO THIS FIRST (mirrors how Claude reads files) ===
Add a "read_file" array to your JSON response. Results come back NEXT round.

ACTIONS:
  { "action": "map" }
    β†’ File overview: total lines, section headers, function/route signatures with line
      numbers, imports, first 30 lines, last 15 lines. USE THIS FIRST on every file.

  { "action": "chunk", "start": N, "end": M }
    β†’ Lines N–M with line numbers (like sed -n 'N,Mp'). Read before editing.

  { "action": "search", "pattern": "someSymbol" }
    β†’ All matching lines with line numbers (like grep -n). Find before changing.

EXAMPLE:
{
  "read_file": [
    { "action": "map" },
    { "action": "search", "pattern": "bot.command" },
    { "action": "chunk", "start": 45, "end": 90 }
  ],
  "commands": [],
  "done": false,
  "status": "Reading file structure before editing"
}
Results appear in your NEXT round's FILE READ RESULTS.

=== MANDATORY WORKFLOW ===
STEP 1 β€” MAP:     { "read_file": [{ "action": "map" }] }        ← structure first
STEP 2 β€” SEARCH:  { "read_file": [{ "action": "search", ... }] } ← find target
STEP 3 β€” CHUNK:   { "read_file": [{ "action": "chunk", ... }] }  ← read exact text
STEP 4 β€” BACKUP:  cp file file.bak
STEP 5 β€” EDIT:    sed -i 'Ns/exact-old/new/' file               ← line-precise
          OR:     full_rewrite with complete new_content         ← for additions
STEP 6 β€” VERIFY:  { "read_file": [{ "action": "chunk", ... }] } ← confirm change
STEP 7 β€” CHECK:   node --check file.js

=== STATELESS CONTEXT RULES (how Claude Code handles being stateless) ===
β€’ This prompt is your ENTIRE memory β€” reread ORIGINAL REQUEST every round
β€’ COMMAND HISTORY shows exactly what ran, what grep found, what failed
β€’ FILE CONTENT has line numbers β€” use them for sed -i 'Ns/...' precision
β€’ If grep shows a line β†’ read it with chunk before editing
β€’ If sed exits 0 but linesChanged=no-match β†’ the pattern was wrong, search again
β€’ If the same sed pattern failed twice β†’ switch to full_rewrite immediately
β€’ NEVER guess at content you haven't read β€” always verify first

=== STRICT RULES ===
1. ALWAYS map or search before editing β€” NEVER edit blind
2. ALWAYS chunk-read the exact text BEFORE editing it
3. ALWAYS cp file.bak before full_rewrite
4. ALWAYS node --check after every JS change
5. NEVER use tee β€” it breaks on backticks, $vars, unicode. Use full_rewrite.
6. NEVER echo -e for multiline code
7. Mark done:true ONLY after a chunk/grep CONFIRMS the change is in the file
8. If sed linesChanged=no-match β†’ search again; pattern was wrong
9. NEVER repeat a command already in COMMAND HISTORY with the same args
10. If sed failed twice on same pattern β†’ full_rewrite immediately
11. SELF-CHECK before done:true β€” "Does the file contain EXACTLY what was requested?"
12. CHANGE DETECTION: after every edit, grep for both old and new text to confirm swap
`;

// ── COMMAND DEDUP & SAFETY FILTER ─────────────────────────────────────────
// Strips commands already run this session (exact match) and known-dangerous patterns.
// Returns { safe: [...], skipped: [...] }
const DESTRUCTIVE_PATTERNS = [
  /^rm\s+-rf?\s+\//, // rm -rf /
  /^>\s*\//, // redirect to root path
  /:(){ :|:& };:/, // fork bomb
];
function filterCommands(commands, alreadyRun = []) {
  const safe = [];
  const skipped = [];
  const runSet = new Set(alreadyRun.map(c => (typeof c === "string" ? c.trim() : "")));
  // Normalise input: flatten, unwrap {cmd} objects, drop anything not a string
  const flat = (Array.isArray(commands) ? commands : [])
    .flat(3)
    .map(c => (c != null && typeof c === "object" && typeof c.cmd === "string") ? c.cmd : c)
    .filter(c => c != null && typeof c === "string");
  for (const cmd of flat) {
    const trimmed = cmd.trim();
    if (!trimmed) continue;
    if (runSet.has(trimmed)) { skipped.push({ cmd: trimmed, reason: "duplicate" }); continue; }
    if (!isSafeCmd(trimmed)) { skipped.push({ cmd: trimmed, reason: "blocked" }); continue; }
    if (DESTRUCTIVE_PATTERNS.some(p => p.test(trimmed))) { skipped.push({ cmd: trimmed, reason: "destructive" }); continue; }
    safe.push(trimmed);
  }
  return { safe, skipped };
}

// ── STRUCTURED COMMAND RESULT ──────────────────────────────────────────────
// Wraps raw stdout/stderr into a structured object the AI can reason about
function structureResult(cmd, stdout, stderr, code) {
  const lines = stdout.split("\n").filter(Boolean);
  const isEdit = /^sed\s+-i/.test(cmd); // tee is blocked; only sed -i counts as edit
  const isGrep = /^grep/.test(cmd);
  const isRead = /^sed\s+-n/.test(cmd) || /^(head|tail|cat)\b/.test(cmd);
  const isCheck = cmd.includes("node --check") || cmd.includes("python3 -m py_compile");
  return {
    cmd,
    exit: code,
    ok: code === 0,
    stdout: stdout.substring(0, 1200),
    stderr: stderr.substring(0, 400),
    // Structured fields for common operations
    ...(isGrep && { matches: lines.slice(0, 30) }),
    ...(isEdit && { linesChanged: code === 0 ? "applied" : "no-match" }),
    ...(isRead && { content: stdout.substring(0, 2000) }),
    ...(isCheck && { syntaxOk: code === 0, syntaxError: stderr || null }),
    warning: code !== 0 ? (stderr || "non-zero exit") : null,
  };
}

// ── Multer ─────────────────────────────────────────────────────────────────
const storage = multer.diskStorage({
  destination: HOME_DIR,
  filename: (req, file, cb) => cb(null, file.originalname || "index.js"),
});
const upload = multer({ storage });

// ── ROBUST JSON PARSER ─────────────────────────────────────────────────────
// Handles: plain text, markdown fenced, partial JSON, escaped newlines
function parseJSON(raw) {
  if (!raw) return null;
  let s = String(raw).trim();

  // Strip markdown fences
  s = s.replace(/^```(?:json)?\s*/i, "").replace(/\s*```\s*$/, "").trim();

  // Try direct parse first
  try { return JSON.parse(s); } catch {}

  // Extract outermost {...} object
  const start = s.indexOf("{");
  const end = s.lastIndexOf("}");
  if (start !== -1 && end !== -1 && end > start) {
    try { return JSON.parse(s.slice(start, end + 1)); } catch {}
  }

  // Try to fix common issues: unescaped newlines in string values
  try {
    const fixed = s
      .replace(/:\s*"([\s\S]*?)"/g, (m, v) => ': "' + v.replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t") + '"')
      .replace(/,\s*}/g, "}").replace(/,\s*\]/g, "]");
    const fStart = fixed.indexOf("{"), fEnd = fixed.lastIndexOf("}");
    if (fStart !== -1 && fEnd !== -1) return JSON.parse(fixed.slice(fStart, fEnd + 1));
  } catch {}

  return null;
}

// ── SANITIZE AI RESPONSE ───────────────────────────────────────────────────
// Ensures "commands" is always a flat array of non-empty strings.
// The AI occasionally returns nulls, objects, nested arrays, or step-label strings.
function sanitizeAIResponse(parsed) {
  if (!parsed) return parsed;
  if (parsed.commands !== undefined) {
    const raw = Array.isArray(parsed.commands) ? parsed.commands : [];
    parsed.commands = raw
      .flat(5)
      .map(c => {
        if (c == null) return null;
        if (typeof c === "string") return c.trim() || null;
        // Unwrap {cmd: "..."} or {command: "..."} objects
        if (typeof c === "object") {
          const v = c.cmd || c.command || c.shell || c.run || c.exec;
          return typeof v === "string" ? v.trim() || null : null;
        }
        return null;
      })
      .filter(c => c != null && c.length > 0 && isSafeCmd(c));
  }
  return parsed;
}


// When AI returns markdown/prose instead of JSON, pull shell commands from it
function extractCommandsFromText(text) {
  const cmds = [];
  // Match lines starting with $ or lines inside code blocks
  const codeBlockRe = /```(?:bash|sh|shell)?\s*([\s\S]*?)```/gi;
  let m;
  while ((m = codeBlockRe.exec(text)) !== null) {
    const lines = m[1].split("\n").map(l => l.replace(/^\$\s*/, "").trim()).filter(l => l && !l.startsWith("#"));
    cmds.push(...lines);
  }
  // Also match lines starting with $ outside code blocks
  const lines = text.split("\n");
  for (const line of lines) {
    const stripped = line.replace(/^\$\s*/, "").trim();
    const firstWord = stripped.split(/\s+/)[0];
    if (line.trim().startsWith("$") && firstWord) {
      if (!cmds.includes(stripped)) cmds.push(stripped);
    }
  }
  return cmds.filter(cmd => isSafeCmd(cmd));
}

// ── EXTRACT CONTENT FROM TEXT ──────────────────────────────────────────────
// Pull file content from markdown code block in plain-text AI response
function extractContentFromText(text) {
  // Match ```js ... ``` or ```javascript ... ``` or ``` ... ```
  const m = text.match(/```(?:js|javascript|typescript|ts|python|py|sh|bash)?\s*([\s\S]+?)```/i);
  if (m) return m[1].trim();
  return null;
}

// ── runCmd ─────────────────────────────────────────────────────────────────
function runCmd(command) {
  return new Promise(resolve => {
    if (!isSafeCmd(command)) {
      return resolve({ stdout: "", stderr: `Blocked: ${command.trim().split(/\s+/)[0]}`, code: 1 });
    }
    exec(command, { cwd: HOME_DIR, timeout: 30000, shell: "/bin/bash" }, (err, stdout, stderr) => {
      const code = err ? (err.code || 1) : 0;
      resolve({ stdout: stdout || "", stderr: stderr || "", code });
    });
  });
}

// ── SMART CONTEXT ──────────────────────────────────────────────────────────
function getSmartContext(content, query, maxLines = 120) {
  const lines = content.split("\n");
  if (lines.length <= maxLines) return content;
  const head = lines.slice(0, 40).join("\n");
  const tail = lines.slice(-15).join("\n");
  const words = query.toLowerCase().split(/\s+/).filter(w => w.length > 3);
  let rs = -1, re = -1;
  for (let i = 0; i < lines.length; i++) {
    const lower = lines[i].toLowerCase();
    if (words.some(w => lower.includes(w))) {
      if (rs === -1) rs = Math.max(0, i - 5);
      re = Math.min(lines.length - 1, i + 35);
    }
  }
  const middle = rs !== -1
    ? `\n// ...[${rs}-${re} relevant]...\n` + lines.slice(rs, re + 1).join("\n")
    : `\n// ...[${lines.length - 55} lines omitted]...\n`;
  return head + middle + tail;
}

// ── FILE INSPECTION β€” Claude-style chunked reading ─────────────────────────
// Mirrors exactly how Claude reads files: overview first, then targeted chunks.
// The AI requests inspections; the system resolves them and feeds results back.

// 1. Build a file map: line count, imports, exports, top-level symbols, section headers.
//    This is the "wc -l + grep structure" step Claude does first.
function buildFileMap(filePath) {
  const abs = path.join(HOME_DIR, filePath);
  if (!fs.existsSync(abs)) return null;
  const content = fs.readFileSync(abs, "utf8");
  const lines = content.split("\n");
  const ext = path.extname(filePath).toLowerCase();

  // Find structural landmarks by line number
  const landmarks = [];
  const importLines = [], exportLines = [], fnLines = [], classLines = [], commentSections = [];

  lines.forEach((line, i) => {
    const n = i + 1;
    const t = line.trim();
    if (/^(import|require|from)\b/.test(t) || /\brequire\s*\(/.test(t)) importLines.push(n);
    if (/^(export|module\.exports)/.test(t)) exportLines.push(n);
    if (/^(async\s+)?function\s+\w+|const\s+\w+\s*=\s*(async\s+)?\(/.test(t) ||
        /^(app|router)\.(get|post|put|delete|patch|use)\s*\(/.test(t)) fnLines.push({ n, preview: t.slice(0, 80) });
    if (/^class\s+\w+/.test(t)) classLines.push({ n, preview: t.slice(0, 60) });
    if (/^\/\/\s*[─━=]{3,}/.test(t) || /^#{1,3}\s/.test(t)) commentSections.push({ n, text: t.slice(0, 60) });
  });

  return {
    file: filePath,
    totalLines: lines.length,
    ext,
    imports: importLines.slice(0, 5),           // first 5 import lines
    exports: exportLines.slice(0, 5),
    functions: fnLines.slice(0, 30),             // up to 30 function signatures
    classes: classLines.slice(0, 10),
    sections: commentSections.slice(0, 20),      // section comment headers
    head: lines.slice(0, 30).map((l, i) => `${i+1}\t${l}`).join("\n"),
    tail: lines.slice(-15).map((l, i) => `${lines.length-15+i+1}\t${l}`).join("\n"),
  };
}

// 2. Read a specific chunk of a file by line range β€” like sed -n 'X,Yp'
//    Returns line-numbered content so the AI can use exact line numbers in sed.
function readFileChunk(filePath, startLine, endLine) {
  const abs = path.join(HOME_DIR, filePath);
  if (!fs.existsSync(abs)) return null;
  const lines = fs.readFileSync(abs, "utf8").split("\n");
  const s = Math.max(1, startLine) - 1;
  const e = Math.min(lines.length, endLine);
  return {
    file: filePath,
    startLine: s + 1,
    endLine: e,
    totalLines: lines.length,
    content: lines.slice(s, e).map((l, i) => `${s + i + 1}\t${l}`).join("\n"),
  };
}

// 3. Search file for a pattern β€” like grep -n, returns structured matches
function searchFile(filePath, pattern) {
  const abs = path.join(HOME_DIR, filePath);
  if (!fs.existsSync(abs)) return null;
  const lines = fs.readFileSync(abs, "utf8").split("\n");
  let re;
  try { re = new RegExp(pattern, "i"); } catch { re = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i"); }
  const matches = [];
  lines.forEach((line, i) => {
    if (re.test(line)) matches.push({ line: i + 1, text: line.trimEnd().slice(0, 120) });
  });
  return { file: filePath, pattern, totalLines: lines.length, matches: matches.slice(0, 40) };
}

// 4. Process an array of read_file actions the AI requested:
//    [{ action: "map" }, { action: "chunk", start, end }, { action: "search", pattern }]
function processReadActions(filePath, actions = []) {
  const results = [];
  for (const a of actions) {
    if (a.action === "map") {
      results.push({ action: "map", result: buildFileMap(filePath) });
    } else if (a.action === "chunk" && a.start && a.end) {
      results.push({ action: "chunk", start: a.start, end: a.end, result: readFileChunk(filePath, a.start, a.end) });
    } else if (a.action === "search" && a.pattern) {
      results.push({ action: "search", pattern: a.pattern, result: searchFile(filePath, a.pattern) });
    }
  }
  return results;
}

// 5. Format read results into a concise string the AI can parse in its next prompt
function formatReadResults(readResults) {
  return readResults.map(r => {
    if (r.action === "map" && r.result) {
      const m = r.result;
      const fns = m.functions.map(f => `  L${f.n}: ${f.preview}`).join("\n");
      const secs = m.sections.map(s => `  L${s.n}: ${s.text}`).join("\n");
      return `=== FILE MAP: ${m.file} (${m.totalLines} lines) ===
IMPORTS at lines: ${m.imports.join(", ") || "none"}
SECTIONS:\n${secs || "  (none)"}
FUNCTIONS/ROUTES:\n${fns || "  (none)"}
HEAD (lines 1-30):\n${m.head}
TAIL (last 15):\n${m.tail}`;
    }
    if (r.action === "chunk" && r.result) {
      return `=== CHUNK ${r.result.file} lines ${r.result.startLine}-${r.result.endLine} (of ${r.result.totalLines}) ===\n${r.result.content}`;
    }
    if (r.action === "search" && r.result) {
      const hits = r.result.matches.map(m => `  L${m.line}: ${m.text}`).join("\n");
      return `=== SEARCH "${r.result.pattern}" in ${r.result.file} β€” ${r.result.matches.length} hits ===\n${hits || "  (no matches)"}`;
    }
    return "";
  }).filter(Boolean).join("\n\n");
}

// ── VERSIONING ─────────────────────────────────────────────────────────────
function readVersions() {
  if (!fs.existsSync(VERSIONS_FILE)) return {};
  try { return JSON.parse(fs.readFileSync(VERSIONS_FILE, "utf8")); } catch { return {}; }
}
function saveVersions(v) {
  fs.writeFileSync(VERSIONS_FILE, JSON.stringify(v, null, 2), "utf8");
}
function versionFile(filePath) {
  const src = path.join(HOME_DIR, filePath);
  if (!fs.existsSync(src)) return;
  const ts = Date.now();
  const ext = path.extname(filePath);
  const base = filePath.replace(ext, "");
  const vName = `.versions/${base}_v${ts}${ext}`;
  const vDir = path.join(HOME_DIR, ".versions");
  if (!fs.existsSync(vDir)) fs.mkdirSync(vDir, { recursive: true });
  fs.copyFileSync(src, path.join(HOME_DIR, vName));
  const versions = readVersions();
  if (!versions[filePath]) versions[filePath] = [];
  versions[filePath].push({ path: vName, ts, label: new Date(ts).toISOString() });
  if (versions[filePath].length > 10) versions[filePath] = versions[filePath].slice(-10);
  saveVersions(versions);
  return vName;
}

// ── TASK MANAGEMENT ────────────────────────────────────────────────────────
function readTasks() {
  if (!fs.existsSync(TASK_FILE)) return [];
  try { return JSON.parse(fs.readFileSync(TASK_FILE, "utf8")); } catch { return []; }
}
function saveTasks(tasks) {
  fs.writeFileSync(TASK_FILE, JSON.stringify(tasks, null, 2), "utf8");
}
function updateTasks(newTasks) {
  const existing = readTasks();
  const merged = [...existing, ...newTasks.filter(t => !existing.find(e => e.task === t.task))];
  saveTasks(merged);
  return merged;
}

// ── COMMAND HISTORY ────────────────────────────────────────────────────────
function appendCmdHistory(entries) {
  let hist = [];
  if (fs.existsSync(CMD_HISTORY_FILE)) {
    try { hist = JSON.parse(fs.readFileSync(CMD_HISTORY_FILE, "utf8")); } catch {}
  }
  hist.push(...entries);
  if (hist.length > 200) hist = hist.slice(-200);
  fs.writeFileSync(CMD_HISTORY_FILE, JSON.stringify(hist, null, 2), "utf8");
}

// ── PROJECT METADATA ───────────────────────────────────────────────────────
function readMeta() {
  if (!fs.existsSync(META_FILE)) return { created_at: new Date().toISOString(), languages: [], last_ai_edit: null };
  try { return JSON.parse(fs.readFileSync(META_FILE, "utf8")); } catch { return {}; }
}
function updateMeta(patch) {
  const meta = { ...readMeta(), ...patch, updated_at: new Date().toISOString() };
  fs.writeFileSync(META_FILE, JSON.stringify(meta, null, 2), "utf8");
  return meta;
}

// ── WEB SEARCH DETECTION ───────────────────────────────────────────────────
const SEARCH_TRIGGERS = [
  /\b(latest|recent|current|today|now|2024|2025|2026|news|trending|new release|just released)\b/i,
  /\b(who is|what is the price|when did|how much does|is .* still|does .* exist)\b/i,
  /\b(search|look up|find out|google|web|online|internet)\b/i,
  /\b(weather|stock|score|result|update|announce|launch)\b/i,
];
function needsWebSearch(msg) {
  return SEARCH_TRIGGERS.some(r => r.test(msg));
}
async function webSearch(query) {
  const short = query.slice(0, 200);
  const url = `https://apis.prexzyvilla.site/ai/gpt-5?text=${encodeURIComponent(short)}`;
  const { data } = await axios.get(url, { timeout: 20000 });
  return { answer: data.text || "", citations: data.citations || [], model: data.model || "gpt-5" };
}

// ── callAI β€” LongCat API (Anthropic format) ────────────────────────────────
// system   = role/task definition + original user request for context anchoring
// query    = specific data for this call (file snippet, cmd results, etc.)
// _modelKey accepted but ignored β€” all calls route to LongCat-Flash-Lite
// history  = [{role:"user"|"assistant", content:"..."}] β€” conversation transcript
//
// On HTTP 429 / rate_limit_exceeded the call is transparently retried once
// using the backup API key β€” the caller never sees the error.
async function callAI(system, query, _modelKey, history = []) {
  const userMessage = query ? `${system}\n\n${query}` : system;

  // Sanitize history β€” Anthropic expects [{role, content}], trim to last 10 turns
  const safeHistory = (Array.isArray(history) ? history : [])
    .filter(h => h && (h.role === "user" || h.role === "assistant") && h.content)
    .slice(-20) // last 10 user+assistant pairs
    .map(h => ({ role: h.role, content: String(h.content).slice(0, 800) }));

  const messages = [
    ...safeHistory,
    { role: "user", content: userMessage.slice(0, 5500) },
  ];

  const payload = {
    model: "LongCat-Flash-Lite",
    max_tokens: 2000,
    system: `You are Cryo, a precise developer AI. Always respond with a single valid JSON object β€” no markdown fences, no prose outside the JSON.

IRON LAW: Change ONLY what the user's request explicitly asks for. Never add, fix, reformat, or improve anything else. Interpret requests literally.

WRITE RULES: When a field like "content" or "new_content" holds file source code, write the raw source code as the string value (with \\n for newlines), NOT another JSON object.

BANNED: Never suggest tee or echo -e for writing code. They cannot handle backticks, $vars, or unicode in JS. Use full_rewrite only.

The original user request is embedded in the message β€” never lose sight of it. Self-check before responding: "Am I only changing what was requested?"`,
    messages,
  };

  // Inner helper β€” attempt one call with the given key
  async function attempt(apiKey) {
    const { data } = await axios.post(LONGCAT_API_URL, payload, {
      timeout: 60000,
      headers: {
        "Content-Type": "application/json",
        "Authorization": `Bearer ${apiKey}`,
        "anthropic-version": "2023-06-01",
      },
    });
    // Anthropic format: data.content is an array of blocks
    if (data.content && Array.isArray(data.content)) {
      const textBlock = data.content.find(b => b.type === "text");
      return textBlock ? textBlock.text : "";
    }
    return data.reply || data.text || data.message || "";
  }

  try {
    return await attempt(LONGCAT_API_KEY);
  } catch (err) {
    // Detect quota exhaustion: HTTP 429 with rate_limit_exceeded code
    const status = err.response?.status;
    const errCode = err.response?.data?.error?.code;
    if (status === 429 && errCode === "rate_limit_exceeded") {
      // Silently retry with backup key β€” no error propagated to caller
      return await attempt(LONGCAT_API_KEY_BACKUP);
    }
    throw err; // any other error re-thrown normally
  }
}

// ── SYNTAX FIX LOOP ────────────────────────────────────────────────────────
async function syntaxFixLoop(filePath, send, modelKey, maxAttempts = 3, transcript = []) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const check = await runCmd(`node --check ${filePath}`);
    const idx = 800 + attempt;
    send("command", { cmd: `node --check ${filePath} (attempt ${attempt})`, index: idx });
    send("command_result", {
      cmd: `node --check ${filePath}`, stdout: check.code === 0 ? "Syntax OK" : "",
      stderr: check.stderr, index: idx, isError: check.code !== 0
    });
    if (check.code === 0) {
      send("status", { text: attempt === 1 ? "Syntax OK" : `Syntax fixed on attempt ${attempt}` });
      return true;
    }

    // Detect the "AI wrote JSON as file content" mistake β€” the error will mention
    // an unexpected token like ':' or '"' at line 1-3. No point trying to fix it β€”
    // the whole file content is wrong, not just a syntax typo.
    const looksLikeJsonBlob = /Unexpected token [':"{[\]]/i.test(check.stderr) &&
      (() => { try { const t = fs.readFileSync(path.join(HOME_DIR, filePath), "utf8").trim(); return t.startsWith("{") || t.startsWith("["); } catch { return false; } })();
    if (looksLikeJsonBlob) {
      send("status", { text: "Content is JSON not source code β€” skipping fix loop" });
      return false;
    }

    send("status", { text: `Syntax error (attempt ${attempt}/${maxAttempts}) β€” fixing...` });
    const absPath = path.join(HOME_DIR, filePath);
    const broken = fs.readFileSync(absPath, "utf8");
    const fixedRaw = await callAI(
      `Fix this JavaScript syntax error. Return ONLY a JSON object with no extra text:\n{"content":"<complete fixed file here>","fix":"<one line description of fix>"}`,
      `SYNTAX ERROR:\n${check.stderr}\n\nFILE CONTENT:\n${broken.slice(0, 3000)}`,
      modelKey,
      transcript
    );
    const fixed = parseJSON(fixedRaw);
    if (fixed?.content) {
      fs.writeFileSync(absPath, fixed.content, "utf8");
      send("status", { text: `Fix applied: ${fixed.fix || "syntax correction"}` });
    } else {
      // Try extracting content from markdown if JSON parse failed
      const extracted = extractContentFromText(fixedRaw || "");
      if (extracted) {
        fs.writeFileSync(absPath, extracted, "utf8");
        send("status", { text: "Extracted fix from response" });
      } else break;
    }
  }
  return false;
}


// ── VERIFY TASK COMPLETION ────────────────────────────────────────────────────
async function verifyTaskCompletion(filePath, tasks, request, modelKey, transcript = []) {
  const abs = path.join(HOME_DIR, filePath);
  if (!fs.existsSync(abs)) return { tasks, allDone: false, missing: [] };
  const content = fs.readFileSync(abs, "utf8");
  const raw = await callAI(
  `Check if the user's request is fully implemented in the file content.
Return ONLY raw JSON:
{"tasks":[{"task":"...","done":true,"note":"where it is or why missing"}],"allDone":true,"missing":["still needed"]}`,
  `REQUEST: "${request}"\nFILE ${filePath}:\n${content.substring(0,3000)}\nTASKS:\n${tasks.map((t,i) => (i+1) + ". " + t.task).join("\n")}`,
  modelKey,
  transcript
);
  return parseJSON(raw) || { tasks, allDone: false, missing: [] };
}

// ── ITERATIVE COMMAND LOOP ─────────────────────────────────────────────────
// Architecture mirrors Claude Code's actual agent loop:
//   1. Loop exits when the model produces output with NO tool calls (stop_reason != "tool_use")
//      β€” not when the model self-reports done:true (that's unreliable).
//   2. max_turns counts tool-use turns only, matching Claude Code's max_turns semantics.
//   3. TODO state is injected as a reminder after every tool call, matching Claude Code's
//      behaviour of keeping the task list visible at every step.
//   4. Context compaction fires when accumulated command log exceeds ~92% of a threshold,
//      summarising older entries so the loop can continue beyond context limits.
async function runAgentLoop(opts) {
  const { filePath, fileContent: initialContent, message, modelKey, send, maxRounds = 10, transcript = [] } = opts;
  // max_turns counts tool-use turns only (matches Claude Code semantics)
  const MAX_TOOL_TURNS = maxRounds;
  const absPath = path.join(HOME_DIR, filePath);
  const allCommandResults = [];
  const historyEntries = [];
  let cmdIndex = 0;
  let fileContent = initialContent;
  let sessionTasks = [];
  let pendingReadResults = []; // read_file results waiting to be shown to AI in next round
  let toolTurnCount = 0; // counts tool-use turns only β€” matches Claude Code max_turns semantics
  // Context compaction: when command log grows beyond this entry count, summarise older
  // entries so the loop can continue past context limits (mirrors Claude Code's ~92% compaction)
  const COMPACTION_THRESHOLD = 40;

  // ── Helper: run a batch of commands and collect results ───────────────
  // Deduplicates against already-run commands, structures output, guards retries
  const cmdRetryCount = {}; // tracks per-cmd-pattern retry attempts
  async function runBatch(commands) {
    // Normalise before anything else β€” AI sometimes returns nulls, objects, nested arrays
    const normalised = (Array.isArray(commands) ? commands : [])
      .flat(3)
      .map(c => (c != null && typeof c === "object" && typeof c.cmd === "string") ? c.cmd : c)
      .filter(c => c != null && typeof c === "string" && c.trim().length > 0);
    const alreadyRunCmds = allCommandResults.map(r => r.cmd);
    const { safe, skipped } = filterCommands(normalised, alreadyRunCmds);

    // Notify about skipped commands so AI sees them in history
    for (const s of skipped) {
      const reason = s.cmd.trim().startsWith("tee ")
        ? "BANNED (tee cannot write JS source β€” use full_rewrite)"
        : `Skipped (${s.reason})`;
      send("status", { text: `${reason}: ${s.cmd.slice(0, 80)}` });
    }

    for (const cmd of safe) {
      // Per-command retry cap: same base command (first 40 chars) max 3 times
      const cmdKey = cmd.slice(0, 40);
      cmdRetryCount[cmdKey] = (cmdRetryCount[cmdKey] || 0) + 1;
      if (cmdRetryCount[cmdKey] > 3) {
        send("status", { text: `Max retries reached for: ${cmd.slice(0, 60)}` });
        continue;
      }

      // Count this as a tool-use turn (matches Claude Code max_turns semantics)
      toolTurnCount++;

      const idx = cmdIndex++;
      send("status", { text: `[${idx + 1}] ${cmd}` });
      send("command", { cmd, index: idx });
      const result = await runCmd(cmd);
      const structured = structureResult(cmd, result.stdout, result.stderr, result.code);
      allCommandResults.push({ cmd, ...result, structured });
      historyEntries.push({ cmd, stdout: result.stdout.slice(0, 500), stderr: result.stderr.slice(0, 200), code: result.code, ts: Date.now() });

      // Emit full structured result β€” frontend sees stdout, stderr, exit code
      send("command_result", {
        cmd,
        stdout: result.stdout,
        stderr: result.stderr,
        exit: result.code,
        ok: result.code === 0,
        structured,
        index: idx,
        isError: result.code !== 0 && !!result.stderr,
      });

      // ── TODO reminder after every tool use (mirrors Claude Code architecture) ──
      // Claude Code injects the current TODO state as a system reminder after each tool
      // call to prevent the model losing track of objectives in long conversations.
      const pendingAfterTool = sessionTasks.filter(t => !t.done);
      if (sessionTasks.length > 0) {
        send("task_reminder", {
          tasks: sessionTasks,
          pending: pendingAfterTool.length,
          text: pendingAfterTool.length > 0
            ? `πŸ“‹ TODO: ${pendingAfterTool.map(t => t.task).join(" | ")}`
            : "βœ… All tasks complete"
        });
      }

      // Auto-fix syntax errors in JS files
      if (cmd.includes("node --check") && result.code !== 0 && result.stderr) {
        const target = cmd.trim().split(/\s+/).pop().replace(/\s*\(.*\)$/, "");
        send("status", { text: `Syntax error in ${target} β€” auto-fixing...` });
        await syntaxFixLoop(target, send, modelKey, 3);
      }
      fileContent = fs.existsSync(absPath) ? fs.readFileSync(absPath, "utf8") : fileContent;

    }
  }

  // ── Build the thread context block β€” passed to EVERY AI call ──────────
  // This IS the agent's memory β€” conversation history serialised into prompt.
  // Following the sheet: include original request, full task state, every
  // command result (what was FOUND, not just exit code), and line-numbered
  // file context so the AI can write precise sed line-number commands.
  function buildThreadContext(round) {
    const fileLines = fileContent.split("\n").length;

    // Line-numbered snippet β€” the AI needs line numbers for sed -i 'Ns/...'
    const lines = fileContent.split("\n");
    const smartLines = getSmartContext(fileContent, message).split("\n");
    // Attach line numbers to the smart context section so AI can reference them
    const numberedCtx = (() => {
      // Find where our smartCtx starts inside the full file
      const headCount = Math.min(40, lines.length);
      const headBlock = lines.slice(0, headCount)
        .map((l, i) => `${i + 1}\t${l}`).join("\n");
      const tailStart = Math.max(headCount, lines.length - 15);
      const tailBlock = lines.slice(tailStart)
        .map((l, i) => `${tailStart + i + 1}\t${l}`).join("\n");
      // Middle: find relevant lines by query keywords
      const words = message.toLowerCase().split(/\s+/).filter(w => w.length > 3);
      let rs = -1, re = -1;
      for (let i = headCount; i < tailStart; i++) {
        if (words.some(w => lines[i].toLowerCase().includes(w))) {
          if (rs === -1) rs = Math.max(headCount, i - 4);
          re = Math.min(tailStart - 1, i + 20);
        }
      }
      const middleBlock = rs !== -1
        ? `\n...[lines ${headCount + 1}–${rs} omitted]...\n` +
          lines.slice(rs, re + 1).map((l, i) => `${rs + i + 1}\t${l}`).join("\n") +
          `\n...[lines ${re + 1}–${tailStart} omitted]...`
        : `\n...[lines ${headCount + 1}–${tailStart} omitted]...`;
      return (headBlock + middleBlock + "\n" + tailBlock).substring(0, 2200);
    })();

    // ── Context compaction (mirrors Claude Code's ~92% context window compaction) ──
    // When the command log grows large, summarise older entries to prevent context overflow.
    // Claude Code does this automatically; we replicate the behaviour here.
    let compactionNote = "";
    let cmdLogEntries = allCommandResults;
    if (allCommandResults.length > COMPACTION_THRESHOLD) {
      const kept = allCommandResults.slice(-14);
      const compacted = allCommandResults.slice(0, allCommandResults.length - 14);
      const editCount = compacted.filter(r => r.cmd.startsWith("sed -i") || r.cmd.startsWith("[full_rewrite]")).length;
      const failCount = compacted.filter(r => r.code !== 0).length;
      compactionNote = `[Context compacted: ${compacted.length} earlier entries β†’ ${editCount} edits, ${failCount} failures]\n`;
      cmdLogEntries = kept;
    }

    // Full command history with structured outputs β€” AI knows exactly what grep found,
    // what sed changed, what errors occurred, without guessing from raw text
    const cmdLog = cmdLogEntries.slice(-14).map((r, i) => {
      const s = r.structured || {};
      const exitLabel = r.code === 0 ? "βœ“" : `βœ—(${r.code})`;
      let detail = "";
      if (s.matches) detail = `\n     MATCHES: ${s.matches.slice(0,10).join(" | ")}`;
      else if (s.content) detail = `\n     CONTENT: ${s.content.slice(0, 300)}`;
      else if (r.stdout.trim()) detail = `\n     OUT: ${r.stdout.trim().slice(0, 300)}`;
      if (r.stderr.trim()) detail += `\n     ERR: ${r.stderr.trim().slice(0, 150)}`;
      if (s.linesChanged) detail += `\n     EDIT: ${s.linesChanged}`;
      if (s.syntaxError) detail += `\n     SYNTAX: ${s.syntaxError.slice(0, 120)}`;
      return `[${i + 1}] ${exitLabel} $ ${r.cmd}${detail}`;
    }).join("\n");

    // Highlight any commands that failed β€” the AI must not repeat them blindly
    const failedCmds = allCommandResults.filter(r => r.code !== 0);
    const skippedTee = allCommandResults.filter(r => r.cmd.startsWith("[BANNED]"));
    const failureNote = (failedCmds.length > 0 || skippedTee.length > 0)
      ? `\nFAILED/BANNED COMMANDS (do NOT repeat these patterns):\n` +
        failedCmds.slice(-4).map(r => `  βœ— ${r.cmd} β†’ ${r.stderr.trim().slice(0, 120)}`).join("\n") +
        (skippedTee.length > 0 ? `\n  β›” tee was blocked β€” use full_rewrite to write code` : "")
      : "";

    const taskStatus = sessionTasks.length > 0
      ? sessionTasks.map((t, i) => `  ${t.done ? "[x]" : "[ ]"} ${i + 1}. ${t.task}`).join("\n")
      : "  (none planned yet)";
    const pending = sessionTasks.filter(t => !t.done);

    return `=== CRYO AGENT β€” Round ${round}/${MAX_TOOL_TURNS} | Tool turns used: ${toolTurnCount}/${MAX_TOOL_TURNS} ===
ORIGINAL REQUEST (never lose this): "${message}"
FILE: ${filePath} | TOTAL LINES: ${fileLines}
${compactionNote}${failureNote}

TASK LIST:
${taskStatus}

PENDING (${pending.length} remaining):
${pending.length > 0 ? pending.map((t, i) => `  ${i + 1}. ${t.task}`).join("\n") : "  ALL TASKS DONE"}

COMMAND HISTORY (${allCommandResults.length} total β€” includes grep results, sed outputs, check results):
${cmdLog || "  (none yet β€” start with read_file map)"}
${pendingReadResults.length > 0 ? `
FILE READ RESULTS (from your read_file requests last round):
${formatReadResults(pendingReadResults)}
` : ""}
FILE CONTENT WITH LINE NUMBERS (use these line numbers in sed -i 'Ns/...' commands):
${numberedCtx}
=== END CONTEXT ===`;
  }

  // ── ROUND 1: Master plan ──────────────────────────────────────────────
  send("status", { text: "Analyzing request..." });

  const fileLines = initialContent.split("\n").length;

  // Build line-numbered initial context for Round 1 (same logic as buildThreadContext)
  const initLines = initialContent.split("\n");
  const initHeadBlock = initLines.slice(0, Math.min(40, initLines.length))
    .map((l, i) => `${i + 1}\t${l}`).join("\n");
  const initTailStart = Math.max(40, initLines.length - 15);
  const initTailBlock = initLines.slice(initTailStart)
    .map((l, i) => `${initTailStart + i + 1}\t${l}`).join("\n");
  const initCtx = (initHeadBlock + `\n...[lines 41–${initTailStart} omitted β€” use grep -n to find sections]...\n` + initTailBlock).substring(0, 2200);

  // ── Auto file map for Round 1 β€” AI gets full structure before planning ──
  // This is exactly what Claude does: understand structure before touching anything.
  const fileMap = buildFileMap(filePath);
  const fileMapStr = fileMap ? formatReadResults([{ action: "map", result: fileMap }]) : "";

  const planPrompt = `You are Cryo - a developer AI agent. You edit files using shell commands.

AGENT LOOP MODEL (exactly how Claude Code works):
  Phase 1 β€” GATHER CONTEXT: read the file, search for relevant sections, understand structure.
  Phase 2 β€” TAKE ACTION: make the changes requested. Use sed for small edits, full_rewrite for large ones.
  Phase 3 β€” VERIFY RESULTS: grep to confirm changes landed, node --check for syntax, re-read if needed.
  These phases BLEND and REPEAT. After verifying, you may need to gather more context for the next task.
  The loop exits when you produce a response with NO commands and NO read_file actions.
  DO NOT self-terminate with done:true unless you have already VERIFIED the change is in the file.

${COMMANDS_REFERENCE}

${fileMapStr ? `=== FILE STRUCTURE (auto-read before you start) ===\n${fileMapStr}\n` : ""}
FILE: ${filePath} | TOTAL LINES: ${fileLines}
FILE CONTENT HEAD+TAIL (with line numbers):
${initCtx}

USER REQUEST: "${message}"

╔══ IRON LAW β€” APPLY BEFORE EVERY DECISION ══╗
β•‘ CHANGE ONLY what the request explicitly asks.
β•‘ DO NOT add, fix, reformat, or "improve" anything else.
β•‘ Treat the request LITERALLY β€” no assumptions, no extras.
β•‘ WHAT NOT TO CHANGE: any code the request does not mention.
β•‘ SELF-CHECK before responding: "Am I touching anything unrequested?"
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•

WRITING CODE RULES:
β›” BANNED: tee, echo -e for multiline. These cannot handle JS source (backticks, $vars fail).
βœ… TO ADD MULTI-LINE CODE: use full_rewrite with complete new_content.
βœ… TO CHANGE 1-3 LINES: use sed -i 'Ns/exact-old/new/' after reading with chunk.

For your FIRST response, output read_file actions to read the sections you'll need,
OR output commands if you already have enough context from the file map above.

Respond with ONLY raw JSON:
{
  "task_type": "query" | "edit" | "create",
  "status": "brief status for the user",
  "tasks": [{"task": "specific subtask", "done": false}],
  "read_file": [{ "action": "chunk", "start": N, "end": M }, ...],
  "commands": ["cp ${filePath} ${filePath}.bak", "..."],
  "reasoning": "what sections you need to read and why, and what you will NOT change",
  "done": false
}

COMMANDS FORMAT RULES β€” VIOLATIONS WILL CRASH THE SYSTEM:
- "commands" MUST be a flat array of plain strings only
- NEVER put null, objects, or nested arrays inside "commands"
- NEVER omit "commands" β€” use [] if no commands needed this round
- NEVER include tee or echo -e for writing code
- Every entry must be a complete, executable bash command string`;

  const planRaw = await callAI(planPrompt, "", modelKey, transcript);
  let plan = sanitizeAIResponse(parseJSON(planRaw));

  if (!plan || (!plan.commands && !plan.read_file)) {
    const extracted = extractCommandsFromText(planRaw || "");
    plan = {
      task_type: "edit",
      status: "Running commands...",
      tasks: [{ task: message, done: false }],
      commands: extracted,
      reasoning: "Extracted from response",
      done: false
    };
  }

  sessionTasks = plan.tasks || [{ task: message, done: false }];
  send("status", { text: plan.status || "Processing..." });
  send("task_update", { tasks: sessionTasks });

  // Process any read_file actions from the plan (Round 1 reads)
  if (plan.read_file && Array.isArray(plan.read_file) && plan.read_file.length > 0) {
    send("status", { text: `Reading file structure (${plan.read_file.map(a => a.action).join(", ")})...` });
    const readResults = processReadActions(filePath, plan.read_file);
    pendingReadResults = readResults;
    for (const r of readResults) {
      const label = r.action === "map" ? "file map" : r.action === "chunk" ? `lines ${r.start}–${r.end}` : `search "${r.pattern}"`;
      send("command_result", { cmd: `[read_file:${r.action}] ${filePath} ${label}`, stdout: formatReadResults([r]), stderr: "", exit: 0, ok: true, isError: false, index: cmdIndex++ });
    }
  }

  await runBatch(plan.commands || []);

  // ── ROUNDS 2–N: Continue until model stops calling tools ─────────────────
  // Exit condition mirrors Claude Code exactly:
  //   "Turns continue until Claude produces output with no tool calls"
  // self-reported done:true is used as a HINT only β€” the real exit is no-tools output.
  // max_turns caps tool-use turns (not rounds), matching Claude Code's max_turns semantics.
  for (let round = 2; round <= MAX_TOOL_TURNS * 3; round++) { // outer round cap is generous; tool turns cap is enforced below
    // Enforce tool-turn budget (like Claude Code's max_turns / maxBudgetUsd)
    if (toolTurnCount >= MAX_TOOL_TURNS) {
      send("status", { text: `Tool turn budget reached (${toolTurnCount}/${MAX_TOOL_TURNS}) β€” stopping` });
      break;
    }
    // Re-read fresh file content
    fileContent = fs.existsSync(absPath) ? fs.readFileSync(absPath, "utf8") : fileContent;

    const threadCtx = buildThreadContext(round);
    send("status", { text: `Round ${round}: checking progress...` });

    const continuePrompt = `You are Cryo - a developer AI agent. You are mid-task. Study the full context below.

${threadCtx}

${COMMANDS_REFERENCE}

AGENT LOOP β€” HOW TO EXIT (matches Claude Code exactly):
  The loop stops when you return a response with NO commands AND NO read_file.
  That is the "no tool calls" exit β€” do not use done:true to stop unless you have zero remaining actions.
  If you still have work to do, include commands or read_file. The loop will continue.
  done:true is a HINT that verification passed β€” it does NOT stop the loop by itself.

THREE PHASES (blend and repeat as needed):
  1. GATHER CONTEXT β€” read_file map/search/chunk to understand the exact target
  2. TAKE ACTION β€” sed -i for 1-3 line edits, full_rewrite for large additions
  3. VERIFY β€” grep to confirm new text is present, node --check for syntax

β›” BANNED THIS SESSION: tee, echo -e for multiline. They CANNOT write JS source.
   Use full_rewrite to add multi-line code. Use sed -i for 1-3 line changes.

⚠ READ THE COMMAND HISTORY β€” it shows what grep found, what lines exist, what failed.
⚠ USE THE LINE NUMBERS in the file content to write precise sed -i 'Ns/...' commands.
⚠ NEVER repeat a command already in COMMAND HISTORY with the same arguments.

DECISION TREE THIS ROUND:
- If you haven't read the target section β†’ read_file chunk/search first
- If grep found lines β†’ sed -n 'START,ENDp' to read exact text before editing
- If you have exact text β†’ sed -i 'Ns/old/new/' for 1-3 lines, OR full_rewrite for multi-line
- If you just edited β†’ grep -n to prove the NEW text is in the file (change detection)
- If JS file was written β†’ node --check to verify syntax
- If sed pattern failed twice β†’ switch to full_rewrite immediately
- If all tasks verified in file β†’ set done:true

CHANGE DETECTION (mandatory before done:true):
  After EVERY edit, run grep to confirm:
  1. The OLD text is gone: grep -c "old pattern" file β†’ should be 0
  2. The NEW text exists: grep -n "new pattern" file β†’ should show your line

Respond with ONLY raw JSON (choose A or B):

A) Normal commands:
{
  "done": true | false,
  "status": "what you are doing",
  "tasks": [{"task": "...", "done": true|false}],
  "commands": ["grep -n ...", "sed -n ...", "sed -i ...", "node --check ..."],
  "reasoning": "what you found, what you changed, what you VERIFIED is in the file"
}

B) Full file rewrite (for adding code blocks or when sed fails):
{
  "done": true,
  "full_rewrite": true,
  "new_content": "COMPLETE new file content here",
  "status": "Rewrote file to add X",
  "tasks": [{"task": "...", "done": true}],
  "reasoning": "why full_rewrite was needed; confirm request was implemented"
}

COMMANDS FORMAT RULES β€” VIOLATIONS WILL CRASH THE SYSTEM:
- "commands" MUST be a flat array of plain strings. Example: ["sed -i '7s/get/head/' bot.js"]
- NEVER put objects, null, arrays-within-arrays, or non-strings in "commands"
- NEVER omit "commands" β€” use [] if you have no commands to run
- NEVER include tee β€” it is blocked and will be skipped
- EVERY command must be a complete shell command runnable in bash
- NEVER include comments, explanations, or step labels inside the commands array

CRITICAL: done:true only when grep CONFIRMS the change is physically in the file,
OR full_rewrite:true is set and the complete file was provided in new_content.
NEVER set done:true based only on the fact that sed ran β€” sed exits 0 even on no-match.`;

    const contRaw = await callAI(continuePrompt, "", modelKey, transcript);
    let cont = sanitizeAIResponse(parseJSON(contRaw));

    if (!cont) {
      const extracted = extractCommandsFromText(contRaw || "");
      if (extracted.length === 0) break;
      cont = { done: false, commands: extracted, tasks: sessionTasks, status: "Continuing...", reasoning: "Extracted" };
    }

    // ── READ_FILE: AI requested structured file inspection ─────────────
    // Process immediately and store results for the NEXT round's context.
    // This is the "Claude reads file in chunks" pattern β€” map β†’ search β†’ chunk β†’ edit.
    if (cont.read_file && Array.isArray(cont.read_file) && cont.read_file.length > 0) {
      send("status", { text: `Reading file (${cont.read_file.map(a => a.action).join(", ")})...` });
      const readResults = processReadActions(filePath, cont.read_file);
      pendingReadResults = readResults; // will appear in next round's context

      // Also emit each result to the frontend so user can see what was read
      for (const r of readResults) {
        const label = r.action === "map" ? "file map"
          : r.action === "chunk" ? `lines ${r.start}–${r.end}`
          : `search "${r.pattern}"`;
        send("command_result", {
          cmd: `[read_file:${r.action}] ${filePath} ${label}`,
          stdout: formatReadResults([r]),
          stderr: "",
          exit: 0,
          ok: true,
          isError: false,
          index: cmdIndex++,
        });
      }

      // Update task list if AI provided one
      if (cont.tasks && cont.tasks.length > 0) {
        sessionTasks = cont.tasks;
        send("task_update", { tasks: sessionTasks });
      }
      send("status", { text: cont.status || `Read complete β€” round ${round + 1} will use results` });
      // Also run any commands the AI included alongside read_file
      if (cont.commands && cont.commands.length > 0) await runBatch(cont.commands);
      continue; // go to next round with read results in context
    }

    // Clear pending reads once the AI has seen them (they were in this round's context)
    pendingReadResults = [];
    if (cont.full_rewrite && cont.new_content) {
      send("status", { text: "Staging full rewrite..." });
      const stageName = `__cryo_stage_${Date.now()}${path.extname(filePath)}`;
      const stagePath = path.join(HOME_DIR, stageName);
      fs.writeFileSync(stagePath, cont.new_content, "utf8");

      // Validate staged file before overwriting
      let stageOk = true;
      if (filePath.endsWith(".js")) {
        const chk = await runCmd(`node --check ${stageName}`);
        send("command_result", { cmd: `node --check ${stageName}`, stdout: chk.stdout, stderr: chk.stderr, exit: chk.code, ok: chk.code === 0, isError: chk.code !== 0, index: cmdIndex++ });
        if (chk.code !== 0) {
          send("status", { text: "Staged rewrite has syntax errors β€” fixing..." });
          await syntaxFixLoop(stageName, send, modelKey, 3, transcript);
          stageOk = (await runCmd(`node --check ${stageName}`)).code === 0;
        }
      }

      if (stageOk) {
        versionFile(filePath); // backup current before overwrite
        fs.copyFileSync(stagePath, absPath);
        fs.unlinkSync(stagePath);
        fileContent = fs.readFileSync(absPath, "utf8");
        send("command_result", { cmd: `[full_rewrite] ${filePath}`, stdout: "βœ“ File replaced atomically", stderr: "", exit: 0, ok: true, isError: false, index: cmdIndex++ });
        allCommandResults.push({ cmd: `[full_rewrite] ${filePath}`, stdout: "replaced", stderr: "", code: 0 });
        send("status", { text: "Full rewrite applied βœ“" });
        if (cont.tasks && cont.tasks.length > 0) { sessionTasks = cont.tasks; send("task_update", { tasks: sessionTasks }); }
        if (cont.done) break;
        continue;
      } else {
        fs.unlinkSync(stagePath);
        send("status", { text: "Staged rewrite failed validation β€” continuing with commands" });
      }
    }

    if (!cont) {
      const extracted = extractCommandsFromText(contRaw || "");
      if (extracted.length === 0) break;
      cont = { done: false, commands: extracted, tasks: sessionTasks, status: "Continuing...", reasoning: "Extracted" };
    }

    // Update task list from AI's assessment
    if (cont.tasks && cont.tasks.length > 0) {
      sessionTasks = cont.tasks;
      send("task_update", { tasks: sessionTasks });
    }

    if (cont.done || !cont.commands || cont.commands.length === 0) {
      // ── This is the Claude Code stop_reason != "tool_use" exit point ──
      // If the model returned no commands AND no read_file actions, it has
      // decided there is nothing more to call β€” this IS the loop's natural exit.
      const hasNoToolCalls = (!cont.commands || cont.commands.length === 0) &&
                             (!cont.read_file || cont.read_file.length === 0);
      if (hasNoToolCalls) {
        // Model stopped calling tools β€” loop exits (mirrors Claude Code exactly)
        send("status", { text: cont.status || "Model finished β€” no more tool calls" });
        if (cont.tasks && cont.tasks.length > 0) { sessionTasks = cont.tasks; send("task_update", { tasks: sessionTasks }); }
        break;
      }

      // done:true with tasks still marked pending β€” used as hint only, verify below
      const stillPending = sessionTasks.filter(t => !t.done);

      // ── MANDATORY GROUND-TRUTH CHECK ─────────────────────────────────
      // Never trust the AI's self-reported done:true without verifying
      // the actual file on disk. The AI cannot see its own sed output β€”
      // it only knows what it sent, not what actually landed.
      if (cont.done && stillPending.length === 0) {
        fileContent = fs.existsSync(absPath) ? fs.readFileSync(absPath, "utf8") : fileContent;
        const checkRaw = await callAI(
          `Check if the user's request is fully present in the file content.
Return ONLY JSON: {"verified": true | false, "reason": "what is present or missing"}`,
          `REQUEST: "${message}"\nFILE CONTENT:\n${fileContent.substring(0, 3000)}`,
          modelKey,
          transcript
        );
        const check = parseJSON(checkRaw);
        if (check?.verified === false) {
          // AI claimed done but the change isn't in the file β€” keep going
          send("status", { text: `Verification failed: ${check.reason} β€” retrying...` });
          // Force one more round
          sessionTasks = sessionTasks.map(t => ({ ...t, done: false }));
          send("task_update", { tasks: sessionTasks });
          if (round < maxRounds) continue;
        }
        send("status", { text: cont.status || "All tasks complete βœ“" });
        break;
      }

      if (stillPending.length === 0 || cont.done) {
        send("status", { text: cont.status || "All tasks complete βœ“" });
        break;
      }
      // AI said done but tasks aren't β€” push one more round
      if (round >= maxRounds - 1) break;
      send("status", { text: `Verifying ${stillPending.length} pending task(s)...` });
      continue;
    }

    send("status", { text: cont.status || `Round ${round}...` });
    await runBatch(cont.commands);
  }

  // Final file read
  fileContent = fs.existsSync(absPath) ? fs.readFileSync(absPath, "utf8") : fileContent;
  appendCmdHistory(historyEntries);

  return {
    allCommandResults,
    fileContent,
    plan,
    sessionTasks,
    toolTurnCount,
    // A sed edit only "happened" if it ran AND produced output or was verified β€” exit 0 alone is not enough
    // because `sed -i` returns 0 even when the pattern didn't match anything on some systems.
    // tee is blocked entirely β€” no tee detection needed.
    hadSedEdits: allCommandResults.some(r =>
      r.cmd.startsWith("sed -i") && r.code === 0 &&
      (r.structured?.linesChanged === "applied" || !r.stderr)
    ),
    hadTeeWrites: false, // tee is blocked β€” this is always false now
    hadFullRewrite: allCommandResults.some(r => r.cmd.startsWith("[full_rewrite]") && r.code === 0),
  };
}

// ── FILE INSPECT API (map / chunk / search) ───────────────────────────────
app.post("/api/inspect", (req, res) => {
  const { filePath, actions } = req.body;
  if (!filePath) return res.status(400).json({ error: "filePath required" });
  const abs = path.join(HOME_DIR, filePath);
  if (!abs.startsWith(HOME_DIR)) return res.status(403).json({ error: "Forbidden" });
  const results = processReadActions(filePath, actions || [{ action: "map" }]);
  res.json({ results, formatted: formatReadResults(results) });
});

// ── FILE TREE ──────────────────────────────────────────────────────────────
app.get("/api/tree", (req, res) => {
  const buildTree = (dir, base = HOME_DIR) => {
    try {
      return fs.readdirSync(dir, { withFileTypes: true })
        .filter(e => !e.name.startsWith(".cryo_") && !e.name.startsWith(".versions") && !e.name.startsWith("__cryo"))
        .map(e => {
          const relPath = path.relative(base, path.join(dir, e.name));
          if (e.isDirectory()) return { type: "dir", name: e.name, path: relPath, children: buildTree(path.join(dir, e.name), base) };
          const stats = fs.statSync(path.join(dir, e.name));
          return { type: "file", name: e.name, path: relPath, size: stats.size };
        });
    } catch { return []; }
  };
  try { res.json({ tree: buildTree(HOME_DIR) }); }
  catch { res.json({ tree: [] }); }
});

app.post("/api/upload", upload.single("file"), (req, res) => {
  res.json({ success: true, path: req.file?.filename || "index.js" });
});

app.get("/api/file", (req, res) => {
  const fp = path.join(HOME_DIR, req.query.path || "index.js");
  if (!fp.startsWith(HOME_DIR)) return res.status(403).json({ error: "Forbidden" });
  if (!fs.existsSync(fp)) return res.status(404).json({ error: "Not found" });
  res.json({ content: fs.readFileSync(fp, "utf8") });
});

app.post("/api/file", (req, res) => {
  const { filePath, content } = req.body;
  const abs = path.join(HOME_DIR, filePath);
  if (!abs.startsWith(HOME_DIR)) return res.status(403).json({ error: "Forbidden" });
  versionFile(filePath);
  fs.mkdirSync(path.dirname(abs), { recursive: true });
  fs.writeFileSync(abs, content, "utf8");
  res.json({ success: true });
});

app.delete("/api/file", (req, res) => {
  const { filePath } = req.body;
  const abs = path.join(HOME_DIR, filePath);
  if (!abs.startsWith(HOME_DIR)) return res.status(403).json({ error: "Forbidden" });
  if (fs.existsSync(abs)) fs.unlinkSync(abs);
  res.json({ success: true });
});

app.get("/api/download", (req, res) => {
  const fp = path.join(HOME_DIR, req.query.path);
  if (!fp.startsWith(HOME_DIR)) return res.status(403).json({ error: "Forbidden" });
  if (!fs.existsSync(fp)) return res.status(404).json({ error: "Not found" });
  res.download(fp);
});

app.get("/api/state", (req, res) => res.json(readState()));

app.get("/api/tasks", (req, res) => res.json({ tasks: readTasks() }));

app.get("/api/history", (req, res) => {
  if (!fs.existsSync(CMD_HISTORY_FILE)) return res.json({ history: [] });
  try { res.json({ history: JSON.parse(fs.readFileSync(CMD_HISTORY_FILE, "utf8")) }); }
  catch { res.json({ history: [] }); }
});

app.get("/api/versions", (req, res) => {
  const { file } = req.query;
  const versions = readVersions();
  res.json({ versions: file ? (versions[file] || []) : versions });
});

app.post("/api/rollback", (req, res) => {
  const { file, versionPath } = req.body;
  const src = path.join(HOME_DIR, versionPath);
  const dst = path.join(HOME_DIR, file);
  if (!src.startsWith(HOME_DIR) || !dst.startsWith(HOME_DIR)) return res.status(403).json({ error: "Forbidden" });
  if (!fs.existsSync(src)) return res.status(404).json({ error: "Version not found" });
  versionFile(file);
  fs.copyFileSync(src, dst);
  res.json({ success: true, content: fs.readFileSync(dst, "utf8") });
});

app.get("/api/models", (req, res) => {
  res.json({ models: Object.entries(MODELS).map(([k, v]) => ({ key: k, name: v.name, description: v.description })) });
});

app.post("/api/exec", (req, res) => {
  const { command } = req.body;
  if (!isSafeCmd(command)) {
    return res.json({ stdout: "", stderr: `Blocked: ${command.trim().split(/\s+/)[0]}`, code: 1, ok: false });
  }
  exec(command, { cwd: HOME_DIR, timeout: 30000, shell: "/bin/bash" }, (err, stdout, stderr) => {
    const code = err ? (err.code || 1) : 0;
    res.json({
      ...structureResult(command, stdout || "", stderr || "", code),
      code,
    });
  });
});

// ── MAIN CHAT ENDPOINT ─────────────────────────────────────────────────────
app.post("/api/chat", async (req, res) => {
  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache");
  res.setHeader("Connection", "keep-alive");

  const send = (type, data) => {
    try { res.write(`data: ${JSON.stringify({ type, ...data })}\n\n`); } catch {}
  };

  const { message, currentFile, model: modelKey = "cryo2", transcript = [] } = req.body;

  try {
    // ── Web search (no file context) ───────────────────────────────────────
    if (needsWebSearch(message) && !currentFile) {
      send("status", { text: "Searching the web..." });
      send("web_search", { query: message });
      try {
        const result = await webSearch(message);
        send("web_search_result", { query: message, citations: result.citations });
        send("message", { text: result.answer });
      } catch (e) {
        send("message", { text: "Web search failed: " + e.message });
      }
      res.write("data: [DONE]\n\n");
      res.end();
      return;
    }

    let filePath = resolveFile(currentFile);
    const absPath = filePath ? path.join(HOME_DIR, filePath) : path.join(HOME_DIR, "index.js");
    if (!filePath) filePath = "index.js";
    let fileContent = fs.existsSync(absPath) ? fs.readFileSync(absPath, "utf8") : "";
    const hasFile = fileContent.length > 0;
    const existingTasks = readTasks();
    const pendingTasks = existingTasks.filter(t => !t.done);

    // ── NO FILE: Create from scratch ───────────────────────────────────────
    if (!hasFile) {
      send("status", { text: "Planning what to build..." });

      const createPrompt = `You are Cryo - a precise developer AI. Create the code the user requested.

IRON LAW: Create ONLY what the request asks for. Nothing more, nothing less.
Interpret the request LITERALLY. No bonus features. No unrequested "improvements".

USER REQUEST: "${message}"

Respond with ONLY a raw JSON object (no markdown, no prose around it):
{
  "filename": "appropriate filename like bot.js or app.py",
  "description": "one line description of what this is",
  "tasks": [{"task": "what you built", "done": true}],
  "content": "COMPLETE SOURCE CODE HERE β€” this must be raw source code, NOT JSON. Use \\n for newlines.",
  "status": "Created - brief description"
}

CRITICAL RULES FOR THE "content" FIELD:
- "content" must contain the raw source code of the file (JavaScript, Python, etc.)
- "content" must NOT be a JSON object, array, or any JSON value β€” it is plain source code as a JSON string
- "content" must be the complete, working file β€” every import, every function, ready to run
- Use \\n for line breaks inside the string value
- Escape all double-quotes inside the code as \\"
- filename must have the correct extension (.js, .py, etc.)

WRONG β€” content that is JSON:  "content": {"code": "..."}
WRONG β€” content that is the full response again: "content": "{\\"filename\\":\\"bot.js\\"..."
RIGHT β€” content that is source code: "content": "const x = require('y');\\n\\nmodule.exports = x;"`;


      const aiRaw = await callAI(createPrompt, "", modelKey, transcript);
      let parsed = parseJSON(aiRaw);

      // Guard: if parsed.content looks like JSON (the AI put the wrapper inside content),
      // try to extract the nested "content" field, then fall back to markdown extraction.
      if (parsed?.content) {
        const c = parsed.content.trim();
        // Detect if content is itself a JSON blob (starts with { or [)
        if (c.startsWith("{") || c.startsWith("[")) {
          const inner = parseJSON(c);
          if (inner?.content && typeof inner.content === "string" && !inner.content.trim().startsWith("{")) {
            // The real code was nested one level deeper
            parsed = { ...parsed, ...inner };
          } else {
            // Can't recover from JSON-as-content β€” force fallback
            parsed.content = null;
          }
        }
      }

      // Fallback: extract code from markdown response or raw text
      if (!parsed?.content || parsed.content.trim().startsWith("{")) {
        const extracted = extractContentFromText(aiRaw || "");
        const fnMatch = aiRaw.match(/(?:file|save|name)[^a-z]*?([a-z0-9_-]+\.[a-z]{2,4})/i);
        parsed = {
          filename: (parsed?.filename) || fnMatch?.[1] || "index.js",
          content: extracted || aiRaw,
          description: parsed?.description || "Generated code",
          tasks: parsed?.tasks || [{ task: "Generate file", done: true }],
          status: parsed?.status || "Created"
        };
      }

      send("status", { text: `Plan ready β€” ${parsed.tasks?.length || 1} task(s)` });
      send("task_update", { tasks: parsed.tasks || [] });
      send("status", { text: "Checking generated code..." });

      const tmpName = `__cryo_new_${Date.now()}.js`;
      const tmpPath = path.join(HOME_DIR, tmpName);
      fs.writeFileSync(tmpPath, parsed.content, "utf8");

      // Only syntax-check JS files
      if (parsed.filename.endsWith(".js") || parsed.filename.endsWith(".ts")) {
        await syntaxFixLoop(tmpName, send, modelKey, 3, transcript);
        parsed.content = fs.readFileSync(tmpPath, "utf8");
      }
      fs.unlinkSync(tmpPath);

      send("status", { text: `Saving ${parsed.filename}...` });
      const savePath = path.join(HOME_DIR, parsed.filename);
      fs.mkdirSync(path.dirname(savePath), { recursive: true });
      fs.writeFileSync(savePath, parsed.content, "utf8");

      if (parsed.filename.endsWith(".js")) {
        const finalCheck = await runCmd(`node --check ${parsed.filename}`);
        send("command", { cmd: `node --check ${parsed.filename}`, index: 0 });
        send("command_result", {
          cmd: `node --check ${parsed.filename}`,
          stdout: finalCheck.code === 0 ? "βœ“ Syntax OK β€” file saved!" : "",
          stderr: finalCheck.stderr, index: 0, isError: finalCheck.code !== 0
        });
      }

      updateTasks(parsed.tasks || []);
      updateMeta({ last_ai_edit: new Date().toISOString(), languages: [parsed.filename.split(".").pop()] });
      saveState({ activeFile: parsed.filename });
      send("file_created", { filename: parsed.filename, content: parsed.content, description: parsed.description });
      send("message", { text: parsed.status || `Created \`${parsed.filename}\`` });
      send("tree_update", {});
      res.write("data: [DONE]\n\n");
      res.end();
      return;
    }

    // ── FILE EXISTS: Iterative agent loop ─────────────────────────────────
    send("status", { text: "Analyzing..." });

    const loopResult = await runAgentLoop({
      filePath, fileContent, message, modelKey, send, maxRounds: 6, transcript
    });

    const { allCommandResults, hadSedEdits, hadFullRewrite, plan } = loopResult;
    fileContent = loopResult.fileContent;

    const resultsText = allCommandResults
      .map(r => `$ ${r.cmd}\n${r.stdout.substring(0, 400)}${r.stderr ? `\nSTDERR: ${r.stderr.substring(0, 150)}` : ""}`)
      .join("\n\n");

    send("status", { text: "Synthesizing..." });

    // ── QUERY: just answer ─────────────────────────────────────────────
    if (plan.task_type === "query") {
      if (needsWebSearch(message)) {
        send("web_search", { query: message });
        try {
          const wsResult = await webSearch(message);
          send("web_search_result", { query: message, citations: wsResult.citations });
          send("message", { text: wsResult.answer });
        } catch {
          const ansRaw = await callAI(
            `You are Cryo. Answer the question based on the file content and command results. Be concise and direct. Return ONLY JSON: {"answer":"your answer here"}`,
            `Question: "${message}"\nCommand results:\n${resultsText}\nFile context:\n${fileContent.substring(0, 1500)}`,
            modelKey,
            transcript
          );
          const ans = parseJSON(ansRaw);
          send("message", { text: ans?.answer || ansRaw || "Could not generate answer." });
        }
      } else {
        const ansRaw = await callAI(
          `You are Cryo. Answer the question using the command results and file content below. Return ONLY JSON: {"answer":"your answer"}`,
          `QUESTION: "${message}"\nCOMMAND RESULTS:\n${resultsText.substring(0, 1200)}\nFILE (${filePath}):\n${fileContent.substring(0, 1200)}`,
          modelKey,
          transcript
        );
        const ans = parseJSON(ansRaw);
        send("message", { text: ans?.answer || ansRaw || "Could not generate answer." });
      }

    } else {
      // ── EDIT/CREATE: sed/tee was used β†’ verify & summarize ──────────────
      if (hadSedEdits || loopResult.hadTeeWrites || hadFullRewrite) {
        send("status", { text: "Verifying edits..." });
        if (filePath.endsWith(".js")) {
          await syntaxFixLoop(filePath, send, modelKey, 3, transcript);
        }
        fileContent = fs.existsSync(absPath) ? fs.readFileSync(absPath, "utf8") : fileContent;

        // ── GROUND-TRUTH VERIFICATION ────────────────────────────────────
        // Ask the AI to check the ACTUAL file content against the request.
        // This is the only honest source β€” not the command log.
        const verifyRaw = await callAI(
          `You are a code reviewer applying the IRON LAW.
The request was: "${message}"
Check: (1) Is EXACTLY what was requested implemented? (2) Was anything NOT in the request changed?
Return ONLY JSON (no markdown):
{
  "implemented": true | false,
  "summary": "one honest sentence: what changed, or what is MISSING if not implemented",
  "missing": "describe what is still missing, or null if fully done",
  "extra_changes": "describe any changes made BEYOND the request, or null if clean"
}`,
          `ORIGINAL REQUEST: "${message}"
ACTUAL FILE CONTENT (this is the real file on disk β€” trust this, not the command log):
${fileContent.substring(0, 3500)}`,
          modelKey,
          transcript
        );
        const verify = parseJSON(verifyRaw);

        const allTasks = [...existingTasks.map(t => ({ ...t, done: true })), ...(plan.tasks || []).map(t => ({ ...t, done: verify?.implemented !== false }))];
        saveTasks(allTasks);
        send("task_update", { tasks: allTasks });
        versionFile(filePath);
        updateMeta({ last_ai_edit: new Date().toISOString() });
        send("file_updated", { filename: filePath, content: fileContent, description: verify?.summary || "Updated" });

        if (verify?.implemented === false) {
          // Change did NOT land β€” report honestly, do not claim success
          send("message", { text: `⚠️ Edit incomplete: ${verify.summary}${verify.missing ? ` β€” still missing: ${verify.missing}` : ""}` });
        } else {
          send("message", { text: verify?.summary || "Changes applied successfully." });
        }

      } else {
        // ── No sed edits were made β†’ ask AI for complete file content ───
        // Include full file context + command history so the AI has the same
        // "read the file first" context that the sheet mandates.
        const editRaw = await callAI(
          `You are Cryo. The shell-command agent ran but made no file edits.
Now produce the COMPLETE updated file that fulfills the user's request.

IRON LAW: Change ONLY what the request asks for. Preserve everything else EXACTLY.
SELF-CHECK before writing: "Am I only changing what was requested?"

RULES:
- Preserve EVERYTHING not changed by the request β€” same structure, same order, same style
- Output must be the complete working file
- Apply ONLY what the request asked for β€” nothing more
- Do NOT add features, fix other things, or reformat existing code
- Return ONLY raw JSON (no markdown fences):
{"content":"COMPLETE file here","description":"one line of what changed","status":"Done - brief summary"}`,
          `ORIGINAL REQUEST: "${message}"
FILE: ${filePath} (${fileContent.split("\n").length} lines)
COMMAND RESULTS:\n${resultsText.substring(0, 800)}
CURRENT FILE CONTENT (line numbers for reference):\n${
  fileContent.split("\n").slice(0, 300).map((l, i) => `${i + 1}\t${l}`).join("\n").substring(0, 3500)
}\nProduce the complete updated file now.`,
          modelKey,
          transcript
        );

        let editParsed = parseJSON(editRaw);

        // Fallback: extract code from markdown
        if (!editParsed?.content) {
          const extracted = extractContentFromText(editRaw || "");
          editParsed = extracted
            ? { content: extracted, description: "Updated", status: "Done" }
            : { content: fileContent, description: "No changes made", status: "Done" };
        }

        versionFile(filePath);

        // Pre-save syntax check
        send("status", { text: "Checking before saving..." });
        const tmpName = `__cryo_presave_${Date.now()}.js`;
        const tmpPath = path.join(HOME_DIR, tmpName);
        fs.writeFileSync(tmpPath, editParsed.content, "utf8");

        if (filePath.endsWith(".js")) {
          await syntaxFixLoop(tmpName, send, modelKey, 3, transcript);
          editParsed.content = fs.readFileSync(tmpPath, "utf8");
        }
        fs.unlinkSync(tmpPath);

        fs.writeFileSync(absPath, editParsed.content, "utf8");
        send("status", { text: `Saved ${filePath}` });

        if (filePath.endsWith(".js")) {
          await syntaxFixLoop(filePath, send, modelKey, 3, transcript);
        }
        fileContent = fs.readFileSync(absPath, "utf8");

        // Verify honestly against real file
        const verifyFbRaw = await callAI(
          `You are a code reviewer. Check if the user's request is implemented in the actual file.
Return ONLY JSON:
{"implemented": true | false, "summary": "one honest sentence", "missing": "what is missing or null"}`,
          `REQUEST: "${message}"\nACTUAL FILE (trust this):\n${fileContent.substring(0, 3500)}`,
          modelKey,
          transcript
        );
        const verifyFb = parseJSON(verifyFbRaw);

        const allTasks = [
          ...existingTasks.map(t => ({ ...t, done: true })),
          ...(plan.tasks || []).map(t => ({ ...t, done: verifyFb?.implemented !== false }))
        ];
        saveTasks(allTasks);
        updateMeta({ last_ai_edit: new Date().toISOString() });
        send("task_update", { tasks: allTasks });
        send("file_updated", { filename: filePath, content: fileContent, description: verifyFb?.summary || editParsed.description });

        if (verifyFb?.implemented === false) {
          send("message", { text: `⚠️ Edit incomplete: ${verifyFb.summary}${verifyFb.missing ? ` β€” still missing: ${verifyFb.missing}` : ""}` });
        } else {
          send("message", { text: verifyFb?.summary || editParsed.status || editParsed.description });
        }
        send("tree_update", {});
      }
    }

    // ── Pending tasks check ────────────────────────────────────────────
    const latestTasks = readTasks();
    const stillPending = latestTasks.filter(t => !t.done);
    if (stillPending.length > 0) {
      send("pending_tasks", { tasks: stillPending, count: stillPending.length });
    }

    res.write("data: [DONE]\n\n");
    res.end();

  } catch (err) {
    console.error("Chat error:", err);
    send("error", { text: err.message });
    res.write("data: [DONE]\n\n");
    res.end();
  }
});

app.listen(PORT, () => {
  console.log(`\n❄️  Cryo Dev Server β†’ http://localhost:${PORT}`);
  console.log(`πŸ“  Home dir: ${HOME_DIR}\n`);
});