ronds_ai 0.1.13 → 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +324 -324
- package/bin/ronds_ai.js +265 -265
- package/lib/code_record.js +925 -918
- package/lib/doctor.js +89 -89
- package/lib/skills_install.js +226 -226
- package/lib/skills_prompt.js +130 -130
- package/lib/skills_targets.js +81 -81
- package/package.json +16 -16
package/lib/code_record.js
CHANGED
|
@@ -1,918 +1,925 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const os = require('os');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const http = require('http');
|
|
5
|
-
const https = require('https');
|
|
6
|
-
const { execFileSync } = require('child_process');
|
|
7
|
-
const { runGit } = require('./git');
|
|
8
|
-
const { createHash, randomUUID } = require('crypto');
|
|
9
|
-
|
|
10
|
-
const DEFAULT_TIMEOUT_MS = 10000;
|
|
11
|
-
const MAX_ADDED_CONTENT_CHARS = 20000;
|
|
12
|
-
const DEFAULT_HOOK_REPORT_URL = 'https://aihub.ronds.com/api/api/v1/ai-code-events';
|
|
13
|
-
const CLAUDE_SUPPORTED_HOOK_EVENT = 'PostToolUse';
|
|
14
|
-
const CLAUDE_SUPPORTED_TOOLS = new Set(['Write', 'Edit', 'MultiEdit']);
|
|
15
|
-
const CURSOR_SUPPORTED_HOOK_EVENT = 'afterFileEdit';
|
|
16
|
-
const CODEX_SUPPORTED_HOOK_EVENTS = new Set(['UserPromptSubmit', 'Stop']);
|
|
17
|
-
const CODEX_SNAPSHOT_DIR = path.join(os.homedir(), '.ronds_ai', 'codex-snapshots');
|
|
18
|
-
const CODEX_SNAPSHOT_TTL_SEC = 86400;
|
|
19
|
-
const GIT_STATUS_ARGS = ['status', '--porcelain=v1', '--untracked-files=all'];
|
|
20
|
-
const CODEX_STOP_OUTPUT = {
|
|
21
|
-
continue: false,
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
function readStdin() {
|
|
25
|
-
return new Promise((resolve, reject) => {
|
|
26
|
-
let data = '';
|
|
27
|
-
process.stdin.setEncoding('utf8');
|
|
28
|
-
process.stdin.on('data', (chunk) => {
|
|
29
|
-
data += chunk;
|
|
30
|
-
});
|
|
31
|
-
process.stdin.on('end', () => resolve(data));
|
|
32
|
-
process.stdin.on('error', reject);
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function parseJson(raw, label) {
|
|
37
|
-
try {
|
|
38
|
-
return JSON.parse(raw);
|
|
39
|
-
} catch (error) {
|
|
40
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
matrix[i][j] =
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
let
|
|
77
|
-
let
|
|
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
|
-
payload.
|
|
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
|
-
const
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
changeInfo =
|
|
386
|
-
} else {
|
|
387
|
-
changeInfo =
|
|
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
|
-
const
|
|
413
|
-
const
|
|
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
|
-
const
|
|
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
|
-
const
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
const
|
|
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
|
-
const
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
snapshot =
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
const
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
...
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
const
|
|
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
|
-
const
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
const
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
const
|
|
768
|
-
fs.
|
|
769
|
-
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
event,
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
const
|
|
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
|
-
const
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
const
|
|
897
|
-
results.push({
|
|
898
|
-
status: '
|
|
899
|
-
event_id: event.event_id,
|
|
900
|
-
|
|
901
|
-
});
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
};
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const http = require('http');
|
|
5
|
+
const https = require('https');
|
|
6
|
+
const { execFileSync } = require('child_process');
|
|
7
|
+
const { runGit } = require('./git');
|
|
8
|
+
const { createHash, randomUUID } = require('crypto');
|
|
9
|
+
|
|
10
|
+
const DEFAULT_TIMEOUT_MS = 10000;
|
|
11
|
+
const MAX_ADDED_CONTENT_CHARS = 20000;
|
|
12
|
+
const DEFAULT_HOOK_REPORT_URL = 'https://aihub.ronds.com/api/api/v1/ai-code-events';
|
|
13
|
+
const CLAUDE_SUPPORTED_HOOK_EVENT = 'PostToolUse';
|
|
14
|
+
const CLAUDE_SUPPORTED_TOOLS = new Set(['Write', 'Edit', 'MultiEdit']);
|
|
15
|
+
const CURSOR_SUPPORTED_HOOK_EVENT = 'afterFileEdit';
|
|
16
|
+
const CODEX_SUPPORTED_HOOK_EVENTS = new Set(['UserPromptSubmit', 'Stop']);
|
|
17
|
+
const CODEX_SNAPSHOT_DIR = path.join(os.homedir(), '.ronds_ai', 'codex-snapshots');
|
|
18
|
+
const CODEX_SNAPSHOT_TTL_SEC = 86400;
|
|
19
|
+
const GIT_STATUS_ARGS = ['status', '--porcelain=v1', '--untracked-files=all'];
|
|
20
|
+
const CODEX_STOP_OUTPUT = {
|
|
21
|
+
continue: false,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function readStdin() {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
let data = '';
|
|
27
|
+
process.stdin.setEncoding('utf8');
|
|
28
|
+
process.stdin.on('data', (chunk) => {
|
|
29
|
+
data += chunk;
|
|
30
|
+
});
|
|
31
|
+
process.stdin.on('end', () => resolve(data));
|
|
32
|
+
process.stdin.on('error', reject);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseJson(raw, label) {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(raw);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
41
|
+
const wrappedError = new Error(`${label} is not valid JSON: ${detail}`);
|
|
42
|
+
wrappedError.rawPayload = raw;
|
|
43
|
+
throw wrappedError;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeLines(text) {
|
|
48
|
+
return String(text || '').replace(/\r\n/g, '\n').split('\n');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function buildLcsMatrix(beforeLines, afterLines) {
|
|
52
|
+
const matrix = Array.from(
|
|
53
|
+
{ length: beforeLines.length + 1 },
|
|
54
|
+
() => Array(afterLines.length + 1).fill(0),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
for (let i = beforeLines.length - 1; i >= 0; i -= 1) {
|
|
58
|
+
for (let j = afterLines.length - 1; j >= 0; j -= 1) {
|
|
59
|
+
if (beforeLines[i] === afterLines[j]) {
|
|
60
|
+
matrix[i][j] = matrix[i + 1][j + 1] + 1;
|
|
61
|
+
} else {
|
|
62
|
+
matrix[i][j] = Math.max(matrix[i + 1][j], matrix[i][j + 1]);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return matrix;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function diffLineStats(beforeText, afterText) {
|
|
71
|
+
const beforeLines = normalizeLines(beforeText);
|
|
72
|
+
const afterLines = normalizeLines(afterText);
|
|
73
|
+
const matrix = buildLcsMatrix(beforeLines, afterLines);
|
|
74
|
+
const addedLines = [];
|
|
75
|
+
|
|
76
|
+
let added = 0;
|
|
77
|
+
let removed = 0;
|
|
78
|
+
let i = 0;
|
|
79
|
+
let j = 0;
|
|
80
|
+
|
|
81
|
+
while (i < beforeLines.length && j < afterLines.length) {
|
|
82
|
+
if (beforeLines[i] === afterLines[j]) {
|
|
83
|
+
i += 1;
|
|
84
|
+
j += 1;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (matrix[i][j + 1] >= matrix[i + 1][j]) {
|
|
89
|
+
if (afterLines[j] !== '') {
|
|
90
|
+
addedLines.push(afterLines[j]);
|
|
91
|
+
added += 1;
|
|
92
|
+
}
|
|
93
|
+
j += 1;
|
|
94
|
+
} else {
|
|
95
|
+
if (beforeLines[i] !== '') {
|
|
96
|
+
removed += 1;
|
|
97
|
+
}
|
|
98
|
+
i += 1;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
while (j < afterLines.length) {
|
|
103
|
+
if (afterLines[j] !== '') {
|
|
104
|
+
addedLines.push(afterLines[j]);
|
|
105
|
+
added += 1;
|
|
106
|
+
}
|
|
107
|
+
j += 1;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
while (i < beforeLines.length) {
|
|
111
|
+
if (beforeLines[i] !== '') {
|
|
112
|
+
removed += 1;
|
|
113
|
+
}
|
|
114
|
+
i += 1;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let addedContent = addedLines.join('\n');
|
|
118
|
+
if (addedContent.length > MAX_ADDED_CONTENT_CHARS) {
|
|
119
|
+
addedContent = `${addedContent.slice(0, MAX_ADDED_CONTENT_CHARS)}\n... (truncated)`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return { added, removed, addedContent };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function findGitRepoRoot(startDir) {
|
|
126
|
+
let currentDir = path.resolve(startDir);
|
|
127
|
+
|
|
128
|
+
while (true) {
|
|
129
|
+
if (fs.existsSync(path.join(currentDir, '.git'))) {
|
|
130
|
+
return currentDir;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const parentDir = path.dirname(currentDir);
|
|
134
|
+
if (parentDir === currentDir) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
currentDir = parentDir;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function extractRepoNameFromRemoteUrl(remoteUrl) {
|
|
143
|
+
const normalizedUrl = String(remoteUrl || '').trim().replace(/[\\/]+$/, '');
|
|
144
|
+
if (!normalizedUrl) {
|
|
145
|
+
return '';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const lastSegment = normalizedUrl.split(/[:/]/).pop() || '';
|
|
149
|
+
return lastSegment.replace(/\.git$/i, '').trim();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function resolveGitMetadata(repoRoot) {
|
|
153
|
+
const remoteUrl = runGit(repoRoot, ['remote', 'get-url', 'origin'], false);
|
|
154
|
+
const remoteRepoName = extractRepoNameFromRemoteUrl(remoteUrl);
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
repo_name: remoteRepoName || path.basename(repoRoot),
|
|
158
|
+
remote_url: remoteUrl,
|
|
159
|
+
'user.name': runGit(repoRoot, ['config', 'user.name'], false),
|
|
160
|
+
'user.email': runGit(repoRoot, ['config', 'user.email'], false),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function normalizeWorkspaceRoot(root) {
|
|
165
|
+
const raw = String(root || '').trim();
|
|
166
|
+
if (!raw) {
|
|
167
|
+
return '';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const normalized = raw.replace(/^\/+/, '');
|
|
171
|
+
if (/^[a-zA-Z]:/.test(normalized)) {
|
|
172
|
+
return path.normalize(normalized);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return path.normalize(raw);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function resolveClaudeAbsoluteFilePath(filePath, payload) {
|
|
179
|
+
const rawPath = String(filePath || '').trim();
|
|
180
|
+
if (!rawPath) {
|
|
181
|
+
return '';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (path.isAbsolute(rawPath)) {
|
|
185
|
+
return path.normalize(rawPath);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const candidateBases = [
|
|
189
|
+
payload.cwd,
|
|
190
|
+
payload.workspace && payload.workspace.current_dir,
|
|
191
|
+
payload.workspace && payload.workspace.project_dir,
|
|
192
|
+
process.env.CLAUDE_PROJECT_DIR,
|
|
193
|
+
process.cwd(),
|
|
194
|
+
].filter(Boolean);
|
|
195
|
+
|
|
196
|
+
for (const base of candidateBases) {
|
|
197
|
+
const candidate = path.resolve(base, rawPath);
|
|
198
|
+
if (fs.existsSync(candidate) || fs.existsSync(path.dirname(candidate))) {
|
|
199
|
+
return candidate;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return path.resolve(process.cwd(), rawPath);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function resolveCursorAbsoluteFilePath(filePath, payload) {
|
|
207
|
+
const rawPath = String(filePath || '').trim();
|
|
208
|
+
if (!rawPath) {
|
|
209
|
+
return '';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (path.isAbsolute(rawPath)) {
|
|
213
|
+
return path.normalize(rawPath);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const workspaceRoots = Array.isArray(payload.workspace_roots)
|
|
217
|
+
? payload.workspace_roots.map(normalizeWorkspaceRoot)
|
|
218
|
+
: [];
|
|
219
|
+
const candidateBases = [
|
|
220
|
+
...workspaceRoots,
|
|
221
|
+
process.env.CURSOR_PROJECT_DIR,
|
|
222
|
+
process.cwd(),
|
|
223
|
+
].filter(Boolean);
|
|
224
|
+
|
|
225
|
+
for (const base of candidateBases) {
|
|
226
|
+
const candidate = path.resolve(base, rawPath);
|
|
227
|
+
if (fs.existsSync(candidate) || fs.existsSync(path.dirname(candidate))) {
|
|
228
|
+
return candidate;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return path.resolve(process.cwd(), rawPath);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function getGitInfo(filePath) {
|
|
236
|
+
if (!filePath) {
|
|
237
|
+
return { repoRoot: null, git: null, repoRelativePath: '' };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const lookupBase = fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()
|
|
241
|
+
? filePath
|
|
242
|
+
: path.dirname(filePath);
|
|
243
|
+
const repoRoot = findGitRepoRoot(lookupBase);
|
|
244
|
+
if (!repoRoot) {
|
|
245
|
+
return {
|
|
246
|
+
repoRoot: null,
|
|
247
|
+
git: null,
|
|
248
|
+
repoRelativePath: String(filePath).replace(/\\/g, '/'),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
repoRoot,
|
|
254
|
+
repoRelativePath: path.relative(repoRoot, filePath).replace(/\\/g, '/'),
|
|
255
|
+
git: resolveGitMetadata(repoRoot),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function readWorkerIdConfig(baseDir) {
|
|
260
|
+
if (!baseDir) {
|
|
261
|
+
return '';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const configPath = path.join(baseDir, '.ai_config', 'config.json');
|
|
265
|
+
try {
|
|
266
|
+
if (!fs.existsSync(configPath)) {
|
|
267
|
+
return '';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const parsed = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
271
|
+
if (typeof parsed.worker_id === 'string' && parsed.worker_id.trim()) {
|
|
272
|
+
return parsed.worker_id.trim();
|
|
273
|
+
}
|
|
274
|
+
} catch {
|
|
275
|
+
return '';
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return '';
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function resolveWorkerId(repoRoot) {
|
|
282
|
+
const candidates = [process.cwd(), repoRoot];
|
|
283
|
+
for (const candidate of candidates) {
|
|
284
|
+
const workerId = readWorkerIdConfig(candidate);
|
|
285
|
+
if (workerId) {
|
|
286
|
+
return workerId;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return process.env.MCP_TRACKER_WORKER_ID || process.env.WORKER_ID || os.userInfo().username;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function buildWriteChange(toolInput) {
|
|
294
|
+
const content = String(toolInput.content || '').replace(/\r\n/g, '\n');
|
|
295
|
+
return {
|
|
296
|
+
operation: 'write',
|
|
297
|
+
changes: {
|
|
298
|
+
added_content: content,
|
|
299
|
+
added: content ? content.split('\n').filter((line) => line !== '').length : 0,
|
|
300
|
+
removed: 0,
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function buildEditChange(toolInput) {
|
|
306
|
+
const diff = diffLineStats(toolInput.old_string || '', toolInput.new_string || '');
|
|
307
|
+
return {
|
|
308
|
+
operation: 'edit',
|
|
309
|
+
changes: {
|
|
310
|
+
added_content: diff.addedContent,
|
|
311
|
+
added: diff.added,
|
|
312
|
+
removed: diff.removed,
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function buildMultiEditChange(toolInput) {
|
|
318
|
+
const edits = Array.isArray(toolInput.edits) ? toolInput.edits : [];
|
|
319
|
+
const aggregate = {
|
|
320
|
+
added_content: [],
|
|
321
|
+
added: 0,
|
|
322
|
+
removed: 0,
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
for (const edit of edits) {
|
|
326
|
+
const diff = diffLineStats(edit && edit.old_string, edit && edit.new_string);
|
|
327
|
+
if (diff.addedContent) {
|
|
328
|
+
aggregate.added_content.push(diff.addedContent);
|
|
329
|
+
}
|
|
330
|
+
aggregate.added += diff.added;
|
|
331
|
+
aggregate.removed += diff.removed;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
operation: 'edit',
|
|
336
|
+
changes: {
|
|
337
|
+
added_content: aggregate.added_content.join('\n'),
|
|
338
|
+
added: aggregate.added,
|
|
339
|
+
removed: aggregate.removed,
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function buildCursorChange(payload) {
|
|
345
|
+
const edits = Array.isArray(payload.edits) ? payload.edits : [];
|
|
346
|
+
const aggregate = {
|
|
347
|
+
added_content: [],
|
|
348
|
+
added: 0,
|
|
349
|
+
removed: 0,
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
for (const edit of edits) {
|
|
353
|
+
const diff = diffLineStats(edit && edit.old_string, edit && edit.new_string);
|
|
354
|
+
if (diff.addedContent) {
|
|
355
|
+
aggregate.added_content.push(diff.addedContent);
|
|
356
|
+
}
|
|
357
|
+
aggregate.added += diff.added;
|
|
358
|
+
aggregate.removed += diff.removed;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
operation: 'edit',
|
|
363
|
+
changes: {
|
|
364
|
+
added_content: aggregate.added_content.join('\n'),
|
|
365
|
+
added: aggregate.added,
|
|
366
|
+
removed: aggregate.removed,
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function buildClaudeEvent(payload, source) {
|
|
372
|
+
const hookEvent = payload.hook_event_name || payload.hookEvent;
|
|
373
|
+
const toolName = payload.tool_name || payload.toolName;
|
|
374
|
+
if (hookEvent !== CLAUDE_SUPPORTED_HOOK_EVENT || !CLAUDE_SUPPORTED_TOOLS.has(toolName)) {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const toolInput = payload.tool_input || payload.toolInput || {};
|
|
379
|
+
const absoluteFilePath = resolveClaudeAbsoluteFilePath(toolInput.file_path, payload);
|
|
380
|
+
const { repoRoot, git, repoRelativePath } = getGitInfo(absoluteFilePath);
|
|
381
|
+
const workerId = resolveWorkerId(repoRoot);
|
|
382
|
+
|
|
383
|
+
let changeInfo;
|
|
384
|
+
if (toolName === 'Write') {
|
|
385
|
+
changeInfo = buildWriteChange(toolInput);
|
|
386
|
+
} else if (toolName === 'Edit') {
|
|
387
|
+
changeInfo = buildEditChange(toolInput);
|
|
388
|
+
} else {
|
|
389
|
+
changeInfo = buildMultiEditChange(toolInput);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
event_id: randomUUID(),
|
|
394
|
+
timestamp: new Date().toISOString(),
|
|
395
|
+
source,
|
|
396
|
+
worker_id: workerId,
|
|
397
|
+
git,
|
|
398
|
+
file: {
|
|
399
|
+
path: repoRelativePath || String(toolInput.file_path || ''),
|
|
400
|
+
operation: changeInfo.operation,
|
|
401
|
+
},
|
|
402
|
+
changes: changeInfo.changes,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function buildCursorEvent(payload, source) {
|
|
407
|
+
const hookEvent = payload.hook_event_name || payload.hookEvent;
|
|
408
|
+
if (hookEvent !== CURSOR_SUPPORTED_HOOK_EVENT) {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const absoluteFilePath = resolveCursorAbsoluteFilePath(payload.file_path, payload);
|
|
413
|
+
const { repoRoot, git, repoRelativePath } = getGitInfo(absoluteFilePath);
|
|
414
|
+
const workerId = resolveWorkerId(repoRoot);
|
|
415
|
+
const changeInfo = buildCursorChange(payload);
|
|
416
|
+
|
|
417
|
+
return {
|
|
418
|
+
event_id: randomUUID(),
|
|
419
|
+
timestamp: new Date().toISOString(),
|
|
420
|
+
source,
|
|
421
|
+
worker_id: workerId,
|
|
422
|
+
git,
|
|
423
|
+
file: {
|
|
424
|
+
path: repoRelativePath || String(payload.file_path || ''),
|
|
425
|
+
operation: changeInfo.operation,
|
|
426
|
+
},
|
|
427
|
+
changes: changeInfo.changes,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function getCodexSnapshotFilePath(payload, repoRoot) {
|
|
432
|
+
const sessionId = String(payload.session_id || payload.sessionId || '').trim();
|
|
433
|
+
const turnId = String(payload.turn_id || payload.turnId || '').trim();
|
|
434
|
+
if (!sessionId || !turnId || !repoRoot) {
|
|
435
|
+
return '';
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const key = `${repoRoot}::${sessionId}::${turnId}`;
|
|
439
|
+
const fileName = `${createHash('sha1').update(key).digest('hex')}.json`;
|
|
440
|
+
return path.join(CODEX_SNAPSHOT_DIR, fileName);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function parseGitStatus(repoRoot) {
|
|
444
|
+
const output = runGit(repoRoot, GIT_STATUS_ARGS, false);
|
|
445
|
+
const entries = [];
|
|
446
|
+
|
|
447
|
+
for (const rawLine of output.split('\n')) {
|
|
448
|
+
if (!rawLine) {
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const line = rawLine.replace(/\r$/, '');
|
|
453
|
+
const statusCode = line.slice(0, 2);
|
|
454
|
+
const body = line.slice(3);
|
|
455
|
+
if (!body) {
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const normalizedBody = body.includes(' -> ') ? body.split(' -> ').pop() : body;
|
|
460
|
+
if (!normalizedBody) {
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const state = statusCode.includes('D') ? 'deleted' : 'present';
|
|
465
|
+
entries.push({
|
|
466
|
+
path: normalizedBody,
|
|
467
|
+
state,
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return entries;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function readFileState(filePath) {
|
|
475
|
+
try {
|
|
476
|
+
const buffer = fs.readFileSync(filePath);
|
|
477
|
+
if (buffer.includes(0)) {
|
|
478
|
+
return {
|
|
479
|
+
exists: true,
|
|
480
|
+
isBinary: true,
|
|
481
|
+
content: '',
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return {
|
|
486
|
+
exists: true,
|
|
487
|
+
isBinary: false,
|
|
488
|
+
content: buffer.toString('utf8').replace(/\r\n/g, '\n'),
|
|
489
|
+
};
|
|
490
|
+
} catch {
|
|
491
|
+
return {
|
|
492
|
+
exists: false,
|
|
493
|
+
isBinary: false,
|
|
494
|
+
content: '',
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function readGitRefFileState(repoRoot, ref, relativePath) {
|
|
500
|
+
try {
|
|
501
|
+
const buffer = execFileSync('git', ['show', `${ref}:${relativePath}`], {
|
|
502
|
+
cwd: repoRoot,
|
|
503
|
+
encoding: null,
|
|
504
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
if (buffer.includes(0)) {
|
|
508
|
+
return {
|
|
509
|
+
exists: true,
|
|
510
|
+
isBinary: true,
|
|
511
|
+
content: '',
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return {
|
|
516
|
+
exists: true,
|
|
517
|
+
isBinary: false,
|
|
518
|
+
content: buffer.toString('utf8').replace(/\r\n/g, '\n'),
|
|
519
|
+
};
|
|
520
|
+
} catch {
|
|
521
|
+
return {
|
|
522
|
+
exists: false,
|
|
523
|
+
isBinary: false,
|
|
524
|
+
content: '',
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function readHeadFileState(repoRoot, relativePath) {
|
|
530
|
+
return readGitRefFileState(repoRoot, 'HEAD', relativePath);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function readCurrentHead(repoRoot) {
|
|
534
|
+
return runGit(repoRoot, ['rev-parse', 'HEAD'], false);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function parseGitDiffNameOnly(repoRoot, fromRef, toRef) {
|
|
538
|
+
const normalizedFromRef = String(fromRef || '').trim();
|
|
539
|
+
const normalizedToRef = String(toRef || '').trim();
|
|
540
|
+
if (!normalizedFromRef || !normalizedToRef || normalizedFromRef === normalizedToRef) {
|
|
541
|
+
return [];
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const output = runGit(repoRoot, ['diff', '--name-only', `${normalizedFromRef}..${normalizedToRef}`], false);
|
|
545
|
+
return output.split('\n')
|
|
546
|
+
.map((line) => line.trim().replace(/\\/g, '/'))
|
|
547
|
+
.filter(Boolean);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function statesEqual(beforeState, afterState) {
|
|
551
|
+
return beforeState.exists === afterState.exists
|
|
552
|
+
&& beforeState.isBinary === afterState.isBinary
|
|
553
|
+
&& beforeState.content === afterState.content;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function buildCodexChange(beforeState, afterState) {
|
|
557
|
+
if (afterState.isBinary || beforeState.isBinary) {
|
|
558
|
+
const operation = beforeState.exists ? 'edit' : 'write';
|
|
559
|
+
return {
|
|
560
|
+
operation,
|
|
561
|
+
changes: {
|
|
562
|
+
added_content: '',
|
|
563
|
+
added: 0,
|
|
564
|
+
removed: 0,
|
|
565
|
+
},
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const diff = diffLineStats(beforeState.content, afterState.content);
|
|
570
|
+
return {
|
|
571
|
+
operation: beforeState.exists ? 'edit' : 'write',
|
|
572
|
+
changes: {
|
|
573
|
+
added_content: diff.addedContent,
|
|
574
|
+
added: diff.added,
|
|
575
|
+
removed: diff.removed,
|
|
576
|
+
},
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function cleanupStaleSnapshots() {
|
|
581
|
+
let entries;
|
|
582
|
+
try {
|
|
583
|
+
if (!fs.existsSync(CODEX_SNAPSHOT_DIR)) {
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
entries = fs.readdirSync(CODEX_SNAPSHOT_DIR);
|
|
587
|
+
} catch {
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const nowSec = Date.now() / 1000;
|
|
592
|
+
for (const entry of entries) {
|
|
593
|
+
if (!entry.endsWith('.json')) {
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const filePath = path.join(CODEX_SNAPSHOT_DIR, entry);
|
|
598
|
+
try {
|
|
599
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
600
|
+
const snapshot = JSON.parse(raw);
|
|
601
|
+
const createdAt = String(snapshot.created_at || '');
|
|
602
|
+
const createdSec = new Date(createdAt).getTime() / 1000;
|
|
603
|
+
if (isNaN(createdSec)) {
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
if (nowSec - createdSec > CODEX_SNAPSHOT_TTL_SEC) {
|
|
607
|
+
fs.rmSync(filePath, { force: true });
|
|
608
|
+
}
|
|
609
|
+
} catch {
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function handleCodexUserPromptSubmit(payload) {
|
|
616
|
+
const cwd = payload.cwd || process.cwd();
|
|
617
|
+
const repoRoot = findGitRepoRoot(cwd);
|
|
618
|
+
const snapshotPath = getCodexSnapshotFilePath(payload, repoRoot);
|
|
619
|
+
if (!repoRoot || !snapshotPath) {
|
|
620
|
+
return [];
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const head = readCurrentHead(repoRoot);
|
|
624
|
+
const baseline = {};
|
|
625
|
+
for (const entry of parseGitStatus(repoRoot)) {
|
|
626
|
+
const absolutePath = path.join(repoRoot, entry.path);
|
|
627
|
+
baseline[entry.path] = entry.state === 'deleted'
|
|
628
|
+
? readHeadFileState(repoRoot, entry.path)
|
|
629
|
+
: readFileState(absolutePath);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
cleanupStaleSnapshots();
|
|
633
|
+
fs.mkdirSync(CODEX_SNAPSHOT_DIR, { recursive: true });
|
|
634
|
+
fs.writeFileSync(snapshotPath, JSON.stringify({
|
|
635
|
+
version: 1,
|
|
636
|
+
session_id: payload.session_id || payload.sessionId || '',
|
|
637
|
+
turn_id: payload.turn_id || payload.turnId || '',
|
|
638
|
+
repo_root: repoRoot,
|
|
639
|
+
head,
|
|
640
|
+
created_at: new Date().toISOString(),
|
|
641
|
+
baseline,
|
|
642
|
+
}), 'utf8');
|
|
643
|
+
|
|
644
|
+
return [];
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function buildCodexStopEvents(payload, source) {
|
|
648
|
+
const cwd = payload.cwd || process.cwd();
|
|
649
|
+
const repoRoot = findGitRepoRoot(cwd);
|
|
650
|
+
const snapshotPath = getCodexSnapshotFilePath(payload, repoRoot);
|
|
651
|
+
if (!repoRoot || !snapshotPath) {
|
|
652
|
+
return [];
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
let snapshot = null;
|
|
656
|
+
if (fs.existsSync(snapshotPath)) {
|
|
657
|
+
try {
|
|
658
|
+
snapshot = parseJson(fs.readFileSync(snapshotPath, 'utf8'), 'Codex snapshot');
|
|
659
|
+
} catch {
|
|
660
|
+
snapshot = null;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const baseline = snapshot && snapshot.baseline && typeof snapshot.baseline === 'object'
|
|
665
|
+
? snapshot.baseline
|
|
666
|
+
: {};
|
|
667
|
+
const currentEntries = parseGitStatus(repoRoot);
|
|
668
|
+
const currentPaths = new Set(currentEntries.map((entry) => entry.path));
|
|
669
|
+
const baseRef = snapshot && typeof snapshot.head === 'string' && snapshot.head.trim()
|
|
670
|
+
? snapshot.head.trim()
|
|
671
|
+
: 'HEAD';
|
|
672
|
+
const currentHead = readCurrentHead(repoRoot) || 'HEAD';
|
|
673
|
+
const committedPaths = new Set(parseGitDiffNameOnly(repoRoot, baseRef, currentHead));
|
|
674
|
+
const allPaths = new Set([
|
|
675
|
+
...Object.keys(baseline),
|
|
676
|
+
...currentPaths,
|
|
677
|
+
...committedPaths,
|
|
678
|
+
]);
|
|
679
|
+
const workerId = resolveWorkerId(repoRoot);
|
|
680
|
+
const git = resolveGitMetadata(repoRoot);
|
|
681
|
+
const events = [];
|
|
682
|
+
|
|
683
|
+
for (const repoRelativePath of allPaths) {
|
|
684
|
+
const absolutePath = path.join(repoRoot, repoRelativePath);
|
|
685
|
+
const beforeState = baseline[repoRelativePath] || readGitRefFileState(repoRoot, baseRef, repoRelativePath);
|
|
686
|
+
let afterState = readFileState(absolutePath);
|
|
687
|
+
if (!afterState.exists && committedPaths.has(repoRelativePath)) {
|
|
688
|
+
afterState = readGitRefFileState(repoRoot, currentHead, repoRelativePath);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (statesEqual(beforeState, afterState)) {
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const changeInfo = buildCodexChange(beforeState, afterState);
|
|
696
|
+
events.push({
|
|
697
|
+
event_id: randomUUID(),
|
|
698
|
+
timestamp: new Date().toISOString(),
|
|
699
|
+
source,
|
|
700
|
+
worker_id: workerId,
|
|
701
|
+
git,
|
|
702
|
+
file: {
|
|
703
|
+
path: repoRelativePath,
|
|
704
|
+
operation: changeInfo.operation,
|
|
705
|
+
},
|
|
706
|
+
changes: changeInfo.changes,
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (fs.existsSync(snapshotPath)) {
|
|
711
|
+
fs.rmSync(snapshotPath, { force: true });
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
return events;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function buildCodexEvents(payload, source) {
|
|
718
|
+
const hookEvent = payload.hook_event_name || payload.hookEvent;
|
|
719
|
+
if (!CODEX_SUPPORTED_HOOK_EVENTS.has(hookEvent)) {
|
|
720
|
+
return [];
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (hookEvent === 'UserPromptSubmit') {
|
|
724
|
+
return handleCodexUserPromptSubmit(payload);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
return buildCodexStopEvents(payload, source);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function loadRequestConfig() {
|
|
731
|
+
const url = process.env.HOOK_REPORT_URL || process.env.CHANGE_REPORT_URL || DEFAULT_HOOK_REPORT_URL;
|
|
732
|
+
const timeoutMs = Number(process.env.HOOK_REPORT_TIMEOUT_MS || DEFAULT_TIMEOUT_MS);
|
|
733
|
+
const token = process.env.HOOK_REPORT_TOKEN || '';
|
|
734
|
+
|
|
735
|
+
let extraHeaders = {};
|
|
736
|
+
if (process.env.HOOK_REPORT_HEADERS) {
|
|
737
|
+
extraHeaders = parseJson(process.env.HOOK_REPORT_HEADERS, 'HOOK_REPORT_HEADERS');
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return { url, timeoutMs, token, extraHeaders };
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function formatDateStamp(timestamp) {
|
|
744
|
+
const iso = String(timestamp || '');
|
|
745
|
+
const match = iso.match(/^(\d{4})-(\d{2})-(\d{2})/);
|
|
746
|
+
if (match) {
|
|
747
|
+
return `${match[1]}${match[2]}${match[3]}`;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const date = new Date();
|
|
751
|
+
const year = String(date.getFullYear());
|
|
752
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
753
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
754
|
+
return `${year}${month}${day}`;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function getFailedEventDir() {
|
|
758
|
+
return path.join(os.homedir(), '.ronds_ai', 'failed-events');
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function getFailedEventFilePath(source, timestamp) {
|
|
762
|
+
const fileName = `${formatDateStamp(timestamp)}-${source}-error.jsonl`;
|
|
763
|
+
return path.join(getFailedEventDir(), fileName);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function appendFailedRecord(source, record, timestamp) {
|
|
767
|
+
const failedEventDir = getFailedEventDir();
|
|
768
|
+
fs.mkdirSync(failedEventDir, { recursive: true });
|
|
769
|
+
const filePath = getFailedEventFilePath(source, timestamp);
|
|
770
|
+
fs.appendFileSync(filePath, `${JSON.stringify(record)}\n`, 'utf8');
|
|
771
|
+
return filePath;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function saveFailedEvent(event, error) {
|
|
775
|
+
const errorMessage = error instanceof Error ? error.message : String(error || '');
|
|
776
|
+
const record = {
|
|
777
|
+
timestamp: event.timestamp,
|
|
778
|
+
event_id: event.event_id,
|
|
779
|
+
source: event.source,
|
|
780
|
+
error: errorMessage,
|
|
781
|
+
event,
|
|
782
|
+
};
|
|
783
|
+
return appendFailedRecord(event.source, record, event.timestamp);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function saveCliError(error, context = {}) {
|
|
787
|
+
const timestamp = new Date().toISOString();
|
|
788
|
+
const errorMessage = error instanceof Error ? error.message : String(error || '');
|
|
789
|
+
const record = {
|
|
790
|
+
timestamp,
|
|
791
|
+
source: 'cli',
|
|
792
|
+
error: errorMessage,
|
|
793
|
+
context,
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
if (error && typeof error.rawPayload === 'string') {
|
|
797
|
+
record.raw_payload = error.rawPayload;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
return appendFailedRecord('cli', record, timestamp);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function sendJsonRequest(event, config) {
|
|
804
|
+
if (process.env.HOOK_REQUEST_DRY_RUN === '1') {
|
|
805
|
+
process.stdout.write(`${JSON.stringify(event, null, 2)}\n`);
|
|
806
|
+
return Promise.resolve({ statusCode: 0, body: 'dry-run', dryRun: true });
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
if (!config.url) {
|
|
810
|
+
return Promise.reject(new Error('Missing request URL'));
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const target = new URL(config.url);
|
|
814
|
+
const body = JSON.stringify(event);
|
|
815
|
+
const transport = target.protocol === 'https:' ? https : http;
|
|
816
|
+
const headers = {
|
|
817
|
+
'content-type': 'application/json',
|
|
818
|
+
'content-length': Buffer.byteLength(body),
|
|
819
|
+
...config.extraHeaders,
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
if (config.token) {
|
|
823
|
+
headers.authorization = `Bearer ${config.token}`;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
return new Promise((resolve, reject) => {
|
|
827
|
+
const request = transport.request({
|
|
828
|
+
protocol: target.protocol,
|
|
829
|
+
hostname: target.hostname,
|
|
830
|
+
port: target.port || undefined,
|
|
831
|
+
path: `${target.pathname}${target.search}`,
|
|
832
|
+
method: 'POST',
|
|
833
|
+
headers,
|
|
834
|
+
timeout: config.timeoutMs,
|
|
835
|
+
}, (response) => {
|
|
836
|
+
let responseBody = '';
|
|
837
|
+
response.setEncoding('utf8');
|
|
838
|
+
response.on('data', (chunk) => {
|
|
839
|
+
responseBody += chunk;
|
|
840
|
+
});
|
|
841
|
+
response.on('end', () => {
|
|
842
|
+
const statusCode = response.statusCode || 0;
|
|
843
|
+
if (statusCode >= 200 && statusCode < 300) {
|
|
844
|
+
resolve({ statusCode, body: responseBody });
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
reject(new Error(`Request failed with status ${statusCode}: ${responseBody}`));
|
|
849
|
+
});
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
request.on('timeout', () => {
|
|
853
|
+
request.destroy(new Error(`Request timed out after ${config.timeoutMs}ms`));
|
|
854
|
+
});
|
|
855
|
+
request.on('error', reject);
|
|
856
|
+
request.write(body);
|
|
857
|
+
request.end();
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function buildEvent(payload, source) {
|
|
862
|
+
if (source === 'claude') {
|
|
863
|
+
return buildClaudeEvent(payload, source);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if (source === 'cursor') {
|
|
867
|
+
return buildCursorEvent(payload, source);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
throw new Error(`Unsupported source: ${source}`);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function buildEvents(payload, source) {
|
|
874
|
+
if (source === 'codex') {
|
|
875
|
+
return buildCodexEvents(payload, source);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const singleEvent = buildEvent(payload, source);
|
|
879
|
+
return singleEvent ? [singleEvent] : [];
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
async function runCodeRecord(source) {
|
|
883
|
+
const raw = (await readStdin()).trim();
|
|
884
|
+
if (!raw) {
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const payload = parseJson(raw, 'Hook payload');
|
|
889
|
+
const events = buildEvents(payload, source);
|
|
890
|
+
const hookEvent = payload.hook_event_name || payload.hookEvent;
|
|
891
|
+
const config = loadRequestConfig();
|
|
892
|
+
|
|
893
|
+
const results = [];
|
|
894
|
+
for (const event of events) {
|
|
895
|
+
try {
|
|
896
|
+
const result = await sendJsonRequest(event, config);
|
|
897
|
+
results.push({
|
|
898
|
+
status: result.dryRun ? 'dry-run' : 'sent',
|
|
899
|
+
event_id: event.event_id,
|
|
900
|
+
response_status: result.statusCode,
|
|
901
|
+
});
|
|
902
|
+
} catch (error) {
|
|
903
|
+
const savedPath = saveFailedEvent(event, error);
|
|
904
|
+
results.push({
|
|
905
|
+
status: 'saved',
|
|
906
|
+
event_id: event.event_id,
|
|
907
|
+
saved_path: savedPath,
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
if (source === 'codex' && hookEvent === 'Stop') {
|
|
913
|
+
process.stdout.write(`${JSON.stringify(CODEX_STOP_OUTPUT)}\n`);
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
for (const result of results) {
|
|
918
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
module.exports = {
|
|
923
|
+
runCodeRecord,
|
|
924
|
+
saveCliError,
|
|
925
|
+
};
|