mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 18:02:17 +03:00
Compare commits
2699 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a5d1c1479 | ||
|
|
75d9a83c25 | ||
|
|
ebe0b6607c | ||
|
|
c8a20b1107 | ||
|
|
34e62d8725 | ||
|
|
1ea91b6c71 | ||
|
|
5abaa7e0f4 | ||
|
|
f702cbbd38 | ||
|
|
91b6983564 | ||
|
|
a224bf6530 | ||
|
|
39526b2b8c | ||
|
|
a8cfe243e1 | ||
|
|
a3ae2c422d | ||
|
|
87175d6c16 | ||
|
|
527ab764dd | ||
|
|
460e4e733f | ||
|
|
b50dfb98bc | ||
|
|
f5073604b3 | ||
|
|
0e6af20c2a | ||
|
|
428dfcdf2d | ||
|
|
b654a1ebe7 | ||
|
|
50f5d95a2f | ||
|
|
c5f15f6725 | ||
|
|
c5458e4c08 | ||
|
|
0912ade9e1 | ||
|
|
249b0ce759 | ||
|
|
29c2f1bdc2 | ||
|
|
dd790c38a2 | ||
|
|
6248699ce5 | ||
|
|
5735b4d4db | ||
|
|
78c344b3a2 | ||
|
|
cd9d684960 | ||
|
|
8536593bdc | ||
|
|
7dfcd0a4fd | ||
|
|
a9cef6dbad | ||
|
|
ae94b268f0 | ||
|
|
8bd125ed2f | ||
|
|
ec23a0461d | ||
|
|
fbf37ae0da | ||
|
|
3ff3e3dd15 | ||
|
|
49fe356b91 | ||
|
|
5ef3482254 | ||
|
|
8a44ed57d7 | ||
|
|
6e1105e2c2 | ||
|
|
c0c2efff97 | ||
|
|
546d7e95da | ||
|
|
42e1466cf4 | ||
|
|
16277bd6df | ||
|
|
55913d1c42 | ||
|
|
f8d9873e27 | ||
|
|
8d34f4c650 | ||
|
|
5f37f0f804 | ||
|
|
ef800eee40 | ||
|
|
29c5a03efe | ||
|
|
b9af4553cd | ||
|
|
61683e0c3d | ||
|
|
db674c8be0 | ||
|
|
5040c8b254 | ||
|
|
bfe90c0d0d | ||
|
|
4bc6b33f16 | ||
|
|
3b9cb7d568 | ||
|
|
e7eb777969 | ||
|
|
8b8bb3da1b | ||
|
|
e57cf437db | ||
|
|
6756006b4f | ||
|
|
3355920db9 | ||
|
|
f04c02c221 | ||
|
|
ae07f437b1 | ||
|
|
2ae523b12c | ||
|
|
6959e067fa | ||
|
|
81fb3f50e8 | ||
|
|
4b2e9b780a | ||
|
|
ca42874098 | ||
|
|
266dd038f1 | ||
|
|
6f6837d070 | ||
|
|
7c11b952a7 | ||
|
|
2e3b6d0bc6 | ||
|
|
639061ac2f | ||
|
|
1110ec9d37 | ||
|
|
d3c187a62f | ||
|
|
48c2d675fb | ||
|
|
f74d276e50 | ||
|
|
48235a3c66 | ||
|
|
3fbec5065e | ||
|
|
8cca3630ae | ||
|
|
22400a4f86 | ||
|
|
6b05fb7906 | ||
|
|
b34dc46bd0 | ||
|
|
b9c78d192a | ||
|
|
4b3009ad7d | ||
|
|
cfd2e19267 | ||
|
|
7ba05fdae5 | ||
|
|
3957c4d76b | ||
|
|
5fcccc7364 | ||
|
|
70d55664ee | ||
|
|
1410c50fa1 | ||
|
|
e1cde147df | ||
|
|
2ebc84ea77 | ||
|
|
61de0c709a | ||
|
|
e463d7945f | ||
|
|
10c8f32bd9 | ||
|
|
cfa948fd9a | ||
|
|
6fcc99ba26 | ||
|
|
65105feeaf | ||
|
|
205ef64ac4 | ||
|
|
595e9e3b1f | ||
|
|
12b34d4a93 | ||
|
|
fec6164e92 | ||
|
|
c24b0f9569 | ||
|
|
0de964d42b | ||
|
|
f43badc3d4 | ||
|
|
3394ded6bb | ||
|
|
fa3ee31f06 | ||
|
|
3470be891e | ||
|
|
1c4d850441 | ||
|
|
62f609755a | ||
|
|
c9dc205c74 | ||
|
|
7e02b445b2 | ||
|
|
62652b1fc1 | ||
|
|
44d9abac96 | ||
|
|
eb83eaf25f | ||
|
|
0c4d50a6f5 | ||
|
|
d291834481 | ||
|
|
04d44f6262 | ||
|
|
72900bead3 | ||
|
|
5ce3f7f4d6 | ||
|
|
985973b35a | ||
|
|
5098d8fd7e | ||
|
|
b7316d56e8 | ||
|
|
5ed96f5072 | ||
|
|
85a4bacf31 | ||
|
|
6401faf9f0 | ||
|
|
32b3549425 | ||
|
|
55160bc52a | ||
|
|
57354ac6d7 | ||
|
|
0c44185d0d | ||
|
|
e50126e639 | ||
|
|
531c9de8ca | ||
|
|
b17fd87470 | ||
|
|
f2e368830a | ||
|
|
06b34cc12c | ||
|
|
400dbc386a | ||
|
|
775c87bb8f | ||
|
|
33928afec0 | ||
|
|
9c5b75e3bf | ||
|
|
d62b128144 | ||
|
|
fe3ab7a836 | ||
|
|
2af324f6ee | ||
|
|
5da9b1b778 | ||
|
|
b464946b1b | ||
|
|
c4fe3d63be | ||
|
|
dd24571949 | ||
|
|
41b7f13b27 | ||
|
|
6c488d6d2a | ||
|
|
2bbd0fff45 | ||
|
|
1dae73cfef | ||
|
|
f67c7d9383 | ||
|
|
0b2085db83 | ||
|
|
b9126e51f7 | ||
|
|
1e709fdef1 | ||
|
|
855dc86ea8 | ||
|
|
b3cb5b68d8 | ||
|
|
75c1d2ead2 | ||
|
|
877aafdb99 | ||
|
|
3786e929d6 | ||
|
|
c6a51605ce | ||
|
|
09fb5cdf34 | ||
|
|
4eb5ba5fb8 | ||
|
|
f6364427ce | ||
|
|
7a40e0f547 | ||
|
|
6d4ee4f85a | ||
|
|
749b945fdf | ||
|
|
d6d585e200 | ||
|
|
db8a2dc735 | ||
|
|
9b8dc4d6db | ||
|
|
1be18f5530 | ||
|
|
193e7a6417 | ||
|
|
b191173ae1 | ||
|
|
1f27c344d4 | ||
|
|
aefd9bbbc7 | ||
|
|
c2451038a0 | ||
|
|
749197466f | ||
|
|
0e2d04526c | ||
|
|
696e16b361 | ||
|
|
0f15797e01 | ||
|
|
891a9e4125 | ||
|
|
9c69a20ea5 | ||
|
|
3748b0238d | ||
|
|
6e6cb5a0b5 | ||
|
|
3227c9ffe3 | ||
|
|
cf0006fd7b | ||
|
|
25f3fe1ac5 | ||
|
|
072e38e552 | ||
|
|
8a6d681c15 | ||
|
|
de5434a0a6 | ||
|
|
634f50a04e | ||
|
|
02564d5a5b | ||
|
|
fc9f8d91f7 | ||
|
|
a45d9190db | ||
|
|
ca8f492240 | ||
|
|
84cde3d009 | ||
|
|
f57dcba59f | ||
|
|
b2fe307128 | ||
|
|
6fa745efcb | ||
|
|
440ca8e3cc | ||
|
|
43d4ea092c | ||
|
|
ee2a698dcb | ||
|
|
317d146302 | ||
|
|
a4a870cda3 | ||
|
|
2b624b1bf3 | ||
|
|
cf3262aee8 | ||
|
|
926ff2b5db | ||
|
|
5a7df8ac29 | ||
|
|
7fdcba9e05 | ||
|
|
42011abc69 | ||
|
|
fd28e772a8 | ||
|
|
8a23c5527e | ||
|
|
13ca2cc62a | ||
|
|
8afaca4b9f | ||
|
|
3757e6dc30 | ||
|
|
801f3c9c22 | ||
|
|
8669bf69ec | ||
|
|
debf7cb283 | ||
|
|
7e9efe7cb0 | ||
|
|
d7dcd233a2 | ||
|
|
b83d1a0fc8 | ||
|
|
08f03a0fa6 | ||
|
|
56b0ea91c9 | ||
|
|
bbfd3865a5 | ||
|
|
dae0501d75 | ||
|
|
8eb721ec31 | ||
|
|
ebef1648be | ||
|
|
1640530ec8 | ||
|
|
4913439d91 | ||
|
|
789a263967 | ||
|
|
f224b9f104 | ||
|
|
f80de415ec | ||
|
|
ec138c6fee | ||
|
|
b3b006b630 | ||
|
|
04335c5a6b | ||
|
|
c15ea65a26 | ||
|
|
2af6923e6e | ||
|
|
9ccb7b1c1e | ||
|
|
51918cb5d4 | ||
|
|
91af593bb2 | ||
|
|
9efad44fc9 | ||
|
|
367497958f | ||
|
|
b3baa0e9f4 | ||
|
|
56d6ad604c | ||
|
|
52222aaf76 | ||
|
|
124ed82f02 | ||
|
|
50ad3b0e22 | ||
|
|
beb43a8bea | ||
|
|
3046705c22 | ||
|
|
5848fe3948 | ||
|
|
718637c5cd | ||
|
|
132658d009 | ||
|
|
06bdc31a50 | ||
|
|
6a51b96a47 | ||
|
|
c9c6c63216 | ||
|
|
dc6c276992 | ||
|
|
8b555d7ee6 | ||
|
|
feb263ff4d | ||
|
|
a0d766de8a | ||
|
|
00e19f3941 | ||
|
|
dc6e7b00d0 | ||
|
|
5c41961351 | ||
|
|
72afecffeb | ||
|
|
5221a81a75 | ||
|
|
8db3fec05a | ||
|
|
baefcd06f0 | ||
|
|
b060ebb05b | ||
|
|
acc9a8780d | ||
|
|
cfc6be6a12 | ||
|
|
15d20b7b59 | ||
|
|
35cb91c3a9 | ||
|
|
da8218856b | ||
|
|
523f674fff | ||
|
|
7b73ac47da | ||
|
|
07f3b71fc9 | ||
|
|
a5f33017fd | ||
|
|
8e5c1d9ead | ||
|
|
a045ddca56 | ||
|
|
1daf15efbd | ||
|
|
93526e3d9c | ||
|
|
b653cfd160 | ||
|
|
79e19b5466 | ||
|
|
b39d0d8861 | ||
|
|
3975d2c10f | ||
|
|
58c2cfccd9 | ||
|
|
291c1ffaf8 | ||
|
|
68ca8bf1e9 | ||
|
|
4f01a8995b | ||
|
|
fa1d1fe7eb | ||
|
|
a6af54a047 | ||
|
|
4fde71a4b1 | ||
|
|
149e901514 | ||
|
|
69f0735c7a | ||
|
|
0f656571d5 | ||
|
|
875c8f77c1 | ||
|
|
79d03575ee | ||
|
|
4d000f1eb0 | ||
|
|
d0d8638a02 | ||
|
|
0d09858526 | ||
|
|
855eeb3d2d | ||
|
|
0edf90bb5a | ||
|
|
634b4fe0ce | ||
|
|
bb0ec76f24 | ||
|
|
1c949c248b | ||
|
|
9a9561f630 | ||
|
|
0cc6fec85e | ||
|
|
eba07d8918 | ||
|
|
f320caa724 | ||
|
|
5e949276d4 | ||
|
|
69a2b27a33 | ||
|
|
09db1129a1 | ||
|
|
e3e962dbda | ||
|
|
2faf3608b2 | ||
|
|
dcc21f1052 | ||
|
|
8a471e712c | ||
|
|
d8445caf90 | ||
|
|
d577759002 | ||
|
|
956208c251 | ||
|
|
1dd17260ac | ||
|
|
b3e5ee3333 | ||
|
|
cd62899f31 | ||
|
|
d28d756e80 | ||
|
|
79438ef391 | ||
|
|
59128b6742 | ||
|
|
b3cfac3c14 | ||
|
|
5072b82e93 | ||
|
|
27f7e5c4fe | ||
|
|
b329cfc84a | ||
|
|
151528735b | ||
|
|
ed19c824c0 | ||
|
|
47de3bf60f | ||
|
|
4a84ab9c1b | ||
|
|
133dd0026d | ||
|
|
4f80be1f2f | ||
|
|
fe6cffb54b | ||
|
|
76c20240f1 | ||
|
|
96e528e3e2 | ||
|
|
853c9574e1 | ||
|
|
7a2682efb5 | ||
|
|
23c10916e0 | ||
|
|
7648e4b16e | ||
|
|
2f2583a02f | ||
|
|
fc45ff1bdd | ||
|
|
145fcae0e9 | ||
|
|
f4370ca15f | ||
|
|
92b2f20d32 | ||
|
|
931024eb5a | ||
|
|
761ff3e781 | ||
|
|
54be51a664 | ||
|
|
379e0bbd3a | ||
|
|
582e89840d | ||
|
|
d1880f1d4a | ||
|
|
3cfba85461 | ||
|
|
f79ab2c3d1 | ||
|
|
55659d92be | ||
|
|
23b1bd1ffe | ||
|
|
b8707dbbb4 | ||
|
|
76362c4efe | ||
|
|
8c48abc40c | ||
|
|
28629bf7f0 | ||
|
|
2468ebf8f7 | ||
|
|
33ad5012c6 | ||
|
|
6e392932c2 | ||
|
|
ba14064ea4 | ||
|
|
302ea853d5 | ||
|
|
85489a0295 | ||
|
|
993cd32829 | ||
|
|
18e5bf26a6 | ||
|
|
7c128b0f4e | ||
|
|
685954c0dd | ||
|
|
6c6e8d3f2f | ||
|
|
a5fa94bdcb | ||
|
|
6b63aa3948 | ||
|
|
8f915b18b0 | ||
|
|
0bb1ae7240 | ||
|
|
9da0e704a7 | ||
|
|
af80efe75a | ||
|
|
ab911265ed | ||
|
|
162e2f4b98 | ||
|
|
22d27ca273 | ||
|
|
8dcc21476b | ||
|
|
1887483c0f | ||
|
|
7fb5505b12 | ||
|
|
77a4429bf4 | ||
|
|
4ed8e4e673 | ||
|
|
31031422d3 | ||
|
|
2e494f8f07 | ||
|
|
e007fc11aa | ||
|
|
e7a4ea8c7f | ||
|
|
cfc83e2fd8 | ||
|
|
bd292b1e80 | ||
|
|
c6b269a4d5 | ||
|
|
aa0e312d8a | ||
|
|
3ce114af44 | ||
|
|
24b2e77aae | ||
|
|
955049186f | ||
|
|
4fe2ef8887 | ||
|
|
4b1e57443a | ||
|
|
d9b85704e4 | ||
|
|
dd06e64207 | ||
|
|
d9c2c13851 | ||
|
|
f3f1f9f36e | ||
|
|
67f713d3a5 | ||
|
|
4a31a795b9 | ||
|
|
83f242fa4d | ||
|
|
2f794d92a4 | ||
|
|
e9904f1394 | ||
|
|
eff7de2284 | ||
|
|
bbf3ba0138 | ||
|
|
d2b413c9ab | ||
|
|
ed1993d5bf | ||
|
|
1d21eb1686 | ||
|
|
e1a6ecf238 | ||
|
|
57a80b6c1a | ||
|
|
7210a73e1f | ||
|
|
f63f29830f | ||
|
|
e84e1b41bf | ||
|
|
5ce332d2ee | ||
|
|
7ab68365ba | ||
|
|
190e273d5b | ||
|
|
242c50cb0c | ||
|
|
f56485f3cf | ||
|
|
037f4e8d50 | ||
|
|
e244fd51d4 | ||
|
|
7c89858797 | ||
|
|
871f0520bb | ||
|
|
1a39c31ff4 | ||
|
|
153a421354 | ||
|
|
18ab2e9259 | ||
|
|
c6f5b394f8 | ||
|
|
c49d817a68 | ||
|
|
26758b3ed9 | ||
|
|
678dee2ede | ||
|
|
da939ca2ba | ||
|
|
6c976a6b68 | ||
|
|
c2bf5c7db5 | ||
|
|
4726dea901 | ||
|
|
72dd7c9b49 | ||
|
|
18ef28ea77 | ||
|
|
35a55d73f3 | ||
|
|
fe9efd2191 | ||
|
|
bcbc095798 | ||
|
|
52f3285dcd | ||
|
|
ebed308fbb | ||
|
|
52285d8a7a | ||
|
|
1b14b5b012 | ||
|
|
2d601ea459 | ||
|
|
831f64ed38 | ||
|
|
0c3d33899d | ||
|
|
0caca472d4 | ||
|
|
ce746d4f5b | ||
|
|
b9db934e39 | ||
|
|
bf83aa55de | ||
|
|
22f2c033da | ||
|
|
30130d6c2c | ||
|
|
44a04df4f6 | ||
|
|
253f5e5904 | ||
|
|
e2b4c2b06e | ||
|
|
9ae31e7f05 | ||
|
|
bbdcb97a06 | ||
|
|
29b9f27919 | ||
|
|
46df8470a9 | ||
|
|
8c4d726c7b | ||
|
|
81928cccc5 | ||
|
|
14884568a9 | ||
|
|
788f8e1794 | ||
|
|
6a0b19c535 | ||
|
|
2eb4b1ac3b | ||
|
|
8b93dd5583 | ||
|
|
a249724777 | ||
|
|
078f687592 | ||
|
|
6f93d81e58 | ||
|
|
e584fe9e33 | ||
|
|
7d0e51a7a7 | ||
|
|
799959432b | ||
|
|
9fcc8648e9 | ||
|
|
91b457a802 | ||
|
|
e022335b60 | ||
|
|
b23127e53c | ||
|
|
52f29f2347 | ||
|
|
acf6b93df2 | ||
|
|
3796fc0925 | ||
|
|
8aa2c9461a | ||
|
|
0ccc1f0d60 | ||
|
|
9668b5fd34 | ||
|
|
9638a88145 | ||
|
|
caa262a4c5 | ||
|
|
8a26128e93 | ||
|
|
5592b25243 | ||
|
|
7b97b01fe6 | ||
|
|
fd65db99c8 | ||
|
|
352b87bd30 | ||
|
|
31b1985c64 | ||
|
|
fb963154ea | ||
|
|
55111efcb5 | ||
|
|
399b9f8d9d | ||
|
|
ae3d73a2e7 | ||
|
|
dfe9a3e288 | ||
|
|
58b70de654 | ||
|
|
afeee02e4a | ||
|
|
c86f60b1d3 | ||
|
|
eb62ad72f1 | ||
|
|
4968de9405 | ||
|
|
afe2a67c76 | ||
|
|
6991024935 | ||
|
|
82784a6f41 | ||
|
|
ee97583e02 | ||
|
|
519fdf41b8 | ||
|
|
675668c430 | ||
|
|
2c7af9fc45 | ||
|
|
8a1d9beb55 | ||
|
|
c044cfdefb | ||
|
|
ac39badc2e | ||
|
|
eb02fda327 | ||
|
|
b4665fc852 | ||
|
|
b43ab4d4c3 | ||
|
|
25f794affa | ||
|
|
46932961d4 | ||
|
|
a7a42140a0 | ||
|
|
0a800d87e1 | ||
|
|
ddf1ba21b3 | ||
|
|
f3b944a55a | ||
|
|
6f2b360ce8 | ||
|
|
ca6916a867 | ||
|
|
918a539baf | ||
|
|
26d6b7a76f | ||
|
|
4437b0040e | ||
|
|
20cb648ea9 | ||
|
|
204e15628c | ||
|
|
46e5aa5522 | ||
|
|
c89663d774 | ||
|
|
20b35c4d20 | ||
|
|
6fa2d5e84f | ||
|
|
dbe9d84649 | ||
|
|
9e49baefa4 | ||
|
|
75b979282f | ||
|
|
e354d942e3 | ||
|
|
c53587ef2d | ||
|
|
d94b6b5012 | ||
|
|
15d6fc35da | ||
|
|
da982d31be | ||
|
|
a260795327 | ||
|
|
4f20fbc36c | ||
|
|
4ac6e58360 | ||
|
|
fc620514a9 | ||
|
|
e6aee3d26c | ||
|
|
061c0c29fe | ||
|
|
af7d056f7e | ||
|
|
a408b30896 | ||
|
|
13f7ebce5a | ||
|
|
5c11b57542 | ||
|
|
340f68bbee | ||
|
|
b23b624f82 | ||
|
|
607ae5ca5e | ||
|
|
007e19fe30 | ||
|
|
5455bf9b77 | ||
|
|
a6d4c012ae | ||
|
|
4f87062f3e | ||
|
|
be76707452 | ||
|
|
08b95863a2 | ||
|
|
ed1554b7a1 | ||
|
|
dd51a261e0 | ||
|
|
ff730b372e | ||
|
|
08d88d40a5 | ||
|
|
95944dad92 | ||
|
|
2638c92fba | ||
|
|
41946c44c9 | ||
|
|
704f686396 | ||
|
|
5c4c0a94ff | ||
|
|
191bcff0ac | ||
|
|
dcb32a6ba0 | ||
|
|
0ffbbd631e | ||
|
|
b3fe7ddcb1 | ||
|
|
d8277a4ba0 | ||
|
|
e5974c4ae3 | ||
|
|
49b8f9b911 | ||
|
|
25199b20fe | ||
|
|
5da471bf15 | ||
|
|
d995713e21 | ||
|
|
f188e76b8c | ||
|
|
6d722ecd27 | ||
|
|
90cf685165 | ||
|
|
c52b365e1d | ||
|
|
a14802fe2c | ||
|
|
2777c0541c | ||
|
|
51c4e1cdc2 | ||
|
|
f946f6e0a7 | ||
|
|
52143d2c8e | ||
|
|
0594b1d2ea | ||
|
|
5925f313f9 | ||
|
|
78b084502a | ||
|
|
c1b004c74e | ||
|
|
8b1da7b92c | ||
|
|
992848431b | ||
|
|
c83792a8cd | ||
|
|
83f25922b0 | ||
|
|
05212b0a3f | ||
|
|
4ee9ba9766 | ||
|
|
970e14725b | ||
|
|
ef1022e9c8 | ||
|
|
fa3b6d18ac | ||
|
|
7395d71e1d | ||
|
|
21ca713af4 | ||
|
|
08de5a1bbb | ||
|
|
fea8e5a1e9 | ||
|
|
f632da7943 | ||
|
|
2880155772 | ||
|
|
e4c8c14fb3 | ||
|
|
d3e0353203 | ||
|
|
6fc66eaba7 | ||
|
|
6e2c5e2d4c | ||
|
|
fca89049c9 | ||
|
|
1814d2bde0 | ||
|
|
fdb4c63244 | ||
|
|
19c15a5139 | ||
|
|
bbecbccb0a | ||
|
|
66d5808d4a | ||
|
|
360a97b654 | ||
|
|
66d5d40a7d | ||
|
|
7d8b783195 | ||
|
|
966e411ecb | ||
|
|
602f897c50 | ||
|
|
67af6c303f | ||
|
|
c5967381d6 | ||
|
|
4e53fba8b9 | ||
|
|
0f748cb732 | ||
|
|
f8812b95c7 | ||
|
|
fa1295d3cf | ||
|
|
8b3f170b38 | ||
|
|
110fa35d04 | ||
|
|
ee408653dd | ||
|
|
82a5bb3778 | ||
|
|
40bd60959e | ||
|
|
9e7cb90a01 | ||
|
|
bff9a0c4a6 | ||
|
|
9f79a6bb94 | ||
|
|
5caee79f43 | ||
|
|
72fb5460d0 | ||
|
|
cb489d155c | ||
|
|
c061f347f2 | ||
|
|
ed19e0f43e | ||
|
|
dc8dc941e8 | ||
|
|
87dfbcca62 | ||
|
|
ec6456ba73 | ||
|
|
e58aea9df7 | ||
|
|
4aea2c8b30 | ||
|
|
f7279b3e19 | ||
|
|
fbb4dfaf37 | ||
|
|
e1ab7c9273 | ||
|
|
88e03caff1 | ||
|
|
8e4d28097a | ||
|
|
9ddcd8bda8 | ||
|
|
c14e43a52f | ||
|
|
f8322b3bd7 | ||
|
|
ee228e6657 | ||
|
|
4ea2a96e13 | ||
|
|
5d006f7bee | ||
|
|
2b83552668 | ||
|
|
61f5866ecc | ||
|
|
7c569e9cfe | ||
|
|
86db9bcf47 | ||
|
|
8bcbbcdfcb | ||
|
|
1966215b92 | ||
|
|
fa07fbedbc | ||
|
|
1c7d002031 | ||
|
|
a3e9934fa0 | ||
|
|
35c6db05bf | ||
|
|
12b254097b | ||
|
|
58e5ce3900 | ||
|
|
0da32bdfec | ||
|
|
883317e58c | ||
|
|
4c9fe12832 | ||
|
|
cc79237af7 | ||
|
|
01aafd348c | ||
|
|
e0928f6b37 | ||
|
|
73bda23c60 | ||
|
|
5731541bad | ||
|
|
75f4343881 | ||
|
|
abf7a3d5e3 | ||
|
|
6eee061c0e | ||
|
|
887926e0ad | ||
|
|
7e13bd36f5 | ||
|
|
9d663db3f0 | ||
|
|
bc941d3dd9 | ||
|
|
fa29e19863 | ||
|
|
3d75fb3fae | ||
|
|
7d6854e925 | ||
|
|
a8106bbadd | ||
|
|
73fc6e3ca6 | ||
|
|
503446c463 | ||
|
|
d06d1ae49c | ||
|
|
09733e4906 | ||
|
|
149d13cb9c | ||
|
|
7f0da3d0b2 | ||
|
|
75008d8098 | ||
|
|
bfb5ab0f58 | ||
|
|
f5e155b7c8 | ||
|
|
cdd71ab211 | ||
|
|
4a6865650d | ||
|
|
4f202f3fa5 | ||
|
|
043d345647 | ||
|
|
862a9c2c89 | ||
|
|
e38333e627 | ||
|
|
9f24404ab8 | ||
|
|
3de0c84d1d | ||
|
|
ad2600b4d0 | ||
|
|
7b80e80a1e | ||
|
|
32f3f3d94f | ||
|
|
276e5da137 | ||
|
|
a52c9ceb45 | ||
|
|
a21aa1f53c | ||
|
|
aacd43bb45 | ||
|
|
e26f79f052 | ||
|
|
13ce9d69cb | ||
|
|
831829dbc3 | ||
|
|
deb2180c9b | ||
|
|
60bb00be41 | ||
|
|
a47bb90388 | ||
|
|
bc6d0a3641 | ||
|
|
39ae0314b6 | ||
|
|
dc94613e6a | ||
|
|
56ccf4f8dd | ||
|
|
ad966b15f2 | ||
|
|
640ed6d2bc | ||
|
|
43553646ed | ||
|
|
4331e129ab | ||
|
|
ef23e702af | ||
|
|
80d52d9a77 | ||
|
|
fc84e5a34a | ||
|
|
962faa84e9 | ||
|
|
afdebbc793 | ||
|
|
9875b40420 | ||
|
|
23ec2f9fa8 | ||
|
|
3662f90095 | ||
|
|
1e80366b42 | ||
|
|
9d3eb480a2 | ||
|
|
1929b44235 | ||
|
|
f09c2d6b87 | ||
|
|
16febc0510 | ||
|
|
cababfe58a | ||
|
|
eeb836d62a | ||
|
|
31da6a09a1 | ||
|
|
e7753698c9 | ||
|
|
f1af90e97e | ||
|
|
321f6070ac | ||
|
|
57d37869c9 | ||
|
|
0e844570dc | ||
|
|
7330947ce2 | ||
|
|
72d0e1ff1b | ||
|
|
61fb2ac36d | ||
|
|
a430381434 | ||
|
|
4cd7ee1134 | ||
|
|
40cc0d116a | ||
|
|
e9d96fa3ff | ||
|
|
7214efa86e | ||
|
|
eb2579ec0a | ||
|
|
d96f0c2fda | ||
|
|
a13d2f9aff | ||
|
|
312f2f3aeb | ||
|
|
84955146d0 | ||
|
|
a7e00fbe4a | ||
|
|
b4ba8379de | ||
|
|
d1ff4a6905 | ||
|
|
3dc7542eca | ||
|
|
c5dded8992 | ||
|
|
aace2fcbd0 | ||
|
|
5c67df5508 | ||
|
|
e0613e6600 | ||
|
|
269186b9d1 | ||
|
|
76326c6497 | ||
|
|
23aa213cef | ||
|
|
ffde066951 | ||
|
|
667ce4db06 | ||
|
|
a591b4fc4b | ||
|
|
fb0361fc8c | ||
|
|
f1cd77472c | ||
|
|
770aa1b123 | ||
|
|
0ab613e57f | ||
|
|
8a9d0d3504 | ||
|
|
dda5269e77 | ||
|
|
e7b5ced09c | ||
|
|
22d562782b | ||
|
|
1064b85a7d | ||
|
|
1985af8965 | ||
|
|
d7eb92be5a | ||
|
|
90898172bf | ||
|
|
08e18867fd | ||
|
|
811a3ab577 | ||
|
|
7166d0f106 | ||
|
|
171081dbbb | ||
|
|
28b7172c0a | ||
|
|
7665ad3950 | ||
|
|
dfd83bacd1 | ||
|
|
2839ed9c20 | ||
|
|
55ae7a2409 | ||
|
|
c50c398a81 | ||
|
|
40d29cab55 | ||
|
|
eb3520ab73 | ||
|
|
e9175b2f8c | ||
|
|
cd4f4084c0 | ||
|
|
913b8b84bc | ||
|
|
fc354a44cc | ||
|
|
c737007879 | ||
|
|
c63c5d5008 | ||
|
|
d4e68cfcf9 | ||
|
|
7dab1fb731 | ||
|
|
8077e9b62b | ||
|
|
2082ffbad5 | ||
|
|
64b1a20010 | ||
|
|
3d769e6a73 | ||
|
|
afe4b19588 | ||
|
|
bf96e704ff | ||
|
|
da089b09f6 | ||
|
|
d07313333f | ||
|
|
52e031b849 | ||
|
|
bc7226ae95 | ||
|
|
0d1e332217 | ||
|
|
fe7775c384 | ||
|
|
89b740d9f7 | ||
|
|
6a45ea2c97 | ||
|
|
3f900a833e | ||
|
|
8f3d9e2ec7 | ||
|
|
5ad6df52e0 | ||
|
|
7985f6818e | ||
|
|
3b473082e5 | ||
|
|
d775dd31f5 | ||
|
|
baeaaee0f7 | ||
|
|
d4408add04 | ||
|
|
4dc959c1f2 | ||
|
|
0024d20e28 | ||
|
|
c74657f29a | ||
|
|
b048d62ad0 | ||
|
|
a1d40035df | ||
|
|
55785f5919 | ||
|
|
3e3ba607b5 | ||
|
|
a15b6964b7 | ||
|
|
223560a7ec | ||
|
|
773d71204f | ||
|
|
291ad52891 | ||
|
|
adf99811fa | ||
|
|
37058ad3d2 | ||
|
|
ec0ce5bed7 | ||
|
|
9577a8d9e8 | ||
|
|
3f063e5c52 | ||
|
|
372c887ca3 | ||
|
|
802a92e92f | ||
|
|
72e31e6329 | ||
|
|
0d5885fff0 | ||
|
|
0083d643bc | ||
|
|
3c42c9aac3 | ||
|
|
99c6dc7fd6 | ||
|
|
1a342ae5dc | ||
|
|
88972bf92a | ||
|
|
f6fdfea3ae | ||
|
|
d3c1abc6e1 | ||
|
|
b73f3610fc | ||
|
|
0c27b89c63 | ||
|
|
b8746b1464 | ||
|
|
fb2fb7c6f4 | ||
|
|
9e1d4885b5 | ||
|
|
0e8148d602 | ||
|
|
1d38900f17 | ||
|
|
9dc377e1d8 | ||
|
|
dc8b85611f | ||
|
|
d7a14cecde | ||
|
|
53fd36e4f2 | ||
|
|
879eda9379 | ||
|
|
6b7f33183c | ||
|
|
2bd8e05824 | ||
|
|
37a4a66e93 | ||
|
|
6424c82570 | ||
|
|
d8baf4434d | ||
|
|
6e38bd5393 | ||
|
|
dde74281de | ||
|
|
a7233d1bf1 | ||
|
|
970f6a3ae7 | ||
|
|
743be29852 | ||
|
|
aa40bc34f5 | ||
|
|
0f35c148ea | ||
|
|
a2d0db2a86 | ||
|
|
fe25fdcfdc | ||
|
|
f64b209750 | ||
|
|
c49929327a | ||
|
|
5aff529f0b | ||
|
|
0cf33fdaa3 | ||
|
|
476ef48fa5 | ||
|
|
4b9c129e1c | ||
|
|
b75b52eefb | ||
|
|
230f0410f7 | ||
|
|
59f4d42795 | ||
|
|
ebbd7c34c8 | ||
|
|
1e3c08565c | ||
|
|
e4beb49c6d | ||
|
|
8400dbea7d | ||
|
|
c12b7546a8 | ||
|
|
01092fa349 | ||
|
|
41ff569260 | ||
|
|
1b2a4fd659 | ||
|
|
00182afb2f | ||
|
|
a10a4bf491 | ||
|
|
399a911675 | ||
|
|
7ec572ca47 | ||
|
|
66193a2ac8 | ||
|
|
95a43597c9 | ||
|
|
3d78cd6848 | ||
|
|
e21ebaa389 | ||
|
|
d2ce20dc5c | ||
|
|
c766aa5403 | ||
|
|
35d56358b8 | ||
|
|
29c170f83a | ||
|
|
64b8194210 | ||
|
|
f8a23f41a6 | ||
|
|
cf703138f2 | ||
|
|
abe8cae083 | ||
|
|
fa950a8a49 | ||
|
|
a906bda7fc | ||
|
|
8f14452614 | ||
|
|
2666043daf | ||
|
|
20358bc8bf | ||
|
|
b44d1d9b90 | ||
|
|
eadcf43ca0 | ||
|
|
4e79d4708c | ||
|
|
a7df2b0b55 | ||
|
|
7b575f38aa | ||
|
|
f7d45ef31f | ||
|
|
85f2f8d8d8 | ||
|
|
1f9b340d13 | ||
|
|
fb95689601 | ||
|
|
d8e977cb42 | ||
|
|
89886e69a9 | ||
|
|
8ad5f0dbcc | ||
|
|
e9bb874ddc | ||
|
|
9042d5049d | ||
|
|
3a8defed09 | ||
|
|
0428943d84 | ||
|
|
373181dade | ||
|
|
f44fb3d9b7 | ||
|
|
32e0a7cb16 | ||
|
|
4ac29c4d9b | ||
|
|
3d70ad414e | ||
|
|
21f4f8ddce | ||
|
|
f3226b7f8d | ||
|
|
ea9d9e09ba | ||
|
|
ea6f556ed2 | ||
|
|
10b125115e | ||
|
|
ce6706f3e9 | ||
|
|
ec863041f9 | ||
|
|
3ff2f6a7c9 | ||
|
|
983263e45e | ||
|
|
f8c7e7904e | ||
|
|
934c133103 | ||
|
|
8ebddc2ca3 | ||
|
|
9aa89b17a5 | ||
|
|
94ba3b3a23 | ||
|
|
8ca5902a98 | ||
|
|
de4cbe2e3f | ||
|
|
fa0f3cc8be | ||
|
|
eaeab62373 | ||
|
|
7b21e04604 | ||
|
|
dc23c2c0d1 | ||
|
|
5e0d6263ed | ||
|
|
89ef91ca1b | ||
|
|
4597cfd54e | ||
|
|
595b5dc642 | ||
|
|
e0a4f703cb | ||
|
|
16f14b76a9 | ||
|
|
e9d2ebb4f9 | ||
|
|
e929559e9a | ||
|
|
6c172df547 | ||
|
|
902d0b9f41 | ||
|
|
cd27c5cf33 | ||
|
|
1b26018534 | ||
|
|
cc460b36b9 | ||
|
|
d26b92b551 | ||
|
|
3963575529 | ||
|
|
294751d8a3 | ||
|
|
3ec9ca11b1 | ||
|
|
24ffdde03d | ||
|
|
c7b7b847f0 | ||
|
|
e12814fb98 | ||
|
|
90873cd3e2 | ||
|
|
ff80f03953 | ||
|
|
ce3e4ba618 | ||
|
|
71702a086c | ||
|
|
6d6a4abb8a | ||
|
|
9e0d2f6a70 | ||
|
|
0cd388efb8 | ||
|
|
4cdd0dfd1a | ||
|
|
d7cfb626bb | ||
|
|
8053ebe798 | ||
|
|
57e55268e5 | ||
|
|
4f7a0c265d | ||
|
|
1292465a5b | ||
|
|
ae8a7d2dc5 | ||
|
|
2f1c904d74 | ||
|
|
aa535748ef | ||
|
|
42952ee42f | ||
|
|
8a0ca60e4b | ||
|
|
e6e7f8d402 | ||
|
|
edd6941de4 | ||
|
|
6bf7b9601d | ||
|
|
dc103318ab | ||
|
|
1194a1a5a5 | ||
|
|
70d7fc71b3 | ||
|
|
96fa89052b | ||
|
|
97505e200a | ||
|
|
a16e47c593 | ||
|
|
4080eb9312 | ||
|
|
c9538194aa | ||
|
|
73022ff3b3 | ||
|
|
58fb988c52 | ||
|
|
95f7c69e36 | ||
|
|
b3d928c593 | ||
|
|
ea691bfd41 | ||
|
|
00ae48f58d | ||
|
|
d5ea907d69 | ||
|
|
8100b53fd7 | ||
|
|
f23f301547 | ||
|
|
bda1f290ac | ||
|
|
2f905598e8 | ||
|
|
f769ce93cc | ||
|
|
9c9ba8f2bd | ||
|
|
67bce7721b | ||
|
|
84fbfa36c6 | ||
|
|
a46e920148 | ||
|
|
cf959c768d | ||
|
|
34b0cda024 | ||
|
|
e93721162d | ||
|
|
013e33e1a5 | ||
|
|
084f2c53ce | ||
|
|
9e198184a7 | ||
|
|
7f3dccf6dd | ||
|
|
0274af8c9c | ||
|
|
905b7555c2 | ||
|
|
b1974dac12 | ||
|
|
610b6fda3b | ||
|
|
1a5010b2c6 | ||
|
|
3ee0150367 | ||
|
|
4ecddaacd9 | ||
|
|
819c0762b9 | ||
|
|
26edd01ca3 | ||
|
|
a5e32a6d32 | ||
|
|
caeba1fd91 | ||
|
|
cd67c18049 | ||
|
|
c7074761c5 | ||
|
|
f399ece9f9 | ||
|
|
eace1dc44d | ||
|
|
4b475df5b2 | ||
|
|
c0d92f3569 | ||
|
|
88dbefa141 | ||
|
|
74a37bcf78 | ||
|
|
8a8e6ca349 | ||
|
|
ed9a7e5495 | ||
|
|
ccda2fbe44 | ||
|
|
cc07e5f7f6 | ||
|
|
31a0628cf1 | ||
|
|
18a25e4e4c | ||
|
|
778a7170a5 | ||
|
|
1c6d54ef57 | ||
|
|
da4c3660f4 | ||
|
|
665b3e2d5d | ||
|
|
b1e13668f2 | ||
|
|
b5f6bea880 | ||
|
|
5666950929 | ||
|
|
021cfd791f | ||
|
|
cdb271ea23 | ||
|
|
ba6a8602fb | ||
|
|
d4a92830be | ||
|
|
03ec5808ff | ||
|
|
6eb5d663b0 | ||
|
|
6e0b801b6a | ||
|
|
6747e22757 | ||
|
|
6dd883e5f4 | ||
|
|
4671a1eb98 | ||
|
|
845e2b3d01 | ||
|
|
bc91fb9e54 | ||
|
|
9d334c82b9 | ||
|
|
98e70a706e | ||
|
|
eec5fa3feb | ||
|
|
712f1ea3e9 | ||
|
|
388b84de3c | ||
|
|
9881e190bf | ||
|
|
4ac4ac3fd4 | ||
|
|
cf0f947adb | ||
|
|
ae77d1370e | ||
|
|
4d48454c11 | ||
|
|
c9fc36ca14 | ||
|
|
3ff6137767 | ||
|
|
cbe7358dc8 | ||
|
|
3008ba9a13 | ||
|
|
f14679f7a5 | ||
|
|
97912c7d9c | ||
|
|
725ec7f2a9 | ||
|
|
c26c3a46a9 | ||
|
|
19edb8efa4 | ||
|
|
52d5b86e88 | ||
|
|
738e108d06 | ||
|
|
78b845b68c | ||
|
|
56ae1b8246 | ||
|
|
314cad79ba | ||
|
|
4eef082cf3 | ||
|
|
acf6ad4ba0 | ||
|
|
2601e0e49d | ||
|
|
f00e9a1272 | ||
|
|
cabdfb04e7 | ||
|
|
6dcbdfd605 | ||
|
|
d60a5465db | ||
|
|
d2f32090b7 | ||
|
|
0fb535db8e | ||
|
|
f0a750c8a3 | ||
|
|
29c92c38fb | ||
|
|
cb89abc53a | ||
|
|
a71dd1aee6 | ||
|
|
fec0deb758 | ||
|
|
ff28b1a990 | ||
|
|
e30969fecd | ||
|
|
dbd4b0a7d2 | ||
|
|
71745820ed | ||
|
|
abeb78b95c | ||
|
|
4692621e4f | ||
|
|
1c86cd5fec | ||
|
|
fbf129da56 | ||
|
|
0c90c9768b | ||
|
|
dd67b25df1 | ||
|
|
accb58f5b2 | ||
|
|
d08301de8c | ||
|
|
9c9f2b9d45 | ||
|
|
05a50fcf53 | ||
|
|
744a5606e4 | ||
|
|
aa6b2f7567 | ||
|
|
167404f19e | ||
|
|
a691893413 | ||
|
|
213d38cd50 | ||
|
|
d179ace708 | ||
|
|
6df01aeb34 | ||
|
|
e30b706e66 | ||
|
|
6dc5bb9437 | ||
|
|
ead269d30d | ||
|
|
72afe8fe04 | ||
|
|
abaf2e9704 | ||
|
|
3547258282 | ||
|
|
77373ef9af | ||
|
|
a6c96257a5 | ||
|
|
091b1238da | ||
|
|
8a8fcc77a8 | ||
|
|
13495d4d13 | ||
|
|
df76aaef96 | ||
|
|
1e9e6d4349 | ||
|
|
b9c20b2b06 | ||
|
|
d9120c7b56 | ||
|
|
3dba954900 | ||
|
|
7c56918787 | ||
|
|
fa8ca5dcde | ||
|
|
94ae0f1921 | ||
|
|
f9e799d089 | ||
|
|
ce9dc8e851 | ||
|
|
604d55ed42 | ||
|
|
1beb372057 | ||
|
|
9325553ff9 | ||
|
|
17b5a8540e | ||
|
|
1f66b67309 | ||
|
|
cb4ad33703 | ||
|
|
7775c8cc50 | ||
|
|
e766fb1f13 | ||
|
|
a42da2af01 | ||
|
|
99331f777e | ||
|
|
35a1f6a529 | ||
|
|
d403186e6e | ||
|
|
50097b1fb1 | ||
|
|
3478ac6538 | ||
|
|
a5be284f1d | ||
|
|
1dd7ec91fe | ||
|
|
fc80a986f2 | ||
|
|
299793e788 | ||
|
|
8684d43ebd | ||
|
|
e4a27c41a6 | ||
|
|
43d7f0c312 | ||
|
|
9eac8c2efe | ||
|
|
e64dc3b431 | ||
|
|
872e597512 | ||
|
|
f5df0d337d | ||
|
|
eada1321ff | ||
|
|
57b926f739 | ||
|
|
f4d3cf3576 | ||
|
|
1f962a2167 | ||
|
|
0e15988233 | ||
|
|
af5d800759 | ||
|
|
1e60f0ce51 | ||
|
|
c9aef0e595 | ||
|
|
31acf52d0a | ||
|
|
5b5e21a99e | ||
|
|
7ed15c742f | ||
|
|
7d34b6a7e1 | ||
|
|
619d8c937d | ||
|
|
35c29faf99 | ||
|
|
15b4a54454 | ||
|
|
1b84c04dcd | ||
|
|
5b8e41c1b2 | ||
|
|
7073928abb | ||
|
|
76e34f093d | ||
|
|
4f6f97b369 | ||
|
|
a972293151 | ||
|
|
78531fe033 | ||
|
|
b10b724d17 | ||
|
|
80e3858ae4 | ||
|
|
c656123db9 | ||
|
|
3a0bc955be | ||
|
|
e2469b8f6d | ||
|
|
4057fe55a2 | ||
|
|
968cbfeb15 | ||
|
|
01dd72cf1f | ||
|
|
9a8404b733 | ||
|
|
47468636e4 | ||
|
|
05005acabd | ||
|
|
a42ef13b61 | ||
|
|
49a5a552a3 | ||
|
|
19bf03542c | ||
|
|
ee55ab522f | ||
|
|
bbfcd65855 | ||
|
|
25e1be8001 | ||
|
|
18f2f0446d | ||
|
|
550d15b49e | ||
|
|
4920295e3e | ||
|
|
c5dcf35281 | ||
|
|
34bdd1e8d0 | ||
|
|
1568c53faa | ||
|
|
6b86fb9966 | ||
|
|
2a58543b4d | ||
|
|
0f95330bc1 | ||
|
|
bdfcec7cf7 | ||
|
|
7388623244 | ||
|
|
fff025ca0f | ||
|
|
331e1b7d61 | ||
|
|
16b0499192 | ||
|
|
fe09fe485f | ||
|
|
d52a5af87d | ||
|
|
d539a21f69 | ||
|
|
128f242808 | ||
|
|
66bf8054f9 | ||
|
|
d99cf6350f | ||
|
|
9bd7cb7d00 | ||
|
|
f804a5f443 | ||
|
|
7549a68d6c | ||
|
|
1ef354465a | ||
|
|
a7b6dfb06e | ||
|
|
98c5b250b9 | ||
|
|
098639e146 | ||
|
|
a7d30d9c6f | ||
|
|
0937c5a8a3 | ||
|
|
56608a4654 | ||
|
|
8bb1c83479 | ||
|
|
b709dca2d6 | ||
|
|
b925be2758 | ||
|
|
2e36599d40 | ||
|
|
0d852ab3e0 | ||
|
|
495ca290e0 | ||
|
|
de1bea8227 | ||
|
|
9cd36af3e8 | ||
|
|
3502c5afd1 | ||
|
|
03769eb0ea | ||
|
|
76b0f077a4 | ||
|
|
1dae161823 | ||
|
|
25e22df2cf | ||
|
|
96d0f57558 | ||
|
|
f651a27418 | ||
|
|
3834768b72 | ||
|
|
0d86244c90 | ||
|
|
a389f2699a | ||
|
|
3478dea5de | ||
|
|
0c24ab45af | ||
|
|
cde6f56014 | ||
|
|
9754b04bd6 | ||
|
|
d19b8d83e8 | ||
|
|
496114ae8b | ||
|
|
ceca26ae87 | ||
|
|
20f04574e5 | ||
|
|
1d3d99bb9e | ||
|
|
b38e57d452 | ||
|
|
763d353f5c | ||
|
|
eaa74afb42 | ||
|
|
3245779c11 | ||
|
|
a2a7eb5630 | ||
|
|
bd5dd7cb21 | ||
|
|
9311f2a627 | ||
|
|
bfe64156bf | ||
|
|
333f0b9f2d | ||
|
|
d6cbdc1580 | ||
|
|
77f326e0dd | ||
|
|
881f59354d | ||
|
|
08d0e9f8b4 | ||
|
|
3432dfd280 | ||
|
|
5be86907d7 | ||
|
|
b191842d98 | ||
|
|
293290e12a | ||
|
|
bb1e70acab | ||
|
|
97fe1a1b57 | ||
|
|
860d596c3b | ||
|
|
b909013058 | ||
|
|
f979c606fe | ||
|
|
4a50b2eb6a | ||
|
|
f3ae67473b | ||
|
|
9d32b65a82 | ||
|
|
e57126af4f | ||
|
|
8d1c30ad17 | ||
|
|
ecab0edad1 | ||
|
|
d42842ba25 | ||
|
|
c1659a1c5e | ||
|
|
4a560c0b1c | ||
|
|
891189bbf3 | ||
|
|
01ae037205 | ||
|
|
f53caa93b6 | ||
|
|
c8679b0c79 | ||
|
|
7bc4ac1833 | ||
|
|
c03a5a4443 | ||
|
|
7e1e0e362e | ||
|
|
4a930e7966 | ||
|
|
0307950dc6 | ||
|
|
6d9ba007e5 | ||
|
|
15abfe61ec | ||
|
|
4734d53322 | ||
|
|
71b256aad5 | ||
|
|
e5c4e450c0 | ||
|
|
857b692aac | ||
|
|
738353a0e7 | ||
|
|
4a6e915ebd | ||
|
|
004ed83689 | ||
|
|
4ea123a2c0 | ||
|
|
c8828b8a42 | ||
|
|
3ae6938d1f | ||
|
|
be2ff98f25 | ||
|
|
0357a18cea | ||
|
|
e4e7bdebc6 | ||
|
|
eff2c0beb7 | ||
|
|
fceb9d4145 | ||
|
|
447c13592f | ||
|
|
0afd304949 | ||
|
|
a3d1dc6cf9 | ||
|
|
2fe67ada97 | ||
|
|
949a7a618f | ||
|
|
6034df36b8 | ||
|
|
ea67216bf2 | ||
|
|
38f66917ae | ||
|
|
1e3ac5fff7 | ||
|
|
792a1cb2ab | ||
|
|
5ead25829f | ||
|
|
8cf78ddf00 | ||
|
|
4ae488b25b | ||
|
|
dc6d9e2e4b | ||
|
|
14d18d27b1 | ||
|
|
7b51ccd9e4 | ||
|
|
ce8e9b96ca | ||
|
|
a5982579ac | ||
|
|
46c0a32357 | ||
|
|
21bccce4a1 | ||
|
|
d868124c36 | ||
|
|
bd1ead2237 | ||
|
|
689bf7fbc5 | ||
|
|
25f9a1339f | ||
|
|
bbc0a8d534 | ||
|
|
22492b5707 | ||
|
|
55da8fda74 | ||
|
|
661a63cc45 | ||
|
|
f5700f2b4c | ||
|
|
a5c258ac32 | ||
|
|
a0654b4643 | ||
|
|
adf59ddce7 | ||
|
|
438f25214c | ||
|
|
6902fa34bb | ||
|
|
5cbc08a6a2 | ||
|
|
79c63d1a4f | ||
|
|
01bd0d6760 | ||
|
|
bc7fb96184 | ||
|
|
03b8e21f23 | ||
|
|
68060d636d | ||
|
|
bf04aa3927 | ||
|
|
732a3116ff | ||
|
|
6e9b23c8e2 | ||
|
|
c4570a1387 | ||
|
|
3843751c58 | ||
|
|
ca944f280f | ||
|
|
b140181257 | ||
|
|
4eedf4d1cd | ||
|
|
37cc61e6a3 | ||
|
|
d1ca9c2d51 | ||
|
|
57395ed05c | ||
|
|
8d52a6cc7a | ||
|
|
2206fed7e4 | ||
|
|
67fc68683d | ||
|
|
fb2141382f | ||
|
|
c11c5a866d | ||
|
|
f86754f437 | ||
|
|
b3909b4af5 | ||
|
|
29738cc6f6 | ||
|
|
ee024b34da | ||
|
|
f422493694 | ||
|
|
071fde11d2 | ||
|
|
5c81aba90e | ||
|
|
6007a31380 | ||
|
|
c7a11eea4c | ||
|
|
be6c3ac662 | ||
|
|
9a54e09d15 | ||
|
|
f07c7aea2f | ||
|
|
cba6524926 | ||
|
|
1992516be9 | ||
|
|
cb71fb6907 | ||
|
|
33e7167090 | ||
|
|
f10474548b | ||
|
|
9bfbbd65f5 | ||
|
|
813191beef | ||
|
|
9e45baae58 | ||
|
|
c591922ae6 | ||
|
|
c89d06df18 | ||
|
|
97dca71c2c | ||
|
|
eaec3279ab | ||
|
|
195c313628 | ||
|
|
eb62d6368b | ||
|
|
4655a8504d | ||
|
|
44e4ae2d94 | ||
|
|
5fa5be2136 | ||
|
|
3e83402470 | ||
|
|
e7c2998cf9 | ||
|
|
9ef3dcd581 | ||
|
|
8cfd26c21e | ||
|
|
2d88469761 | ||
|
|
f3e53a108b | ||
|
|
a2436bc50b | ||
|
|
cd56ea7040 | ||
|
|
7efa672976 | ||
|
|
0856854c81 | ||
|
|
488f9653e6 | ||
|
|
48467bc514 | ||
|
|
56f7a5baae | ||
|
|
ee5a1f0e7a | ||
|
|
625bcf105c | ||
|
|
3e3ea37aba | ||
|
|
5049cc6e1d | ||
|
|
b142145a4e | ||
|
|
7eb3056856 | ||
|
|
d8e9c16d83 | ||
|
|
a58db791e1 | ||
|
|
fa2cfe36d4 | ||
|
|
b8c9880a2e | ||
|
|
3b4e7b0e5f | ||
|
|
ed27ef4cee | ||
|
|
1e2b7e728d | ||
|
|
1e9fbd216f | ||
|
|
139cd337a3 | ||
|
|
531d49fa00 | ||
|
|
b17c9a067a | ||
|
|
4d67a4a811 | ||
|
|
c286fdc96a | ||
|
|
b65caf82b4 | ||
|
|
25bd04e400 | ||
|
|
9944d136e4 | ||
|
|
816db26a75 | ||
|
|
25815ae53d | ||
|
|
ea61d00cf7 | ||
|
|
f15576fd98 | ||
|
|
6b64702054 | ||
|
|
0887d544ce | ||
|
|
af57d67320 | ||
|
|
4d460109dd | ||
|
|
10288f87c1 | ||
|
|
05bfe0a7b2 | ||
|
|
6e521af3b3 | ||
|
|
d833cc9c54 | ||
|
|
4e0d926f61 | ||
|
|
9e4ce3ae72 | ||
|
|
7a8e6f8e8c | ||
|
|
e137d63886 | ||
|
|
5e16ef73f9 | ||
|
|
9e4495b85b | ||
|
|
b5e1c8e47b | ||
|
|
36a05831ab | ||
|
|
12b895f94a | ||
|
|
e89015649e | ||
|
|
86fc7841b8 | ||
|
|
76e920fc1a | ||
|
|
99591a2b0f | ||
|
|
770b70a135 | ||
|
|
6845ef6bde | ||
|
|
d4609762bb | ||
|
|
ebf63a75d5 | ||
|
|
3effbe5f06 | ||
|
|
c742433d34 | ||
|
|
bd33b53805 | ||
|
|
1d6739b683 | ||
|
|
e75baed4b6 | ||
|
|
55f73d34e1 | ||
|
|
557157c2d6 | ||
|
|
0bae35d387 | ||
|
|
c7062bc560 | ||
|
|
a344352365 | ||
|
|
33d86ad3b5 | ||
|
|
8bacde0262 | ||
|
|
87c82071c0 | ||
|
|
1f72810649 | ||
|
|
6711095dd6 | ||
|
|
e39a42464e | ||
|
|
515674b6cf | ||
|
|
37cc63e493 | ||
|
|
4cd43f9c93 | ||
|
|
691d83e596 | ||
|
|
4bca25d783 | ||
|
|
74a5bcb3a9 | ||
|
|
9f55159bd5 | ||
|
|
f118082f6b | ||
|
|
586e9f328f | ||
|
|
fb9d52a19b | ||
|
|
815a7b6e1d | ||
|
|
336d889034 | ||
|
|
bba7479688 | ||
|
|
46a532540c | ||
|
|
1442c47bbb | ||
|
|
bb4e0be5f4 | ||
|
|
33aa182757 | ||
|
|
7bd8ace1a2 | ||
|
|
d9f4166418 | ||
|
|
02128618b0 | ||
|
|
f5cd841056 | ||
|
|
804e054bf8 | ||
|
|
3eebfdd349 | ||
|
|
581ff5fc73 | ||
|
|
3c50ffa18e | ||
|
|
4af7a1896c | ||
|
|
e1df1e7350 | ||
|
|
e156cc04c0 | ||
|
|
e0011d8372 | ||
|
|
0da777683f | ||
|
|
39eb5a50ab | ||
|
|
c04a7af39a | ||
|
|
67740a00bd | ||
|
|
6fada51fe8 | ||
|
|
eec2db0590 | ||
|
|
26316d8d76 | ||
|
|
6ea545df05 | ||
|
|
7674059899 | ||
|
|
c1f363fde2 | ||
|
|
ab2d174a0b | ||
|
|
6635540a6d | ||
|
|
a792858793 | ||
|
|
5f6f830d77 | ||
|
|
ba77125052 | ||
|
|
48b44efd67 | ||
|
|
4d9312259c | ||
|
|
7ede1ec4b0 | ||
|
|
b80d97dc38 | ||
|
|
6ee834279d | ||
|
|
c37fb0917f | ||
|
|
9d3986e06e | ||
|
|
dbcc1f4535 | ||
|
|
6c2b37c595 | ||
|
|
b338cc88fb | ||
|
|
58c5f7e373 | ||
|
|
47d188541b | ||
|
|
eb704d6420 | ||
|
|
3d9503658b | ||
|
|
5a1ffbc904 | ||
|
|
14f3a356e8 | ||
|
|
60b75b91bd | ||
|
|
4fa4737982 | ||
|
|
48c777be11 | ||
|
|
84a50beefc | ||
|
|
ab7908e012 | ||
|
|
388e0fe845 | ||
|
|
60b632418f | ||
|
|
89c81bd88b | ||
|
|
99b9b99343 | ||
|
|
0b5dbd6f5c | ||
|
|
726673f926 | ||
|
|
6a522b381c | ||
|
|
1882d263b9 | ||
|
|
b0683f77c2 | ||
|
|
7aceabed07 | ||
|
|
11c51500b3 | ||
|
|
fa886231d3 | ||
|
|
5085dcf96f | ||
|
|
34bcb2b609 | ||
|
|
c608742b84 | ||
|
|
d33c0ed1b5 | ||
|
|
d34618003d | ||
|
|
a24854a3cc | ||
|
|
f1e30dfba2 | ||
|
|
7b75476c4a | ||
|
|
b7bd41942d | ||
|
|
78db90e4bf | ||
|
|
592ca9b5c4 | ||
|
|
9981394557 | ||
|
|
b100325fe0 | ||
|
|
0ddfb48a98 | ||
|
|
d6610b7f8f | ||
|
|
ea540569ed | ||
|
|
e91d19e132 | ||
|
|
bd281e5753 | ||
|
|
7c59f05681 | ||
|
|
6dcde9fcbe | ||
|
|
cc048e55bf | ||
|
|
dd556b44e8 | ||
|
|
c62145af31 | ||
|
|
9148dc9e03 | ||
|
|
606824d282 | ||
|
|
231ca8a935 | ||
|
|
c96221c705 | ||
|
|
19f46eb817 | ||
|
|
185d53da6a | ||
|
|
07c1071c36 | ||
|
|
a9d0453811 | ||
|
|
54ef217de4 | ||
|
|
0ebfa89783 | ||
|
|
2a8b155a16 | ||
|
|
dd814d1591 | ||
|
|
1ba9ff8153 | ||
|
|
1121b81f12 | ||
|
|
f7fbe3946d | ||
|
|
921bfbbe3c | ||
|
|
bca3cb8303 | ||
|
|
fb03687802 | ||
|
|
c0e6a85ffd | ||
|
|
7f723a6bd5 | ||
|
|
02bc2e3ddb | ||
|
|
3c8e448ffb | ||
|
|
7885153933 | ||
|
|
f5161404cb | ||
|
|
a668ac7235 | ||
|
|
2610a286ca | ||
|
|
6daa065b1e | ||
|
|
1b873d3bad | ||
|
|
a0cfae214d | ||
|
|
db0af86085 | ||
|
|
be2f7cb3e5 | ||
|
|
082cb86a23 | ||
|
|
0420144104 | ||
|
|
22656c8699 | ||
|
|
c1c78164d2 | ||
|
|
08f1f05d58 | ||
|
|
c40b67fe77 | ||
|
|
81ebcc9a72 | ||
|
|
9ccc7feb58 | ||
|
|
7706adc444 | ||
|
|
7c76f7443e | ||
|
|
314ef361b6 | ||
|
|
ef401a1a2c | ||
|
|
a0877e484d | ||
|
|
067e0220bd | ||
|
|
da6481f96b | ||
|
|
7dea0441ac | ||
|
|
34c41d5f3d | ||
|
|
77d8dce81c | ||
|
|
d3058cbe07 | ||
|
|
587bab3eb1 | ||
|
|
a36a38bd8d | ||
|
|
629a6936fa | ||
|
|
4e7e50754d | ||
|
|
0288bc681b | ||
|
|
d46e3f07c3 | ||
|
|
d512ab5ddf | ||
|
|
2a052b2db1 | ||
|
|
3ada6d1bff | ||
|
|
86030a0fab | ||
|
|
954c40067e | ||
|
|
6a36606bd5 | ||
|
|
20a72a0f45 | ||
|
|
bcc374eb31 | ||
|
|
f0e1f18c79 | ||
|
|
61b7203062 | ||
|
|
a7e95d00cf | ||
|
|
83c358deb1 | ||
|
|
7dd214f3db | ||
|
|
6698d33f04 | ||
|
|
6dcd5b77aa | ||
|
|
80f2c797c9 | ||
|
|
910471a26f | ||
|
|
ccab588948 | ||
|
|
50683e6600 | ||
|
|
75daf98112 | ||
|
|
9a681a27ad | ||
|
|
25c63c8b10 | ||
|
|
a069df41b8 | ||
|
|
b1183c2c9d | ||
|
|
b777b15ee8 | ||
|
|
35061dfc53 | ||
|
|
a65bca6e49 | ||
|
|
72203f2721 | ||
|
|
03ff03ed52 | ||
|
|
488d50b97e | ||
|
|
88cbd1bd83 | ||
|
|
27fe556bab | ||
|
|
3191b7a991 | ||
|
|
f8d045c275 | ||
|
|
0038fe5ff1 | ||
|
|
3ae810a18e | ||
|
|
afe72c8029 | ||
|
|
2341bba973 | ||
|
|
c0da968af2 | ||
|
|
03fb04f4c5 | ||
|
|
d71c4a0ea7 | ||
|
|
df206d9792 | ||
|
|
49ad44dcaf | ||
|
|
7f785b8fa5 | ||
|
|
b4e674aeb0 | ||
|
|
8ed091703f | ||
|
|
464fd6d4d3 | ||
|
|
a96dfdda67 | ||
|
|
5b140d26c3 | ||
|
|
125fb81fa3 | ||
|
|
ee14372e20 | ||
|
|
5c27e0f9ef | ||
|
|
7607cec7a2 | ||
|
|
5f9c93369e | ||
|
|
9e4132fd3f | ||
|
|
0ae31e0acc | ||
|
|
24721cf2fa | ||
|
|
2ab41a359e | ||
|
|
c87167cac4 | ||
|
|
46311dfaba | ||
|
|
03720bbb81 | ||
|
|
3319fd6a21 | ||
|
|
0f75387f41 | ||
|
|
9fd5d8241e | ||
|
|
0bfee823dd | ||
|
|
668b235b07 | ||
|
|
ea6d0f573d | ||
|
|
d1b8afd3b8 | ||
|
|
0f8b9ca55b | ||
|
|
b5c84a91fb | ||
|
|
cc902db4ab | ||
|
|
faae82eae1 | ||
|
|
49ac0cadfb | ||
|
|
bd5f39e1c6 | ||
|
|
325d048340 | ||
|
|
f1805c8536 | ||
|
|
305545623a | ||
|
|
aac99f6ee2 | ||
|
|
4feea2e721 | ||
|
|
0e73b3b8e1 | ||
|
|
4b0bcf4464 | ||
|
|
57ef0ad41d | ||
|
|
a92d6b75bf | ||
|
|
763da979a8 | ||
|
|
8c58f7a04e | ||
|
|
e1868bdb78 | ||
|
|
f279368531 | ||
|
|
ed72ddc4d3 | ||
|
|
a7fa34c2fc | ||
|
|
51c734e438 | ||
|
|
ad676af3f0 | ||
|
|
1251353694 | ||
|
|
4d97023938 | ||
|
|
adf77053c5 | ||
|
|
9c57888524 | ||
|
|
dd33dc1f9b | ||
|
|
90ed6163f5 | ||
|
|
8f162cd57c | ||
|
|
e7e4bf39fe | ||
|
|
d9341e033b | ||
|
|
bf7d4ebea5 | ||
|
|
8d4a4dc526 | ||
|
|
2a3a0c758a | ||
|
|
94eb7b155e | ||
|
|
50f12869bf | ||
|
|
c81c79ad52 | ||
|
|
f2f6f2f5a8 | ||
|
|
2e2afa616d | ||
|
|
d82a7040f1 | ||
|
|
8fc97a7f91 | ||
|
|
5789c1ae7d | ||
|
|
520a89f5f6 | ||
|
|
15379384dd | ||
|
|
4cc44b37bb | ||
|
|
e121fec599 | ||
|
|
6c669abb23 | ||
|
|
622c4f9f6f | ||
|
|
b7316353f4 | ||
|
|
902a2723ae | ||
|
|
57f3c67e28 | ||
|
|
6a2bc1ef2b | ||
|
|
0b677677d1 | ||
|
|
41f9f96819 | ||
|
|
49f9fee446 | ||
|
|
9588c1ea3e | ||
|
|
c665b01e89 | ||
|
|
5c2db0134f | ||
|
|
de162eb719 | ||
|
|
33297e0226 | ||
|
|
a07e643020 | ||
|
|
304664b318 | ||
|
|
8372a3c7ca | ||
|
|
69bbc0a2a1 | ||
|
|
5bfe881fc8 | ||
|
|
44f691bea4 | ||
|
|
e59db45f5b | ||
|
|
f4087694b1 | ||
|
|
2c63e0fdd6 | ||
|
|
29bb71373e | ||
|
|
ed48858635 | ||
|
|
6f5c8389eb | ||
|
|
52bd72f449 | ||
|
|
b82f26366c | ||
|
|
758057131b | ||
|
|
e37adcd67e | ||
|
|
4e08656422 | ||
|
|
d10e70a77b | ||
|
|
0aa80f1459 | ||
|
|
26732265f0 | ||
|
|
c214c6c120 | ||
|
|
485eb60dde | ||
|
|
7e5e029559 | ||
|
|
116fd7b829 | ||
|
|
1a2b46f00c | ||
|
|
557509ef84 | ||
|
|
56f1c53084 | ||
|
|
afefee2357 | ||
|
|
bfeb1693d6 | ||
|
|
3f82b05f4f | ||
|
|
1fca80e27d | ||
|
|
7928bede1e | ||
|
|
3e62300f9c | ||
|
|
49c9eaa6e1 | ||
|
|
58db8a838a | ||
|
|
5509c65a6f | ||
|
|
fb3ba8bf92 | ||
|
|
667bda6afb | ||
|
|
a381e9aa3b | ||
|
|
32dc3b36ab | ||
|
|
190f02a939 | ||
|
|
aa2027f1b5 | ||
|
|
f665da9348 | ||
|
|
fdc2baab2b | ||
|
|
f4fc0e17da | ||
|
|
b5389cadc8 | ||
|
|
3641869332 | ||
|
|
d570a55ce6 | ||
|
|
fe77e05aff | ||
|
|
906ad8c7c1 | ||
|
|
e5d0a68d70 | ||
|
|
a17adfe366 | ||
|
|
ff158282e7 | ||
|
|
5df8abcddf | ||
|
|
3fad8479ca | ||
|
|
c7da922383 | ||
|
|
c4e2627b43 | ||
|
|
60968a926f | ||
|
|
93001377bf | ||
|
|
b912116a2f | ||
|
|
5899b0f1e4 | ||
|
|
e6e54822f5 | ||
|
|
db5adef813 | ||
|
|
adb8127a30 | ||
|
|
a4d2b8862b | ||
|
|
ad153c226e | ||
|
|
39ce0af4bf | ||
|
|
2e132e47e4 | ||
|
|
8b2cd11e9f | ||
|
|
a69f7c9dfd | ||
|
|
671ac562e7 | ||
|
|
89cb4bbb8c | ||
|
|
a91f8c4d51 | ||
|
|
8ea614266c | ||
|
|
1c0ba24e48 | ||
|
|
3d4b3bd089 | ||
|
|
31783c0d0a | ||
|
|
d244affa6c | ||
|
|
82a999e6e9 | ||
|
|
74fdb728b4 | ||
|
|
be6a53b3eb | ||
|
|
ff00af60ae | ||
|
|
ccabd09742 | ||
|
|
f784729e67 | ||
|
|
9771e956f4 | ||
|
|
5bd209aded | ||
|
|
a9554779ea | ||
|
|
fc90ad5949 | ||
|
|
3f7765fdc8 | ||
|
|
ee58871f65 | ||
|
|
b2b6472222 | ||
|
|
1c8fb4139d | ||
|
|
50057ce9c8 | ||
|
|
51dd7c9abd | ||
|
|
f250cd246c | ||
|
|
0b871b3fa5 | ||
|
|
e00a95bf02 | ||
|
|
2d3b7da4cd | ||
|
|
5083128774 | ||
|
|
e2d1b19216 | ||
|
|
e2287fae58 | ||
|
|
ef519ac5ff | ||
|
|
e7d978e027 | ||
|
|
b6d4442800 | ||
|
|
895e3931bd | ||
|
|
27ff33f93b | ||
|
|
a987425f4a | ||
|
|
971d2dfc31 | ||
|
|
5bb99f941c | ||
|
|
86334452c0 | ||
|
|
8c224878dc | ||
|
|
603db8ce6a | ||
|
|
d4b64ba26b | ||
|
|
0f0a3474fd | ||
|
|
b1de2b1a4a | ||
|
|
87ed178e27 | ||
|
|
5bae4dbf9d | ||
|
|
89eb5b7eb9 | ||
|
|
fbd30dc4ee | ||
|
|
fb9f72fc90 | ||
|
|
30558764ba | ||
|
|
fe2aaa81ca | ||
|
|
b38351a470 | ||
|
|
1d47cadae8 | ||
|
|
47cb9e8e44 | ||
|
|
ac10d25f5f | ||
|
|
597a0f21e0 | ||
|
|
4c15a83e9c | ||
|
|
2df8b234fe | ||
|
|
d852a51672 | ||
|
|
5ec8d943a3 | ||
|
|
9b4a5523cc | ||
|
|
70a4d38d04 | ||
|
|
0ad8576ae5 | ||
|
|
e712883ce1 | ||
|
|
c0f9b33bba | ||
|
|
6de76ea5d1 | ||
|
|
262e72d541 | ||
|
|
2f765529e5 | ||
|
|
bcea92e313 | ||
|
|
56ef849868 | ||
|
|
2a0c4d2b0d | ||
|
|
d4ad9b3778 | ||
|
|
df972b9ae9 | ||
|
|
f695559379 | ||
|
|
3fa7828324 | ||
|
|
dbe17b4b16 | ||
|
|
ee4df2806f | ||
|
|
a405f2e81e | ||
|
|
afc0bc9323 | ||
|
|
ce28dcc630 | ||
|
|
892830e125 | ||
|
|
75425ab1a9 | ||
|
|
2722847a59 | ||
|
|
641f84e9f8 | ||
|
|
1f0a5842f9 | ||
|
|
28c2fb92a8 | ||
|
|
715101cf5e | ||
|
|
ac37a44ffa | ||
|
|
ae3d2bebbe | ||
|
|
f8d4e1a307 | ||
|
|
b98d6984a1 | ||
|
|
8f5c9a3c72 | ||
|
|
e071393eb5 | ||
|
|
6f9fec658f | ||
|
|
9227964cb6 | ||
|
|
cf6056cede | ||
|
|
4397612349 | ||
|
|
cf3719a663 | ||
|
|
77bf35d728 | ||
|
|
e7addec0a1 | ||
|
|
243d61d95f | ||
|
|
028874fd05 | ||
|
|
6d366fe80f | ||
|
|
0924f767e9 | ||
|
|
173b5a1cd1 | ||
|
|
49e1d51be9 | ||
|
|
23e47a74ee | ||
|
|
fce7f6ce47 | ||
|
|
f3b47a16dd | ||
|
|
aa7b754693 | ||
|
|
397b13e2d8 | ||
|
|
b2c203e8c1 | ||
|
|
6afb314d26 | ||
|
|
28123355b4 | ||
|
|
bcb87f5d55 | ||
|
|
981c1c1263 | ||
|
|
67b9a3bc0e | ||
|
|
ab4914ee6a | ||
|
|
e7c73c76dd | ||
|
|
3591a3fe5c | ||
|
|
fbdce049b2 | ||
|
|
9a8520a2de | ||
|
|
a315ab29bc | ||
|
|
5437d691b5 | ||
|
|
f99c90dc85 | ||
|
|
d838388443 | ||
|
|
0b2c488a61 | ||
|
|
e2eb4ef29d | ||
|
|
76e135077b | ||
|
|
6078cd2eab | ||
|
|
3482dade71 | ||
|
|
ff73de5716 | ||
|
|
04d0c350db | ||
|
|
b6a5c91045 | ||
|
|
7a37c79ebc | ||
|
|
ba227c5ec3 | ||
|
|
7ab75dd15a | ||
|
|
df23162e9d | ||
|
|
2c12f18b44 | ||
|
|
eaeb28b4e1 | ||
|
|
d5647eab33 | ||
|
|
89eb8885b1 | ||
|
|
a5dc5687f8 | ||
|
|
6780485051 | ||
|
|
d043e7a242 | ||
|
|
c5d9b5f51d | ||
|
|
35e2892b98 | ||
|
|
b492c5ac1a | ||
|
|
df38b3c62a | ||
|
|
03a860dd6f | ||
|
|
fec585e44b | ||
|
|
11dfdbb7a3 | ||
|
|
ae1a0f411b | ||
|
|
007b5d7f50 | ||
|
|
c6eadc504b | ||
|
|
a864258cb8 | ||
|
|
8a9c15c874 | ||
|
|
7a666526b7 | ||
|
|
3fc1cac015 | ||
|
|
04a0b07bf6 | ||
|
|
59e48ca91a | ||
|
|
8ff562c5af | ||
|
|
b502a93728 | ||
|
|
b6afa6c2c7 | ||
|
|
5887da0229 | ||
|
|
a7d833d96a | ||
|
|
db3753d611 | ||
|
|
f810b13bca | ||
|
|
5ad687c6d8 | ||
|
|
6ad0910790 | ||
|
|
4d8c0546cf | ||
|
|
35f96d4a40 | ||
|
|
ae96fb6f63 | ||
|
|
67592d80aa | ||
|
|
94a5e43e5d | ||
|
|
26958f8f70 | ||
|
|
a427d215e3 | ||
|
|
271cf37b8a | ||
|
|
179c03e79d | ||
|
|
0a1b68639b | ||
|
|
d69e7ec850 | ||
|
|
76a6d8292c | ||
|
|
8f09c444b6 | ||
|
|
9032e6abb8 | ||
|
|
1c070d16a6 | ||
|
|
7837fcc657 | ||
|
|
f9690d40d3 | ||
|
|
5de6cd77dc | ||
|
|
aa5ab55b14 | ||
|
|
9195b18981 | ||
|
|
b812d6efb8 | ||
|
|
231a02eb10 | ||
|
|
6736806361 | ||
|
|
8e17756bf8 | ||
|
|
0b133fe55e | ||
|
|
d01266c642 | ||
|
|
fe3f9c86d5 | ||
|
|
14bf3645d6 | ||
|
|
0f4a7b2405 | ||
|
|
681e49a4cc | ||
|
|
6e9c97fbff | ||
|
|
370070f489 | ||
|
|
7168f4014d | ||
|
|
f0912feefb | ||
|
|
e90c9c171a | ||
|
|
d0c172830c | ||
|
|
d5bf0d1199 | ||
|
|
d3a24446b8 | ||
|
|
aa93276e6e | ||
|
|
cf36972969 | ||
|
|
40862f26e6 | ||
|
|
4083447c3f | ||
|
|
3cb34ad827 | ||
|
|
61d7566ca1 | ||
|
|
af338d447b | ||
|
|
6fad06f659 | ||
|
|
1d51d8ff27 | ||
|
|
82dd4aa403 | ||
|
|
8af9bd1ac3 | ||
|
|
9fc3845d92 | ||
|
|
93bbe8e7a8 | ||
|
|
46acd16999 | ||
|
|
5ad2c6abf6 | ||
|
|
d5781d60bd | ||
|
|
e464a95c5a | ||
|
|
a50ea4bb9e | ||
|
|
aa11bb6d93 | ||
|
|
319018f055 | ||
|
|
394b986ccb | ||
|
|
26f7b36ce4 | ||
|
|
f0daad10ce | ||
|
|
0bc557fb8b | ||
|
|
3571421a0e | ||
|
|
aed80f3e4f | ||
|
|
b84e79362e | ||
|
|
dc077bc309 | ||
|
|
0fd634ef43 | ||
|
|
d352b6b509 | ||
|
|
fcc48cc738 | ||
|
|
ec06a345cc | ||
|
|
7690b364e7 | ||
|
|
b94c0c7d04 | ||
|
|
500bfdf588 | ||
|
|
d23b19c466 | ||
|
|
3a5450039d | ||
|
|
b582ddf090 | ||
|
|
ef8b470e8b | ||
|
|
5a0841a994 | ||
|
|
bd462c4e0b | ||
|
|
f11ec4e142 | ||
|
|
a5393a3ec4 | ||
|
|
bf76da3222 | ||
|
|
f171b7de96 | ||
|
|
c0cbf00199 | ||
|
|
0cd6e59fb9 | ||
|
|
11a8adc71c | ||
|
|
b9c7fd879f | ||
|
|
2fc4c7ea33 | ||
|
|
c5003665c3 | ||
|
|
538028c150 | ||
|
|
fb8d187f8d | ||
|
|
1a11301e1a | ||
|
|
4c6cdd5c23 | ||
|
|
30a64b0dd3 | ||
|
|
04de492019 | ||
|
|
07890df6cb | ||
|
|
2f23cfdf1c | ||
|
|
1832946d41 | ||
|
|
6ec8745d2e | ||
|
|
b6bbfe063b | ||
|
|
48182edbd5 | ||
|
|
94a00cb6d6 | ||
|
|
fc24361aa6 | ||
|
|
cec833afc6 | ||
|
|
f1cddba938 | ||
|
|
a0acdfdcb9 | ||
|
|
6637f294df | ||
|
|
ad8a444105 | ||
|
|
877cfa0071 | ||
|
|
e6f0a780b7 | ||
|
|
dd9de2efa9 | ||
|
|
f6b0811f78 | ||
|
|
eba9d854a9 | ||
|
|
437cf9bab0 | ||
|
|
fdaeccf1e5 | ||
|
|
7723e46c26 | ||
|
|
dce355cce6 | ||
|
|
213e7b7093 | ||
|
|
fe7d8f93a1 | ||
|
|
9e2f4216f9 | ||
|
|
a48f7b2222 | ||
|
|
0b85d8a9bc | ||
|
|
58d6938065 | ||
|
|
a536a2b822 | ||
|
|
9ffad1005e | ||
|
|
65edddd62e | ||
|
|
a7cdcd8b3a | ||
|
|
3d6b85ed20 | ||
|
|
7abea2020c | ||
|
|
e16c34f0e3 | ||
|
|
4bfda6a145 | ||
|
|
98470e8551 | ||
|
|
df558ab8d6 | ||
|
|
c07372b58c | ||
|
|
00f59b95ae | ||
|
|
8915a7c2cd | ||
|
|
8595964ab8 | ||
|
|
922dae8546 | ||
|
|
69b3e23400 | ||
|
|
55325773dc | ||
|
|
b84c915b23 | ||
|
|
cfb390936a | ||
|
|
c5f344f333 | ||
|
|
ba4b496306 | ||
|
|
c48554589c | ||
|
|
da0851e21d | ||
|
|
d2d05abac0 | ||
|
|
de3e0423cc | ||
|
|
8d742d7938 | ||
|
|
682fd550fa | ||
|
|
abcf836a0c | ||
|
|
b123fb2cc7 | ||
|
|
0da3621a68 | ||
|
|
8ed452d9ea | ||
|
|
f380d44697 | ||
|
|
86d377a2f0 | ||
|
|
508a6d99f5 | ||
|
|
63e42047e3 | ||
|
|
769be46bf9 | ||
|
|
13829de0d9 | ||
|
|
ad7f570be5 | ||
|
|
9ba4f966db | ||
|
|
ae8d2ac2e1 | ||
|
|
93beb068a3 | ||
|
|
e88d260acd | ||
|
|
8121238872 | ||
|
|
161e377ec1 | ||
|
|
ad4bd800aa | ||
|
|
2fba6f65f4 | ||
|
|
a754ab4f10 | ||
|
|
86cfc468bd | ||
|
|
7df0c1607e | ||
|
|
6acd36e374 | ||
|
|
af51eecbac | ||
|
|
3a23dc8b04 | ||
|
|
ba13e44720 | ||
|
|
e80420f6db | ||
|
|
21ddcfc866 | ||
|
|
20f82cb22c | ||
|
|
7ef75bab23 | ||
|
|
7224e03590 | ||
|
|
cf4f2991a5 | ||
|
|
9eb3c23494 | ||
|
|
c80d8898cc | ||
|
|
bc74dd88e0 | ||
|
|
da87c461ef | ||
|
|
bf2e694f2c | ||
|
|
e5150487c4 | ||
|
|
9ff6353b88 | ||
|
|
926fd8abf4 | ||
|
|
211a7a4cfe | ||
|
|
c1835cd9cc | ||
|
|
5700044393 | ||
|
|
36fbd3d018 | ||
|
|
d1178390a9 | ||
|
|
8182825e92 | ||
|
|
2392006246 | ||
|
|
a6e78cd5dc | ||
|
|
8752790352 | ||
|
|
3976c79e12 | ||
|
|
5c1cf7f4ac | ||
|
|
7e90b8b7be | ||
|
|
912321a030 | ||
|
|
ab0a905499 | ||
|
|
3c6b3c02df | ||
|
|
bcb2e91d97 | ||
|
|
766ef94605 | ||
|
|
e3f016e262 | ||
|
|
65833f1ae0 | ||
|
|
2602cd9ab2 | ||
|
|
8333f3d9de | ||
|
|
dee1d9ba74 | ||
|
|
ed2e0c5080 | ||
|
|
7db810d7d0 | ||
|
|
8dae4e5038 | ||
|
|
b9b28edefe | ||
|
|
58120f435f | ||
|
|
027b8e52da | ||
|
|
aad510a9d5 | ||
|
|
9852a805a1 | ||
|
|
b2cabf0122 | ||
|
|
521ce15f86 | ||
|
|
fb97c11140 | ||
|
|
1c5c62e311 | ||
|
|
77148f7f97 | ||
|
|
a329d2f2bc | ||
|
|
39e9e4446b | ||
|
|
b32de54944 | ||
|
|
071b874e1b | ||
|
|
9ba65d3323 | ||
|
|
890a851bbf | ||
|
|
5f6ca23da4 | ||
|
|
58df1c06ee | ||
|
|
95f8599dc2 | ||
|
|
8a11242d7f | ||
|
|
948513ef5f | ||
|
|
c497a35d21 | ||
|
|
e0a539bc64 | ||
|
|
44b8395ead | ||
|
|
1bc8878490 | ||
|
|
ded2ac493d | ||
|
|
57b3319ac0 | ||
|
|
eba7ba25b8 | ||
|
|
df774892c8 | ||
|
|
f3b4ce6b67 | ||
|
|
bb8545b3e1 | ||
|
|
600149fc2b | ||
|
|
f4de3c8748 | ||
|
|
6e7e04839f | ||
|
|
f62dcc12a0 | ||
|
|
bef591c2e6 | ||
|
|
5907296d36 | ||
|
|
aa2a7d12be | ||
|
|
33fee5dcc5 | ||
|
|
e9ae50be0c | ||
|
|
5886c0fd5e | ||
|
|
ed146fcf07 | ||
|
|
35538e6f77 | ||
|
|
ea924f3bbf | ||
|
|
7bc15a2fc9 | ||
|
|
2bf7db92ee | ||
|
|
95260f56ba | ||
|
|
c5ace0376a | ||
|
|
7ee09388fa | ||
|
|
a15b0ef060 | ||
|
|
57cfd9a315 | ||
|
|
5fb4149c32 | ||
|
|
03d97ba617 | ||
|
|
5205f5f4b4 | ||
|
|
6eda0f4d00 | ||
|
|
9e640cac6b | ||
|
|
061521f87f | ||
|
|
b15eb278e1 | ||
|
|
142ac8eb96 | ||
|
|
88705bb6e9 | ||
|
|
60d4fcfe7e | ||
|
|
038d19ec98 | ||
|
|
e1b98768c7 | ||
|
|
b82af2b849 | ||
|
|
703591d76a | ||
|
|
7142688a77 | ||
|
|
a12622b3d8 | ||
|
|
9248ab4dfd | ||
|
|
5a8c6440f0 | ||
|
|
74b694a4dd | ||
|
|
896b52d5fb | ||
|
|
1429fea27a | ||
|
|
3218563f32 | ||
|
|
d412edbbe1 | ||
|
|
968159a85d | ||
|
|
18a3741fc2 | ||
|
|
f1be3e6bb0 | ||
|
|
b717a02394 | ||
|
|
d68143e63d | ||
|
|
0d306b8b1c | ||
|
|
a655863855 | ||
|
|
58264c80dd | ||
|
|
6f9f1aec65 | ||
|
|
97b1ee5b02 | ||
|
|
fe033cd0b3 | ||
|
|
afbd07c62a | ||
|
|
9b15996545 | ||
|
|
1dbbd7241d | ||
|
|
6c0ef48d45 | ||
|
|
8b57f88ca3 | ||
|
|
3e9fdc777e | ||
|
|
a8ca88797a | ||
|
|
71540b5dc0 | ||
|
|
b5a145d7b3 | ||
|
|
21d6a0a2dd | ||
|
|
80cc7340ac | ||
|
|
45b272ee2f | ||
|
|
f765664580 | ||
|
|
10b44f036d | ||
|
|
1bf4ee3a3c | ||
|
|
5d82ffa503 | ||
|
|
5dc3fd2ec0 | ||
|
|
4562fdda92 | ||
|
|
18258b9b0d | ||
|
|
92e0f242c7 | ||
|
|
428fa9404c | ||
|
|
3cccc480fb | ||
|
|
acb94216c8 | ||
|
|
5fa97841b2 | ||
|
|
4ad66bf7b9 | ||
|
|
64860ed5e5 | ||
|
|
b17faf6e1e | ||
|
|
0ea73bd527 | ||
|
|
b2f0820560 | ||
|
|
7ad5d42982 | ||
|
|
3912734498 | ||
|
|
0fa3f9a057 | ||
|
|
0fbabdcf25 | ||
|
|
67b7ae98a6 | ||
|
|
0f703c95dd | ||
|
|
c34b3f41bd | ||
|
|
e003b17280 | ||
|
|
e003d58c60 | ||
|
|
0546d06c0a | ||
|
|
5337111990 | ||
|
|
bb06f8eb0c | ||
|
|
23e3a1c269 | ||
|
|
e47740e02e | ||
|
|
d9ff0035f5 | ||
|
|
7a7f3be0d2 | ||
|
|
91e45fbe95 | ||
|
|
7d7e9da28c | ||
|
|
24a9739604 | ||
|
|
4fb9687782 | ||
|
|
95ffc21b60 | ||
|
|
f3c5e55b26 | ||
|
|
40183c6a5c | ||
|
|
457c59e38a | ||
|
|
aa93a3f2e2 | ||
|
|
8b9abcb6cc | ||
|
|
1ecc1908c7 | ||
|
|
6a2c7b467d | ||
|
|
0acef57865 | ||
|
|
43046ee649 | ||
|
|
a15fda0c08 | ||
|
|
e5988764ce | ||
|
|
9c9d9b5a8d | ||
|
|
44dc564d85 | ||
|
|
83e367afab | ||
|
|
8b7e7c2669 | ||
|
|
53474021b7 | ||
|
|
da1ed1b5b2 | ||
|
|
e08d661600 | ||
|
|
1aa1bc7a26 | ||
|
|
47634e942e | ||
|
|
15466cbf1a | ||
|
|
2a749db427 | ||
|
|
ecccce86e4 | ||
|
|
bf3f64bea4 | ||
|
|
2f2d6b8535 | ||
|
|
d68c884649 | ||
|
|
8b556de03b | ||
|
|
7229af53c3 | ||
|
|
81b3034c2f | ||
|
|
f0419396b5 | ||
|
|
6b9c2754e8 | ||
|
|
8edb131f8b | ||
|
|
d6f6520a79 | ||
|
|
cc2bb4d719 | ||
|
|
3859f1c9ae | ||
|
|
5f8d774e19 | ||
|
|
538a3e855c | ||
|
|
03f2ef1e2b | ||
|
|
237d0746cf | ||
|
|
33b6c58087 | ||
|
|
e96b023d04 | ||
|
|
7ac1d4621b | ||
|
|
a2d7cbe8fe | ||
|
|
c74ed29739 | ||
|
|
6c8501f122 | ||
|
|
941e945f74 | ||
|
|
f2844d59e4 | ||
|
|
047ff187f6 | ||
|
|
1136c40811 | ||
|
|
5a78dc864f | ||
|
|
15c98c3048 | ||
|
|
0a5b005ce5 | ||
|
|
4d64e64127 | ||
|
|
5470c70cd0 | ||
|
|
47959ee395 | ||
|
|
7c34c178cd | ||
|
|
ac7cb41483 | ||
|
|
0ab388b88e | ||
|
|
54448902f1 | ||
|
|
12107a02fd | ||
|
|
eace06efdc | ||
|
|
ee0afa1eec | ||
|
|
83cdd0dafe | ||
|
|
5be025f1d1 | ||
|
|
c651842ea1 | ||
|
|
423abe6788 | ||
|
|
4003c38fd1 | ||
|
|
3e0c322fd4 | ||
|
|
7fcdd4abdd | ||
|
|
3f3280b2d4 | ||
|
|
aae2399631 | ||
|
|
03bd2b6803 | ||
|
|
48754fd999 | ||
|
|
c496ebdef9 | ||
|
|
c009c40606 | ||
|
|
b29456c8e5 | ||
|
|
38266bf2ff | ||
|
|
c2e51f8948 | ||
|
|
c54a57838e | ||
|
|
64f040bddd | ||
|
|
1a099ea2f2 | ||
|
|
13c45807ef | ||
|
|
dfbb9d5fff | ||
|
|
a7fe369ea0 | ||
|
|
b62e6c5a69 | ||
|
|
92e29a6ad7 | ||
|
|
eeb9c69aa3 | ||
|
|
b7662ed5a1 | ||
|
|
9d6296f610 | ||
|
|
fd2a1320e0 | ||
|
|
8a8a6a4a82 | ||
|
|
8cdc14eec1 | ||
|
|
a1200b2fb5 | ||
|
|
c88c29eddc | ||
|
|
2845c4de98 | ||
|
|
bfa9cd15b7 | ||
|
|
659e2b414d | ||
|
|
7bcb58e3db | ||
|
|
2d7d7776a6 | ||
|
|
c5f429521c | ||
|
|
426d8636bc | ||
|
|
a265c7096e | ||
|
|
1c9953b1ba | ||
|
|
601cc21a44 | ||
|
|
102c42dfe4 | ||
|
|
4953727aa7 | ||
|
|
e6af874b47 | ||
|
|
801b4eef4c | ||
|
|
fe5c20a04e | ||
|
|
246fd05fae | ||
|
|
a09b298127 | ||
|
|
f89f40778f | ||
|
|
3d0c8d8d45 | ||
|
|
0e5e8bf14e | ||
|
|
ce34d329d3 | ||
|
|
eaf4a5805c | ||
|
|
8420e565d4 | ||
|
|
00df10c29a | ||
|
|
1b68deb0f6 | ||
|
|
d1497c9ac8 | ||
|
|
03d4cbf6d5 | ||
|
|
718be831af | ||
|
|
9d5ec523be | ||
|
|
81c43b45fb | ||
|
|
146a491769 | ||
|
|
4c53388579 | ||
|
|
3403ddcc6e | ||
|
|
684b81d835 | ||
|
|
4f32da57fd | ||
|
|
97265e48b3 | ||
|
|
64797158e2 | ||
|
|
8359293dcd | ||
|
|
b2dc53d18b | ||
|
|
edf8dd2a12 | ||
|
|
5a777bd598 | ||
|
|
bd39e01ee1 | ||
|
|
e3ed29aab6 | ||
|
|
896ce9c0e2 | ||
|
|
82934132e9 | ||
|
|
a2012b70de | ||
|
|
bcfeba8a57 | ||
|
|
d3dfd9ce57 | ||
|
|
aa06d5d356 | ||
|
|
448c8a29e1 | ||
|
|
928b7120f4 | ||
|
|
a3deacd718 | ||
|
|
78959fffbd | ||
|
|
1788616e52 | ||
|
|
c61e6d0777 | ||
|
|
41d91d628a | ||
|
|
a3bc7620b1 | ||
|
|
8064c588dc | ||
|
|
564e983c68 | ||
|
|
e1da181740 | ||
|
|
c63209200e | ||
|
|
737808cf53 | ||
|
|
a197bb7736 | ||
|
|
f9dd967bc5 | ||
|
|
44e4d55a66 | ||
|
|
095c84ac16 | ||
|
|
e063eae727 | ||
|
|
f02c5b5c69 | ||
|
|
838f1d645c | ||
|
|
ce2c30c437 | ||
|
|
d56fae0a7b | ||
|
|
e45ef00bef | ||
|
|
e9f31f7394 | ||
|
|
7c10a98eb2 | ||
|
|
f260483101 | ||
|
|
389e6e5c9e | ||
|
|
1cfd5866be | ||
|
|
c7ceac7f41 | ||
|
|
cd6eca0424 | ||
|
|
8c6136fea0 | ||
|
|
9644444028 | ||
|
|
9c4154291d | ||
|
|
533f5f6da6 | ||
|
|
1b8de756cd | ||
|
|
650b415537 | ||
|
|
04b50329fc | ||
|
|
25aab8c55c | ||
|
|
ceda2e70c1 | ||
|
|
2908303d4b | ||
|
|
a9f69711c6 | ||
|
|
a8ab16a720 | ||
|
|
8091b6b508 | ||
|
|
a00ef0fc7e | ||
|
|
5ce6d615a4 | ||
|
|
e06b69cdac | ||
|
|
d261ae7883 | ||
|
|
6fa77a63d7 | ||
|
|
f76c1b32d6 | ||
|
|
0aede2ef63 | ||
|
|
1e3a2e0a27 | ||
|
|
1bdabf43db | ||
|
|
05e568feb0 | ||
|
|
81e2519436 | ||
|
|
ef623c9bb5 | ||
|
|
da581525a6 | ||
|
|
6ff7b6570c | ||
|
|
8b2081837e | ||
|
|
ce978b602a | ||
|
|
9b00f5d550 | ||
|
|
d98ec59c79 | ||
|
|
d79b55be5a | ||
|
|
1f9a402dcd | ||
|
|
f9bcc9418b | ||
|
|
08256a3502 | ||
|
|
9b255e643a | ||
|
|
ca1f918e9e | ||
|
|
bb3fe1cd48 | ||
|
|
5d7772ecb0 | ||
|
|
56ce618eca | ||
|
|
605c3f9be1 | ||
|
|
b0381c7542 | ||
|
|
2f0894c220 | ||
|
|
b328ed5fa9 | ||
|
|
7d72f1711f | ||
|
|
d139b4557f | ||
|
|
cd05e03d63 | ||
|
|
e25029939d | ||
|
|
53de27417d | ||
|
|
74d3374d5c | ||
|
|
3ae00bebe4 | ||
|
|
f9df72c4d7 | ||
|
|
d0fb4576a8 | ||
|
|
0e4b0b3540 | ||
|
|
df1105d0c6 | ||
|
|
44478c36a3 | ||
|
|
fa267274b0 | ||
|
|
0db272946a | ||
|
|
91015b6499 | ||
|
|
2979a36a7c | ||
|
|
72f6d6b7b9 | ||
|
|
d81a7bcedf | ||
|
|
8fbbe8b82b | ||
|
|
271f5f9c64 | ||
|
|
7c992ffd21 | ||
|
|
fc2af8ba87 | ||
|
|
c8a539a6cb | ||
|
|
b7cdaa662a | ||
|
|
0a25930020 | ||
|
|
8643f4015f | ||
|
|
1854711aff | ||
|
|
c905119d82 | ||
|
|
c581ca8339 | ||
|
|
ccf9d9214a | ||
|
|
d37c8b732f | ||
|
|
f707fc1cad | ||
|
|
b1c713de60 | ||
|
|
0f13965391 | ||
|
|
8642e2b721 | ||
|
|
441534853b | ||
|
|
82f42c8664 | ||
|
|
5cd318fa9a | ||
|
|
5506071e9a | ||
|
|
ced98f2da7 | ||
|
|
282ec65e8b | ||
|
|
8e06dc5ace | ||
|
|
bfd3e2c01b | ||
|
|
a1957f0923 | ||
|
|
11a02ba361 | ||
|
|
4643c19abc | ||
|
|
a3369df62f | ||
|
|
4297c42597 | ||
|
|
e06e7157ac | ||
|
|
22f9e6f4c0 | ||
|
|
4b7a9233e7 | ||
|
|
204839f702 | ||
|
|
d15e3109ee | ||
|
|
8b513ee8f8 | ||
|
|
2c1488e65a | ||
|
|
8ebe1cc2d8 | ||
|
|
b0d6c15e63 | ||
|
|
3a3c7a7968 | ||
|
|
783d7ae605 | ||
|
|
bbf7a6b2f8 | ||
|
|
0fe6e24554 | ||
|
|
4bbaf55586 | ||
|
|
cda765a02d | ||
|
|
36856b18db | ||
|
|
66f0a8f994 | ||
|
|
455231170f | ||
|
|
5faeb58ab0 | ||
|
|
056e4a88ff | ||
|
|
8fd944ccf7 | ||
|
|
86105a547c | ||
|
|
9806648c07 | ||
|
|
6186babdb3 | ||
|
|
f2ecefb54a | ||
|
|
43bd529b78 | ||
|
|
9c82b3d4ca | ||
|
|
b19e6a8e87 | ||
|
|
e3a2bd75f3 | ||
|
|
da39e1485f | ||
|
|
88cc53a4b0 | ||
|
|
245243c7e7 | ||
|
|
759ac0df3d | ||
|
|
db8d97b6de | ||
|
|
27d66e4b3e | ||
|
|
ca7854210d | ||
|
|
c009c993c3 | ||
|
|
00188f75ae | ||
|
|
4d086542aa | ||
|
|
1555883633 | ||
|
|
8f2c0acc7e | ||
|
|
0e30d15c01 | ||
|
|
da14390fe0 | ||
|
|
11c0cff4ef | ||
|
|
e322376996 | ||
|
|
4fbe45f30a | ||
|
|
2cd0f60c3c | ||
|
|
1b354be827 | ||
|
|
7db280ee64 | ||
|
|
192c06cadf | ||
|
|
ad7e7abda0 | ||
|
|
02ccb35e80 | ||
|
|
a8a29e17c5 | ||
|
|
75a6d850fc |
52
.agents/skills/capture-release-evidences/SKILL.md
Normal file
52
.agents/skills/capture-release-evidences/SKILL.md
Normal file
@@ -0,0 +1,52 @@
|
||||
---
|
||||
name: capture-release-evidences-cx
|
||||
description: Automatically run a browser-automation agent to visually validate all new UI features from the current release and capture evidence WebP recordings of the changes.
|
||||
---
|
||||
|
||||
# Capture Release Evidences Workflow
|
||||
|
||||
Use this workflow to automatically drive a browser-automation agent to explore the newly deployed or locally running application and record evidence of the UI changes introduced in the latest release.
|
||||
|
||||
> **Tool mapping note (v3.8):** The `browser_subagent` tool referenced below is specific to an earlier agent runtime. In Claude Code, substitute with the available browser MCP tools (e.g. `mcp__claude-in-chrome__*`) for navigation/screenshots, plus the `Write` tool for saving artifacts. The high-level steps remain the same regardless of the browser-automation surface in use.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- OmniRoute must be actively running and accessible (e.g. locally at `http://localhost:20128` or on the Local VPS at `http://192.168.0.15:20128`).
|
||||
- The user must provide the target URL to be tested, or default to `http://192.168.0.15:20128`.
|
||||
|
||||
## Workflow Steps
|
||||
|
||||
### 1. Identify Target Features
|
||||
|
||||
Review the `CHANGELOG.md` for the latest version to map out the new UI elements. For example:
|
||||
|
||||
- **CLI Tools Settings**
|
||||
- **New Provider/Model Listings (e.g., Gemini 3.1, Qoder PAT)**
|
||||
- **New Feature Modals**
|
||||
|
||||
### 2. Run the Browser Subagent
|
||||
|
||||
For each identified feature, invoke the `browser_subagent` using the `default_api:browser_subagent` tool.
|
||||
**Important Task Guidelines for the Subagent:**
|
||||
|
||||
- `TaskName`: Give it a clear name like "Validate CLIProxyAPI Tool Tab".
|
||||
- `TaskSummary`: "Navigate to the CLI Tools tab and verify the new Integration settings."
|
||||
- `Task`: Provide unambiguous instructions for the subagent, such as: "Navigate to http://192.168.0.15:20128/dashboard. Click on the 'Settings' or 'CLI Tools' nav link. Scroll down to find the CLIProxyAPI integration card. Hover over it to trigger UI state. Verify the components render correctly and exit."
|
||||
- `RecordingName`: Ensure it describes the feature (e.g. `v3_4_5_cli_proxy_api`). This is required and strictly automatically saved as a WebP artifacts video by the system.
|
||||
|
||||
_(Note: The `browser_subagent` automatically creates a WebP recording named by the `RecordingName` parameter. No additional tools for screenshots are needed.)_
|
||||
|
||||
### 3. Generate Report Artifact
|
||||
|
||||
After the `browser_subagent` finishes its sessions, generate a final Markdown artifact (using `Write` and `IsArtifact=true`) to present the recordings inline to the user using the `` syntax.
|
||||
|
||||
### Example Invocation
|
||||
|
||||
\```json
|
||||
{
|
||||
"TaskName": "Validating Qoder PAT Configuration UI",
|
||||
"TaskSummary": "Validates the Qoder provider configuration modal",
|
||||
"Task": "Go to http://192.168.0.15:20128/dashboard. Click on the 'Providers' tab. Find 'Qoder' in the list. Click 'Add Token' or 'Configure'. Type 'test_token' and submit. Return when done.",
|
||||
"RecordingName": "qoder_pat_ui_validation"
|
||||
}
|
||||
\```
|
||||
45
.agents/skills/deploy-vps-local/SKILL.md
Normal file
45
.agents/skills/deploy-vps-local/SKILL.md
Normal file
@@ -0,0 +1,45 @@
|
||||
---
|
||||
name: deploy-vps-local-cx
|
||||
description: Deploy the latest OmniRoute code to the Local VPS (192.168.0.15)
|
||||
---
|
||||
|
||||
# Deploy to Local VPS Workflow
|
||||
|
||||
Deploy OmniRoute to the Local VPS using `npm pack + scp` + PM2.
|
||||
|
||||
## Codex Execution Notes
|
||||
|
||||
- Treat `// turbo` / `// turbo-all` as instructions to use `multi_tool_use.parallel` only for independent commands. Do not parallelize dependent build, copy, install, restart, and verification steps.
|
||||
- Report each remote result explicitly before finishing.
|
||||
|
||||
**Local VPS:** `192.168.0.15`
|
||||
**Process manager:** PM2 (`omniroute`)
|
||||
**Port:** `20128`
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Build + pack locally
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute && rm -f omniroute-*.tgz && rm -rf .next/cache app/.next/cache && npm run build:cli && rm -rf app/logs app/coverage app/.git app/.app-build-backup* && npm pack --ignore-scripts
|
||||
```
|
||||
|
||||
### 2. Copy to Local VPS and install
|
||||
|
||||
// turbo-all
|
||||
|
||||
```bash
|
||||
scp omniroute-*.tgz root@192.168.0.15:/tmp/
|
||||
```
|
||||
|
||||
```bash
|
||||
ssh root@192.168.0.15 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && pm2 delete omniroute 2>/dev/null; pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Local done'"
|
||||
```
|
||||
|
||||
### 3. Verify the deployment
|
||||
|
||||
```bash
|
||||
curl -s -o /dev/null -w 'LOCAL HTTP %{http_code}\n' http://192.168.0.15:20128/
|
||||
```
|
||||
362
.agents/skills/generate-release/SKILL.md
Normal file
362
.agents/skills/generate-release/SKILL.md
Normal file
@@ -0,0 +1,362 @@
|
||||
---
|
||||
name: generate-release-cx
|
||||
description: Create a new release, bump version up to the .10 patch threshold, update changelog, and manage Pull Requests
|
||||
---
|
||||
|
||||
# Generate Release Workflow
|
||||
|
||||
Bump version, finalize CHANGELOG, commit, open a **PR to main** and wait for user confirmation before tagging, publishing, and deploying.
|
||||
|
||||
## Codex Execution Notes
|
||||
|
||||
- Treat `// turbo` / `// turbo-all` as instructions to use `multi_tool_use.parallel` for independent reads, checks, and GitHub calls.
|
||||
- When the workflow says `notify_user` or `BlockedOnUser: true`, present the report/status in the final response and stop. Do not continue into the next phase until the user explicitly approves.
|
||||
|
||||
> **VERSION RULE: Always use PATCH bumps (3.x.y → 3.x.y+1)**
|
||||
> NEVER use `npm version minor` or `npm version major`.
|
||||
> Always use: `npm version patch --no-git-tag-version`
|
||||
> The threshold rule: when `y` reaches 10, bump to `3.(x+1).0` — e.g. `3.8.10` → `3.9.0`.
|
||||
|
||||
> **🔴 SINGLE BRANCH RULE**: The `release/vX.Y.Z` branch is the **ONLY** development branch for the entire release cycle. ALL work — bug fixes, feature implementations, PR integrations, issue resolutions — MUST be committed directly on this branch. Never create separate `fix/`, `feat/`, or topic branches. When running `/resolve-issues`, `/implement-features`, or `/review-prs`, always work on the current release branch.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Two-Phase Flow
|
||||
|
||||
```
|
||||
Phase 1 (automated): bump → docs → i18n → commit → push → open PR
|
||||
↕ 🛑 STOP: Notify user, wait for PR confirmation
|
||||
Phase 2 (post-merge): tag → publish → GitHub release → Docker → deploy
|
||||
```
|
||||
|
||||
**NEVER push directly to main or create tags before the user confirms the PR.**
|
||||
|
||||
---
|
||||
|
||||
## Phase 0: Security Verification (MANDATORY)
|
||||
|
||||
Before creating the release, you must ensure the codebase and supply chain are secure and free of known vulnerabilities.
|
||||
|
||||
1. **Run Local Dependencies Audit:**
|
||||
|
||||
```bash
|
||||
npm audit
|
||||
```
|
||||
|
||||
_Fix any `high` or `critical` vulnerabilities identified._
|
||||
|
||||
2. **Check GitHub CodeQL & Dependabot Alerts:**
|
||||
Navigate to the repository's **Security** tab on GitHub, or use the project's `vulnerability-scanner` skill to analyze active alerts. Ensure all static analysis findings (e.g., prototype pollution, insecure randomness, ReDoS, shell injections) are addressed and logically committed on a target branch.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Pre-Merge
|
||||
|
||||
### 1. Create release branch
|
||||
|
||||
```bash
|
||||
git checkout -b release/v3.x.y
|
||||
```
|
||||
|
||||
### 2. Determine and sync version
|
||||
|
||||
Check current version in `package.json`:
|
||||
|
||||
```bash
|
||||
grep '"version"' package.json
|
||||
```
|
||||
|
||||
> **🔴 BRANCH-VERSION PARITY RULE**: The logical version in `package.json` MUST exactly match the release branch name. For example, if you are on `release/v3.7.0`, the version in `package.json` MUST be `3.7.0`.
|
||||
>
|
||||
> - If this is the FIRST time generating a release for a new minor/major branch (e.g., bumping from 3.6.9 to 3.7.0), you MUST ensure the version is bumped to match the new branch logic.
|
||||
> - If you are just bumping a patch on the current branch (e.g., 3.6.9 to 3.6.10), use:
|
||||
> `npm version patch --no-git-tag-version`
|
||||
|
||||
> **⚠️ ATOMIC COMMIT RULE — Version bump MUST happen before committing feature files.**
|
||||
>
|
||||
> **CORRECT order:**
|
||||
>
|
||||
> 1. `npm version patch --no-git-tag-version` ← bump first
|
||||
> 2. implement features / fix bugs
|
||||
> 3. `git add -A && git commit -m "chore(release): v3.x.y — all changes in ONE commit"`
|
||||
>
|
||||
> **OR if features are already staged:**
|
||||
>
|
||||
> 1. implement features (do NOT commit yet)
|
||||
> 2. `npm version patch --no-git-tag-version` ← bump before committing
|
||||
> 3. `git add -A && git commit -m "chore(release): v3.x.y — all changes in ONE commit"`
|
||||
>
|
||||
> **NEVER do this (creates version mismatch in git history):**
|
||||
>
|
||||
> - ~~commit features → then bump version → commit package.json separately~~
|
||||
>
|
||||
> This ensures that `git show v3.x.y` always contains both code changes and the version bump together.
|
||||
> The GitHub release tag will point to a commit that includes ALL changes for that version.
|
||||
|
||||
### 3. Regenerate lock file (REQUIRED after version bump)
|
||||
|
||||
**Mandatory** — skipping causes `@swc/helpers` lock mismatch and CI failures:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 4. Finalize CHANGELOG.md
|
||||
|
||||
> **🔴 NO MIXUPS RULE**: Ensure you do NOT mix the backlog of the previous version with the new one. The new version section must ONLY contain the features and fixes for the current release.
|
||||
|
||||
Replace the `[Unreleased]` header with the new version and date.
|
||||
Keep an empty `## [Unreleased]` section above it, separated by a horizontal rule (`---`).
|
||||
|
||||
```markdown
|
||||
## [Unreleased]
|
||||
|
||||
---
|
||||
|
||||
## [3.7.0] — 2026-04-19
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- ...
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- ...
|
||||
|
||||
### 🏆 Hall de Contribuidores
|
||||
|
||||
Um agradecimento especial a todos que contribuíram com código, revisões e testes para este release:
|
||||
@user1, @user2
|
||||
|
||||
---
|
||||
|
||||
## [3.6.9] — 2026-04-19
|
||||
```
|
||||
|
||||
> **🔴 HALL DE CONTRIBUIDORES RULE**: You MUST parse all the PR author mentions (e.g., `(thanks @username)`) from the new version's changelog items, deduplicate them, and append them as a "Hall de Contribuidores" section at the end of the new release block, exactly as shown above.
|
||||
|
||||
### 5. Update openapi.yaml version ⚠️ MANDATORY
|
||||
|
||||
> **CI will fail** if `docs/reference/openapi.yaml` version ≠ `package.json` version (`check:docs-sync` enforces this).
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
sed -i "s/ version: .*/ version: $VERSION/" docs/reference/openapi.yaml
|
||||
echo "✓ openapi.yaml → $VERSION"
|
||||
|
||||
for dir in electron open-sse; do
|
||||
if [ -d "$dir" ] && [ -f "$dir/package.json" ]; then
|
||||
(cd "$dir" && npm version "$VERSION" --no-git-tag-version --allow-same-version > /dev/null)
|
||||
echo "✓ $dir/package.json → $VERSION"
|
||||
fi
|
||||
done
|
||||
# Re-run install to assert the workspace lockfile is updated
|
||||
npm install
|
||||
```
|
||||
|
||||
### 6. Update README.md and i18n docs
|
||||
|
||||
Manually perform these documentation updates (there is no `/update-docs` workflow — it was deprecated in v3.8):
|
||||
|
||||
- Update feature table rows and "What's new in vX.Y.Z" section in `README.md`
|
||||
- Sync feature changes to all 40 language `docs/i18n/*/README.md` files (use the same row edits across each translated README)
|
||||
- Update the relevant `docs/<AREA>.md` if architecture or counts changed
|
||||
- Re-run `npm run check:docs-sync` and `npm run check:docs-all` to catch drift
|
||||
|
||||
### 7. Run tests
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
All tests must pass before creating the PR.
|
||||
|
||||
### 8. Stage, commit, and push
|
||||
|
||||
// turbo-all
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "chore(release): v3.x.y — summary of changes"
|
||||
git push origin release/v3.x.y
|
||||
```
|
||||
|
||||
### 9. Open PR to main
|
||||
|
||||
### 9. Open PR to main
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
|
||||
# Extract the exact changelog entry for this version from the root CHANGELOG.md
|
||||
awk "/^## \\[$VERSION\\]/{flag=1; print; next} /^---/{if(flag) {flag=0; exit}} flag" CHANGELOG.md > /tmp/changelog_body.txt
|
||||
|
||||
# Append test status and next steps
|
||||
echo "" >> /tmp/changelog_body.txt
|
||||
echo "### Tests" >> /tmp/changelog_body.txt
|
||||
echo "- All tests pass" >> /tmp/changelog_body.txt
|
||||
echo "" >> /tmp/changelog_body.txt
|
||||
echo "### ⚠️ After merging: run Phase 2 steps to tag, publish, and deploy." >> /tmp/changelog_body.txt
|
||||
|
||||
gh pr create \
|
||||
--repo diegosouzapw/OmniRoute \
|
||||
--base main \
|
||||
--head release/v$VERSION \
|
||||
--title "Release v$VERSION" \
|
||||
--body-file /tmp/changelog_body.txt
|
||||
```
|
||||
|
||||
### 10. 🛑 STOP — Notify User & Await PR Confirmation
|
||||
|
||||
**This is a mandatory stop point.** Present the report in the final response and stop. Do not continue to the next phase until the user explicitly approves.
|
||||
|
||||
Inform the user:
|
||||
|
||||
- PR URL
|
||||
- Summary of changes
|
||||
- Test results
|
||||
- List of files changed
|
||||
|
||||
**DO NOT proceed to Phase 2 until the user confirms the PR looks good and merges it.**
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Post-Merge Validation (Local VPS)
|
||||
|
||||
> Run these steps only AFTER the user has merged the PR into `main` and all CI jobs have passed.
|
||||
|
||||
### 11. Deploy to Local VPS for Final Validation (MANDATORY)
|
||||
|
||||
Before cutting the official git tag and publishing to the world, deploy the `main` branch to the Local VPS for a final homologation test.
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
git pull origin main
|
||||
|
||||
# Build and pack locally
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute && rm -f omniroute-*.tgz && rm -rf .next/cache app/.next/cache && npm run build:cli && rm -rf app/logs app/coverage app/.git app/.app-build-backup* && npm pack --ignore-scripts
|
||||
|
||||
# Deploy to LOCAL VPS (192.168.0.15)
|
||||
scp omniroute-*.tgz root@192.168.0.15:/tmp/
|
||||
ssh root@192.168.0.15 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && pm2 delete omniroute 2>/dev/null; pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Local done'"
|
||||
|
||||
# Verify
|
||||
curl -s -o /dev/null -w "LOCAL: HTTP %{http_code}\n" http://192.168.0.15:20128/
|
||||
```
|
||||
|
||||
### 12. 🛑 STOP — Notify User & Await Final OK
|
||||
|
||||
**This is a mandatory stop point.**
|
||||
Inform the user that the `main` branch is now running on the Local VPS.
|
||||
Wait for the user to manually test and give the **OK**.
|
||||
**DO NOT proceed to Phase 3 until the user confirms the local deploy is stable.**
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Official Launch
|
||||
|
||||
> Run these steps only AFTER the user gives the final OK from the Phase 2 local validation.
|
||||
|
||||
### 13. Create Git Tag and GitHub Release (MANDATORY)
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
git pull origin main
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
|
||||
# Extracts the changelog section for this version
|
||||
NOTES=$(awk "/^## \\[$VERSION\\]/{flag=1; next} /^---/{if(flag) {flag=0; exit}} flag" CHANGELOG.md | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
|
||||
if [ -z "$NOTES" ]; then NOTES="OmniRoute v$VERSION Release"; fi
|
||||
|
||||
git tag -a "v$VERSION" -m "Release v$VERSION"
|
||||
git push origin "v$VERSION"
|
||||
gh release create "v$VERSION" --repo diegosouzapw/OmniRoute --title "v$VERSION" --notes "$NOTES" --target main || gh release edit "v$VERSION" --repo diegosouzapw/OmniRoute --title "v$VERSION" --notes "$NOTES"
|
||||
```
|
||||
|
||||
### 14. 🐳 Trigger Docker Hub build (MANDATORY — keep npm and Docker in sync)
|
||||
|
||||
> **CRITICAL**: Docker Hub and npm MUST always publish the same version.
|
||||
> The Docker image is built automatically via GitHub Actions when a new tag is pushed.
|
||||
> After pushing the tag in step 13, **verify the workflow runs**:
|
||||
|
||||
```bash
|
||||
# Verify the Docker workflow triggered
|
||||
gh run list --repo diegosouzapw/OmniRoute --workflow docker-publish.yml --limit 3
|
||||
|
||||
# Wait for the Docker build to complete (usually 5–10 min)
|
||||
gh run watch --repo diegosouzapw/OmniRoute
|
||||
```
|
||||
|
||||
### 15. Publish to NPM (Optional/Automated)
|
||||
|
||||
Normally handled by CI, but if manual publish is required:
|
||||
|
||||
```bash
|
||||
npm publish
|
||||
```
|
||||
|
||||
## Phase 4: Release Monitoring & Artifact Validation
|
||||
|
||||
> After triggering the official release, actively monitor the CI pipelines until all artifacts are successfully generated. If any pipeline fails, stop and apply the necessary corrections before continuing.
|
||||
|
||||
### 16. Monitor CI Pipelines
|
||||
|
||||
Wait for and verify the successful completion of the following automated jobs:
|
||||
|
||||
1. **Docker Hub Publish**
|
||||
2. **Electron Build**
|
||||
3. **NPM Registry Publish** (Check with `npm info omniroute version`)
|
||||
|
||||
```bash
|
||||
# Monitor Docker Hub workflow
|
||||
gh run list --repo diegosouzapw/OmniRoute --workflow docker-publish.yml --limit 1
|
||||
gh run watch <RUN_ID>
|
||||
|
||||
# Monitor Electron build
|
||||
gh run list --repo diegosouzapw/OmniRoute --workflow electron-release.yml --limit 1
|
||||
gh run watch <RUN_ID>
|
||||
|
||||
# Verify NPM version
|
||||
npm info omniroute version
|
||||
```
|
||||
|
||||
### 17. Handle Failures (If Any)
|
||||
|
||||
If a workflow fails:
|
||||
|
||||
- Use `gh run view <RUN_ID> --log-failed` to identify the error.
|
||||
- Apply the fix on the `main` branch.
|
||||
- If necessary, re-trigger the workflow using `gh workflow run <workflow_name.yml> --repo diegosouzapw/OmniRoute --ref v3.x.y`
|
||||
|
||||
### 18. Preserve release branch
|
||||
|
||||
```bash
|
||||
# Branch is kept for historical purposes. Do not delete.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Ensure CHANGELOG, README and `docs/*` are current BEFORE this workflow — run `npm run check:docs-all` and `/version-bump` first (there is no `/update-docs` workflow anymore)
|
||||
- The `prepublishOnly` script runs `npm run build:cli` automatically during `npm publish`
|
||||
- After npm publish, verify with `npm info omniroute version`
|
||||
- Lock file sync errors are caused by skipping `npm install` after version bump
|
||||
- Use `gh auth switch -u diegosouzapw` if git push fails with wrong account
|
||||
|
||||
## Known CI Pitfalls
|
||||
|
||||
| CI failure | Cause | Fix |
|
||||
| ------------------------------------------------------------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------- |
|
||||
| `[docs-sync] FAIL - OpenAPI version differs from package.json` | Skipped step 5 — `docs/reference/openapi.yaml` version not updated | Run step 5 (`sed -i ...`) and commit |
|
||||
| `[docs-sync] FAIL - CHANGELOG.md first section must be "## [Unreleased]"` | `## [Unreleased]` missing or not at top of CHANGELOG | Add `## [Unreleased]\n\n---\n` before the first versioned `## [x.y.z]` |
|
||||
| Electron Linux `.deb` build fails (`FpmTarget` error) | `fpm` Ruby gem not installed on `ubuntu-latest` runner | Already fixed in `electron-release.yml` (`gem install fpm` step) |
|
||||
| Docker Hub `502 error writing layer blob` | Transient Docker Hub network error during ARM64 push | Re-run the Docker publish workflow; no code change needed |
|
||||
713
.agents/skills/implement-features/SKILL.md
Normal file
713
.agents/skills/implement-features/SKILL.md
Normal file
@@ -0,0 +1,713 @@
|
||||
---
|
||||
name: implement-features-cx
|
||||
description: Analyze open feature request issues, implement viable ones on dedicated branches, and respond to authors
|
||||
---
|
||||
|
||||
# /implement-features — Feature Request Harvest, Research & Implementation Workflow
|
||||
|
||||
## Overview
|
||||
|
||||
A **5-phase** workflow that systematically harvests feature requests from GitHub issues, creates structured idea files, researches solutions across the internet and Git repositories, presents a consolidated report for user approval, then generates detailed implementation plans and executes them.
|
||||
|
||||
## Codex Execution Notes
|
||||
|
||||
- Treat `// turbo` / `// turbo-all` as instructions to use `multi_tool_use.parallel` for independent reads, checks, and GitHub calls.
|
||||
- Approval gates are hard stops. Present the report/plan in the final response and do not move to implementation phases until the user explicitly approves.
|
||||
- Keep harvest/research bounded enough to produce the approval report quickly; do not start implementation while still in report phases.
|
||||
|
||||
**Output directory structure:**
|
||||
|
||||
```
|
||||
_ideia/
|
||||
├── viable/ # Features approved for implementation
|
||||
│ ├── need_details/ # ❓ Good idea but waiting for author clarification (issues stay OPEN)
|
||||
│ │ └── 1015-warp-terminal-mitm.md
|
||||
│ ├── 1046-native-playground.md # ✅ Ready — researched and planned
|
||||
│ └── 1046-native-playground.requirements.md
|
||||
├── defer/ # ⏭️ Good ideas deferred for future cycles (issues CLOSED)
|
||||
│ └── 1041-smart-auto-combos.md
|
||||
└── notfit/ # ❌ Out of scope / already exists (issues CLOSED)
|
||||
└── 945-telegram-integration.md
|
||||
|
||||
_tasks/features-vX.Y.Z/ # Implementation plans (per-release)
|
||||
└── 1046-native-playground.plan.md
|
||||
```
|
||||
|
||||
> **LIFECYCLE RULE:** `viable/` files are **DELETED** once the feature is implemented — they are not moved. Only unimplemented features live in `viable/` (or `viable/need_details/`). Files in `defer/` and `notfit/` remain as permanent reference.
|
||||
|
||||
> **BRANCH RULE**: All implementation work MUST happen on the current `release/vX.Y.Z` branch. Never create separate `feat/` branches. If no release branch exists yet, create one first using `/generate-release` Phase 1 steps 1–5.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Harvest: Collect & Catalog Feature Ideas
|
||||
|
||||
### 1.1 Identify the Repository
|
||||
|
||||
// turbo
|
||||
|
||||
- Run: `git -C <project_root> remote get-url origin` to extract owner/repo.
|
||||
|
||||
### 1.2 Ensure Release Branch Exists
|
||||
|
||||
// turbo
|
||||
|
||||
Before doing any work, ensure you are on the current release branch:
|
||||
|
||||
```bash
|
||||
# Check current branch
|
||||
git branch --show-current
|
||||
|
||||
# If on main, determine next version and create the release branch
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
NEXT=$(node -p "const [a,b,c]=('$VERSION').split('.').map(Number); c>=9?a+'.'+(b+1)+'.0':a+'.'+b+'.'+(c+1)")
|
||||
git checkout -b release/v$NEXT
|
||||
npm version patch --no-git-tag-version
|
||||
npm install
|
||||
```
|
||||
|
||||
If already on a `release/vX.Y.Z` branch, continue working there.
|
||||
|
||||
### 1.3 Fetch ALL Open Feature Requests
|
||||
|
||||
// turbo-all
|
||||
|
||||
**⚠️ CRITICAL**: The JSON output of `gh issue list` can be truncated by the tool, silently hiding issues. You MUST use the two-step approach below.
|
||||
|
||||
**Step 1 — Get Issue numbers only** (small output, never truncated):
|
||||
|
||||
```bash
|
||||
# Fetch issues with feature/enhancement labels
|
||||
gh issue list --repo <owner>/<repo> --state open -l "enhancement" --limit 500 --json number --jq '.[].number'
|
||||
|
||||
# Also check for [Feature] in title (common pattern when no labels are set)
|
||||
gh issue list --repo <owner>/<repo> --state open --limit 500 --json number,title --jq '.[] | select(.title | test("\\[Feature\\]|\\[feature\\]|feature request"; "i")) | .number'
|
||||
```
|
||||
|
||||
- Merge both lists, deduplicate. Count and confirm the total.
|
||||
|
||||
**Step 2 — Fetch full metadata for each Issue** (one call per issue):
|
||||
|
||||
```bash
|
||||
gh issue view <NUMBER> --repo <owner>/<repo> --json number,title,labels,body,comments,createdAt,author,assignees
|
||||
```
|
||||
|
||||
- Read the **entire body** — including description, use cases, screenshots, mockups, and any embedded images.
|
||||
- Read **ALL comments** — community discussion, agreements, restrictions, owner responses, and linked PRs.
|
||||
- **Images**: If the body or comments contain image URLs (`` or `https://...png/jpg/gif`), note them — they may contain UI mockups, wireframes, or architecture diagrams that are essential to understanding the request.
|
||||
- You may batch these into parallel calls (up to 4 at a time).
|
||||
- Sort by oldest first (FIFO).
|
||||
|
||||
### 1.4 Create Idea Files (initially in `_ideia/` root)
|
||||
|
||||
For each feature request, create a structured idea file in `<project_root>/_ideia/`:
|
||||
|
||||
**Filename convention**: `<NUMBER>-<kebab-case-short-title>.md`
|
||||
Example: `1046-native-playground.md`, `1041-smart-auto-combos.md`
|
||||
|
||||
#### 1.4a — If the idea file does NOT exist yet, create it:
|
||||
|
||||
```markdown
|
||||
# Feature: <Title from Issue>
|
||||
|
||||
> GitHub Issue: #<NUMBER> — opened by @<author> on <date>
|
||||
> Status: 📋 Cataloged | Priority: TBD
|
||||
|
||||
## 📝 Original Request
|
||||
|
||||
<Paste the FULL issue body here, preserving all formatting, images, and code blocks>
|
||||
|
||||
## 💬 Community Discussion
|
||||
|
||||
<Summarize ALL comments chronologically, noting who said what and any decisions or objections raised>
|
||||
|
||||
### Participants
|
||||
|
||||
- @<author> — Original requester
|
||||
- @<commenter1> — <brief role/opinion>
|
||||
- ...
|
||||
|
||||
### Key Points
|
||||
|
||||
- <bullet list of the most important discussion points>
|
||||
- <agreements reached>
|
||||
- <objections raised>
|
||||
|
||||
## 🎯 Refined Feature Description
|
||||
|
||||
<YOUR interpretation and enrichment of the feature request. Expand on what was asked, fill in logical gaps, provide concrete examples of how it would work. This section should be MORE detailed and clearer than the original request.>
|
||||
|
||||
### What it solves
|
||||
|
||||
- <problem 1>
|
||||
- <problem 2>
|
||||
|
||||
### How it should work (high level)
|
||||
|
||||
1. <step 1>
|
||||
2. <step 2>
|
||||
3. ...
|
||||
|
||||
### Affected areas
|
||||
|
||||
- <list of codebase areas, modules, files likely affected>
|
||||
|
||||
## 📎 Attachments & References
|
||||
|
||||
- <any image URLs, mockup links, or external references from the issue>
|
||||
|
||||
## 🔗 Related Ideas
|
||||
|
||||
- <links to related \_ideia/ files if any overlap found>
|
||||
```
|
||||
|
||||
#### 1.4b — If the idea file ALREADY exists, update it:
|
||||
|
||||
- Append new comments from the issue to the **Community Discussion** section.
|
||||
- Update the **Refined Feature Description** if new information changes the understanding.
|
||||
- Add any new **Related Ideas** cross-references found.
|
||||
- **Do NOT overwrite** existing content — append and enrich it.
|
||||
|
||||
### 1.5 Cross-Reference & Deduplication
|
||||
|
||||
After processing all issues:
|
||||
|
||||
- Scan all `_ideia/*.md` files for overlapping features.
|
||||
- If two features are substantially the same, add `🔗 Related Ideas` cross-references to both.
|
||||
- If one is a strict subset of another, note it in the smaller file: `> ℹ️ This feature is a subset of #<OTHER_NUMBER>. Consider implementing together.`
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Research: Find Solutions & Build Requirements
|
||||
|
||||
For each cataloged idea that is **viable** (aligns with the project's goals):
|
||||
|
||||
### 2.1 Viability Pre-Check
|
||||
|
||||
Before investing in research, quickly assess:
|
||||
|
||||
- [ ] Does this feature align with the project's goals and architecture?
|
||||
- [ ] Is it technically feasible with the current codebase?
|
||||
- [ ] Does it duplicate existing functionality?
|
||||
- [ ] Would it introduce breaking changes or security risks?
|
||||
- [ ] Is there enough detail to understand what's needed?
|
||||
|
||||
**Verdict options:**
|
||||
|
||||
| Verdict | When | Action |
|
||||
| --------------------- | ------------------------------------- | --------------------------- |
|
||||
| ✅ **VIABLE** | Good idea, enough context | Proceed to Research |
|
||||
| ❓ **NEEDS DETAIL** | Good idea, insufficient spec | Skip research, ask author |
|
||||
| ⏭️ **DEFER** | Good idea, too complex for this cycle | Catalog only, skip research |
|
||||
| ❌ **NOT FIT** | Doesn't fit the project | Explain why |
|
||||
| 🔁 **ALREADY EXISTS** | Feature already implemented | Point to existing feature |
|
||||
|
||||
### 2.2 Internet Research (for VIABLE features)
|
||||
|
||||
For each viable feature, perform systematic research:
|
||||
|
||||
**Step 1 — Web search for similar implementations:**
|
||||
|
||||
```
|
||||
WebSearch("how to implement <feature description> in <tech stack>")
|
||||
WebSearch("<feature keyword> implementation nextjs typescript 2025 2026")
|
||||
WebSearch("<feature keyword> open source library npm")
|
||||
```
|
||||
|
||||
**Step 2 — Find reference Git repositories:**
|
||||
|
||||
```
|
||||
WebSearch("site:github.com <feature keyword> <tech stack> stars:>100")
|
||||
WebSearch("github <feature keyword> implementation recently updated 2026")
|
||||
```
|
||||
|
||||
- Find **up to 10 relevant repositories**, sorted by most recently updated.
|
||||
- For each repository:
|
||||
- Note the repo URL, star count, last commit date
|
||||
- Read its README and relevant source files via `WebFetch`
|
||||
- Extract the architectural approach, patterns used, and key code snippets
|
||||
|
||||
**Step 3 — Read API docs and standards:**
|
||||
|
||||
If the feature involves an external API, protocol, or standard:
|
||||
|
||||
- Find and read the official documentation
|
||||
- Note version requirements, authentication patterns, rate limits
|
||||
|
||||
### 2.3 Create Requirements File
|
||||
|
||||
For each researched feature, create a requirements file alongside its idea file:
|
||||
|
||||
**Filename**: `<NUMBER>-<kebab-case-short-title>.requirements.md`
|
||||
|
||||
```markdown
|
||||
# Requirements: <Feature Title>
|
||||
|
||||
> Feature Idea: [#<NUMBER>](./<NUMBER>-<kebab-case-short-title>.md)
|
||||
> Research Date: <YYYY-MM-DD>
|
||||
> Verdict: ✅ VIABLE
|
||||
|
||||
## 🔍 Research Summary
|
||||
|
||||
<Brief summary of what was found during research>
|
||||
|
||||
## 📚 Reference Implementations
|
||||
|
||||
| # | Repository | Stars | Last Updated | Approach | Relevance |
|
||||
| --- | ---------------- | ----- | ------------ | -------- | ------------ |
|
||||
| 1 | [repo/name](url) | ⭐ N | YYYY-MM-DD | <brief> | High/Med/Low |
|
||||
| 2 | ... | | | | |
|
||||
|
||||
### Key Patterns Found
|
||||
|
||||
- <pattern 1 with code snippet or link>
|
||||
- <pattern 2>
|
||||
|
||||
## 📐 Proposed Solution Architecture
|
||||
|
||||
### Approach
|
||||
|
||||
<Describe the chosen approach based on research findings>
|
||||
|
||||
### New Files
|
||||
|
||||
| File | Purpose |
|
||||
| --------------------- | ------------- |
|
||||
| `path/to/new/file.ts` | <description> |
|
||||
|
||||
### Modified Files
|
||||
|
||||
| File | Changes |
|
||||
| -------------------------- | -------------- |
|
||||
| `path/to/existing/file.ts` | <what changes> |
|
||||
|
||||
### Database Changes
|
||||
|
||||
- <migrations needed, if any>
|
||||
|
||||
### API Changes
|
||||
|
||||
- <new/modified endpoints, if any>
|
||||
|
||||
### UI Changes
|
||||
|
||||
- <new/modified pages/components, if any>
|
||||
|
||||
## ⚙️ Implementation Effort
|
||||
|
||||
- **Estimated complexity**: Low / Medium / High / Very High
|
||||
- **Estimated files changed**: ~N
|
||||
- **Dependencies needed**: <new npm packages, if any>
|
||||
- **Breaking changes**: Yes/No — <details>
|
||||
- **i18n impact**: <number of new translation keys>
|
||||
- **Test coverage needed**: <brief description>
|
||||
|
||||
## ⚠️ Open Questions
|
||||
|
||||
- <question 1>
|
||||
- <question 2>
|
||||
|
||||
## 🔗 External References
|
||||
|
||||
- <documentation URLs>
|
||||
- <API references>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2.5 — Organize & Respond: Sort Files and Post GitHub Comments
|
||||
|
||||
### 2.5.1 Create Directory Structure
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
mkdir -p <project_root>/_ideia/viable
|
||||
mkdir -p <project_root>/_ideia/viable/need_details
|
||||
mkdir -p <project_root>/_ideia/defer
|
||||
mkdir -p <project_root>/_ideia/notfit
|
||||
```
|
||||
|
||||
### 2.5.2 Move Idea Files to Category Subdirectories
|
||||
|
||||
After classification, move EVERY idea file to its correct subdirectory:
|
||||
|
||||
```bash
|
||||
# ✅ VIABLE — move idea + requirements files
|
||||
mv _ideia/<NUMBER>-*.md _ideia/viable/
|
||||
mv _ideia/<NUMBER>-*.requirements.md _ideia/viable/
|
||||
|
||||
# ❓ NEEDS DETAIL — viable but waiting for author response
|
||||
mv _ideia/<NUMBER>-*.md _ideia/viable/need_details/
|
||||
|
||||
# ⏭️ DEFER — move idea files only
|
||||
mv _ideia/<NUMBER>-*.md _ideia/defer/
|
||||
|
||||
# ❌ NOT FIT & 🔁 ALREADY EXISTS — move idea files only
|
||||
mv _ideia/<NUMBER>-*.md _ideia/notfit/
|
||||
```
|
||||
|
||||
No files should remain in `_ideia/` root after this step (except subdirectories).
|
||||
|
||||
### 2.5.3 Post GitHub Comments by Category
|
||||
|
||||
**Each category has a specific comment template and action:**
|
||||
|
||||
---
|
||||
|
||||
#### For 🔁 ALREADY EXISTS — Comment + CLOSE issue
|
||||
|
||||
// turbo
|
||||
|
||||
The feature already exists in the system. Explain WHERE it is and HOW to use it.
|
||||
|
||||
```markdown
|
||||
Hi @<author>! Thanks for the suggestion! 🙏
|
||||
|
||||
Great news — this functionality **already exists** in OmniRoute:
|
||||
|
||||
**📍 Where to find it:** <exact dashboard path or settings location>
|
||||
|
||||
**🔧 How to use it:**
|
||||
|
||||
1. <step 1>
|
||||
2. <step 2>
|
||||
3. <step 3>
|
||||
|
||||
If you have any trouble finding or using it, feel free to ask in a Discussion. We're always happy to help!
|
||||
|
||||
Closing this as the feature is already available. 🎉
|
||||
```
|
||||
|
||||
```bash
|
||||
gh issue close <NUMBER> --repo <owner>/<repo> --comment "<comment above>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### For ⏭️ DEFER — Comment + CLOSE issue
|
||||
|
||||
// turbo
|
||||
|
||||
Thank the user, explain the idea was cataloged, and that we'll study it before implementing.
|
||||
|
||||
```markdown
|
||||
Hi @<author>! Thanks for this thoughtful feature request! 🙏
|
||||
|
||||
We really appreciate the detailed proposal. We've **cataloged your idea** and it's now part of our improvement backlog.
|
||||
|
||||
Due to the **significant architectural impact** of this feature, we'll need to conduct thorough use-case studies and architectural analysis before we start development. This ensures we build it right and don't introduce regressions.
|
||||
|
||||
**What happens next:**
|
||||
|
||||
- Your idea is saved in our internal feature backlog
|
||||
- We'll conduct architecture studies when this area is prioritized
|
||||
- We'll notify you here when development begins
|
||||
|
||||
Thank you for contributing to OmniRoute's roadmap! Your input helps shape the product. 🚀
|
||||
```
|
||||
|
||||
```bash
|
||||
gh issue close <NUMBER> --repo <owner>/<repo> --comment "<comment above>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### For ❌ NOT FIT — Comment + CLOSE issue
|
||||
|
||||
// turbo
|
||||
|
||||
Politely explain why the feature doesn't fit the project scope.
|
||||
|
||||
```markdown
|
||||
Hi @<author>! Thanks for the suggestion! 🙏
|
||||
|
||||
After careful analysis, we've determined that this feature **falls outside OmniRoute's core scope** as a proxy/router.
|
||||
|
||||
**Reason:** <explain why — e.g., "Telegram integration belongs in the application/orchestrator layer that consumes OmniRoute's API, not inside the router itself.">
|
||||
|
||||
**Alternative:** <suggest an alternative approach if possible>
|
||||
|
||||
We appreciate you thinking of ways to improve OmniRoute! If you'd like to discuss this further, feel free to open a Discussion. 🙏
|
||||
```
|
||||
|
||||
```bash
|
||||
gh issue close <NUMBER> --repo <owner>/<repo> --comment "<comment above>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### For ❓ NEEDS DETAIL — Comment (keep OPEN)
|
||||
|
||||
// turbo
|
||||
|
||||
Ask for the specific missing details needed.
|
||||
|
||||
```markdown
|
||||
Hi @<author>! Thanks for the feature request — it's an interesting idea and we'd love to explore it further. 🙏
|
||||
|
||||
To move forward, we need a few more details:
|
||||
|
||||
1. <specific question 1>
|
||||
2. <specific question 2>
|
||||
3. <specific question 3>
|
||||
|
||||
If you know of any **open-source projects or repositories** that implement something similar, please share links — it would help us design the best solution.
|
||||
|
||||
Looking forward to your response! 🚀
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### For ✅ VIABLE — Comment (keep OPEN)
|
||||
|
||||
// turbo
|
||||
|
||||
Thank the user, confirm we've cataloged their idea, and explain it may be implemented in future versions.
|
||||
|
||||
```markdown
|
||||
Hi @<author>! Thanks for the great feature suggestion! 🙏
|
||||
|
||||
We've analyzed your request and it aligns well with OmniRoute's roadmap. We've **cataloged this feature** and it's in our implementation backlog.
|
||||
|
||||
**Status:** 📋 Cataloged for future implementation
|
||||
|
||||
This feature may be included in upcoming releases. We'll **respond to this issue and tag you** as soon as implementation begins so you can test it.
|
||||
|
||||
Thank you for helping improve OmniRoute! 🚀
|
||||
```
|
||||
|
||||
**⚠️ Do NOT close viable issues — they remain OPEN for tracking.**
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Report: Present Findings to User
|
||||
|
||||
### 3.1 🛑 MANDATORY STOP — Present Consolidated Report
|
||||
|
||||
After completing Phase 1, Phase 2, and Phase 2.5, **STOP and present the following report** in the chat. Do NOT proceed to implementation.
|
||||
|
||||
Present a structured report containing:
|
||||
|
||||
#### 3.1a — Feature Summary Table
|
||||
|
||||
| # | Issue | Title | Verdict | Location | Action |
|
||||
| --- | ----- | ----- | --------------- | ----------------------------- | ----------------------------- |
|
||||
| 1 | #N | Title | ✅ VIABLE | `_ideia/viable/` | Issue OPEN, comment posted |
|
||||
| 2 | #N | Title | ⏭️ DEFER | `_ideia/defer/` | Issue CLOSED with explanation |
|
||||
| 3 | #N | Title | ❌ NOT FIT | `_ideia/notfit/` | Issue CLOSED with explanation |
|
||||
| 4 | #N | Title | 🔁 EXISTS | `_ideia/notfit/` | Issue CLOSED with guidance |
|
||||
| 5 | #N | Title | ❓ NEEDS DETAIL | `_ideia/viable/need_details/` | Issue OPEN, questions posted |
|
||||
|
||||
#### 3.1b — Viable Features Detail
|
||||
|
||||
For each VIABLE feature, provide a brief paragraph:
|
||||
|
||||
- What was found during research
|
||||
- The proposed approach
|
||||
- Key risks or unknowns
|
||||
- Which reference repositories were most useful
|
||||
|
||||
#### 3.1c — Issues Requiring Author Feedback
|
||||
|
||||
For features marked ❓ NEEDS DETAIL, list:
|
||||
|
||||
- What specific information is missing
|
||||
- What examples or repository references would help
|
||||
|
||||
#### 3.1d — Ask for User Confirmation
|
||||
|
||||
End the report with:
|
||||
|
||||
> **Ready to proceed with implementation?**
|
||||
>
|
||||
> - Reply **"sim"** or **"yes"** to generate full implementation plans for all VIABLE features.
|
||||
> - Reply with specific issue numbers to select only certain features.
|
||||
> - Reply **"não"** or **"no"** to stop here.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — Plan: Generate Implementation Plans (after user says "yes")
|
||||
|
||||
> **⚠️ Do NOT enter this phase without explicit user approval from Phase 3.**
|
||||
|
||||
### 4.1 Create Task Directory
|
||||
|
||||
```bash
|
||||
mkdir -p <project_root>/_tasks/features-vX.Y.Z/
|
||||
```
|
||||
|
||||
### 4.2 Generate One Implementation Plan Per Feature
|
||||
|
||||
For each VIABLE feature approved by the user, create:
|
||||
|
||||
**Filename**: `_tasks/features-vX.Y.Z/<NUMBER>-<kebab-case-title>.plan.md`
|
||||
|
||||
```markdown
|
||||
# Implementation Plan: <Feature Title>
|
||||
|
||||
> Issue: #<NUMBER>
|
||||
> Idea: [\_ideia/viable/<NUMBER>-title.md](../../_ideia/viable/<NUMBER>-title.md)
|
||||
> Requirements: [\_ideia/viable/<NUMBER>-title.requirements.md](../../_ideia/viable/<NUMBER>-title.requirements.md)
|
||||
> Branch: `release/vX.Y.Z`
|
||||
|
||||
## Overview
|
||||
|
||||
<Brief description of what will be built>
|
||||
|
||||
## Pre-Implementation Checklist
|
||||
|
||||
- [ ] Read all related source files listed below
|
||||
- [ ] Confirm no conflicts with in-flight PRs
|
||||
- [ ] Verify database migration numbering
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: <Title>
|
||||
|
||||
**Files:**
|
||||
|
||||
- `path/to/file.ts` — <what to change>
|
||||
|
||||
**Details:**
|
||||
<Detailed description of the change, including code patterns to follow, function signatures, etc.>
|
||||
|
||||
### Step 2: <Title>
|
||||
|
||||
...
|
||||
|
||||
### Step N: Tests
|
||||
|
||||
**New test files:**
|
||||
|
||||
- `tests/unit/<test-file>.test.mjs` — <what to test>
|
||||
|
||||
**Test cases:**
|
||||
|
||||
- [ ] <test case 1>
|
||||
- [ ] <test case 2>
|
||||
|
||||
### Step N+1: i18n
|
||||
|
||||
**Translation keys to add:**
|
||||
|
||||
- `<namespace>.<key>` — "<English value>"
|
||||
|
||||
### Step N+2: Documentation
|
||||
|
||||
- [ ] Update CHANGELOG.md
|
||||
- [ ] Update relevant docs/ files
|
||||
|
||||
## Verification Plan
|
||||
|
||||
1. Run `npm run build` — must pass
|
||||
2. Run `npm test` — all tests must pass
|
||||
3. Run `npm run lint` — no new errors
|
||||
4. <Manual verification steps>
|
||||
|
||||
## Commit Plan
|
||||
```
|
||||
|
||||
feat: <description> (#<NUMBER>)
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
### 4.3 Present Plans for Final Approval
|
||||
|
||||
Present a summary of all generated plans:
|
||||
|
||||
> **Implementation plans generated:**
|
||||
>
|
||||
> | # | Feature | Plan File | Steps | Effort |
|
||||
> | --- | ------- | ---------------------------------------- | ------- | ------ |
|
||||
> | 1 | <title> | `_tasks/features-vX.Y.Z/N-title.plan.md` | N steps | Medium |
|
||||
>
|
||||
> Reply **"sim"** or **"yes"** to begin implementation of all features.
|
||||
> Reply with specific issue numbers to implement only certain ones.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — Execute: Implement the Plans (after user says "yes")
|
||||
|
||||
> **⚠️ Do NOT enter this phase without explicit user approval from Phase 4.**
|
||||
|
||||
### 5.1 Implement Each Feature
|
||||
|
||||
For each approved plan, execute it step by step:
|
||||
|
||||
1. **Follow the plan** — implement exactly as specified in the `.plan.md` file
|
||||
2. **Build** — Run `npm run build` after each feature to verify compilation
|
||||
3. **Test** — Run `npm test` to ensure no regressions
|
||||
4. **Commit** — Commit with: `feat: <description> (#<NUMBER>)`
|
||||
5. **Update the plan** — Mark completed steps with `[x]` in the plan file
|
||||
6. **Continue** — Move to the next feature (do NOT switch branches)
|
||||
|
||||
### 5.2 Respond to Authors (Update Viable Issues)
|
||||
|
||||
For each implemented feature, **close the issue with a final comment**:
|
||||
|
||||
````markdown
|
||||
✅ **Implemented in `release/vX.Y.Z`!**
|
||||
|
||||
Hi @<author>! Great news — your feature request has been implemented! 🎉
|
||||
|
||||
**What was done:**
|
||||
|
||||
- <bullet list of what was built>
|
||||
|
||||
**How to try it:**
|
||||
|
||||
```bash
|
||||
git fetch origin && git checkout release/vX.Y.Z
|
||||
npm install && npm run dev
|
||||
```
|
||||
````
|
||||
|
||||
This will be included in the upcoming **vX.Y.Z** release. Feel free to reopen if you spot any issues! 🚀
|
||||
|
||||
````
|
||||
|
||||
```bash
|
||||
gh issue close <NUMBER> --repo <owner>/<repo> --comment "<comment above>"
|
||||
````
|
||||
|
||||
Then **DELETE the idea file** — it has served its purpose:
|
||||
|
||||
```bash
|
||||
# ✅ Implemented files are DELETED (not moved)
|
||||
rm _ideia/viable/<NUMBER>-<title>.md
|
||||
rm _ideia/viable/<NUMBER>-<title>.requirements.md # if exists
|
||||
```
|
||||
|
||||
> **Why delete?** `viable/` only holds features that still NEED to be done. Once implemented, the commit history and CHANGELOG are the source of truth. Keeping the file would be confusing.
|
||||
|
||||
### 5.3 Finalize & Push
|
||||
|
||||
After implementing all approved features:
|
||||
|
||||
1. **Update CHANGELOG.md** on the release branch with all new feature entries
|
||||
2. Push the release branch: `git push origin release/vX.Y.Z`
|
||||
3. Run `/generate-release` workflow Phase 1 steps 7–10 (tests → commit → push → open PR to main → wait for user)
|
||||
|
||||
### 5.4 Final Summary Report
|
||||
|
||||
Present a final summary report to the user:
|
||||
|
||||
| Issue | Title | Verdict | Action | Commit |
|
||||
| ----- | ----- | --------------- | -------------------------------------------------- | --------- |
|
||||
| #N | Title | ✅ Implemented | Issue closed, idea file deleted | `abc1234` |
|
||||
| #N | Title | ⏭️ Deferred | Issue closed + saved in `_ideia/defer/` | — |
|
||||
| #N | Title | ❌ Not Fit | Issue closed + saved in `_ideia/notfit/` | — |
|
||||
| #N | Title | 🔁 Exists | Issue closed + saved in `_ideia/notfit/` | — |
|
||||
| #N | Title | ❓ Needs Detail | Issue OPEN, moved to `_ideia/viable/need_details/` | — |
|
||||
|
||||
Include:
|
||||
|
||||
- Total features harvested
|
||||
- Total ideas cataloged (`viable/need_details/` + `defer/` + `notfit/`)
|
||||
- Total features implemented (idea files deleted, issues closed)
|
||||
- Total features deferred
|
||||
- Total issues closed
|
||||
- Total issues left open (needs detail only — viable are closed after implementation)
|
||||
- Test results (pass/fail count)
|
||||
51
.agents/skills/issue-triage/SKILL.md
Normal file
51
.agents/skills/issue-triage/SKILL.md
Normal file
@@ -0,0 +1,51 @@
|
||||
---
|
||||
name: issue-triage-cx
|
||||
description: How to respond to GitHub issues with insufficient information
|
||||
---
|
||||
|
||||
# Issue Triage Workflow
|
||||
|
||||
Respond to GitHub issues that need more information before they can be investigated.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Identify issues needing triage
|
||||
|
||||
```bash
|
||||
gh issue list --state open --limit 20
|
||||
```
|
||||
|
||||
### 2. Evaluate each issue
|
||||
|
||||
Check if the issue has:
|
||||
|
||||
- Clear reproduction steps
|
||||
- Environment details (OS, Node.js version, OmniRoute version)
|
||||
- Error logs/screenshots
|
||||
- Expected vs actual behavior
|
||||
|
||||
### 3. Respond with triage template
|
||||
|
||||
For issues missing information:
|
||||
|
||||
```markdown
|
||||
Thank you for reporting this issue! To help us investigate, please provide:
|
||||
|
||||
1. **OmniRoute version**: (`omniroute --version`)
|
||||
2. **Node.js version**: (`node --version`)
|
||||
3. **Operating system**: (e.g., Ubuntu 24.04, macOS 15, Windows 11)
|
||||
4. **Installation method**: (npm, Docker, source)
|
||||
5. **Steps to reproduce**: (exact commands/actions that trigger the issue)
|
||||
6. **Error logs**: (paste relevant logs from the console)
|
||||
7. **Expected behavior**: (what should happen)
|
||||
|
||||
This will help us debug and resolve your issue faster. 🙏
|
||||
```
|
||||
|
||||
### 4. Label the issue
|
||||
|
||||
Add appropriate labels: `needs-info`, `bug`, `enhancement`, `question`, etc.
|
||||
|
||||
```bash
|
||||
gh issue edit <NUMBER> --add-label "needs-info"
|
||||
```
|
||||
173
.agents/skills/resolve-issues/SKILL.md
Normal file
173
.agents/skills/resolve-issues/SKILL.md
Normal file
@@ -0,0 +1,173 @@
|
||||
---
|
||||
name: resolve-issues-cx
|
||||
description: Fetch all open GitHub issues, analyze bugs, resolve what's possible, triage the rest, wait for user validation, then commit and release
|
||||
---
|
||||
|
||||
# /resolve-issues — Automated Issue Resolution Workflow
|
||||
|
||||
## Overview
|
||||
|
||||
This workflow fetches all open issues from the project's GitHub repository, classifies them, analyzes bugs, proposes a resolution plan, waits for user validation, and ONLY THEN implements the fixes, commits, and closes the issues on the current release branch (`release/vX.Y.Z`). It does NOT merge or release automatically — the release branch is later merged via PR to main.
|
||||
|
||||
## Codex Execution Notes
|
||||
|
||||
- Treat `// turbo` / `// turbo-all` as instructions to use `multi_tool_use.parallel` for independent reads, checks, and GitHub calls.
|
||||
- The initial report/plan is a hard stop. Do not edit code, close issues, or commit until the user explicitly approves the report.
|
||||
- Keep classification and bug analysis bounded enough to produce the user-facing report before deep implementation work.
|
||||
|
||||
> **BRANCH RULE**: All work MUST happen on the current `release/vX.Y.Z` branch. Never create separate `fix/` branches. If no release branch exists yet, create one first using `/generate-release` Phase 1 steps 1–5.
|
||||
|
||||
> **⛔ PR PROHIBITION**: If a fix is associated with a contributor's PR, you MUST merge their PR — NEVER close it and re-implement the fix yourself. See `/review-prs` workflow for the full policy. The `gh pr close` command is FORBIDDEN unless the repository owner explicitly requests it.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Identify the GitHub Repository
|
||||
|
||||
// turbo
|
||||
|
||||
- Run: `git -C <project_root> remote get-url origin` to extract the owner/repo
|
||||
- Parse the owner and repo name from the URL
|
||||
|
||||
### 2. Ensure Release Branch Exists
|
||||
|
||||
// turbo
|
||||
|
||||
Before doing any work, ensure you are on the current release branch:
|
||||
|
||||
```bash
|
||||
# Check current branch
|
||||
git branch --show-current
|
||||
|
||||
# If on main, determine next version and create the release branch
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
NEXT=$(node -p "const [a,b,c]=('$VERSION').split('.').map(Number); c>=9?a+'.'+(b+1)+'.0':a+'.'+b+'.'+(c+1)")
|
||||
git checkout -b release/v$NEXT
|
||||
npm version patch --no-git-tag-version
|
||||
npm install
|
||||
```
|
||||
|
||||
If already on a `release/vX.Y.Z` branch, continue working there.
|
||||
|
||||
### 3. Fetch All Open Issues
|
||||
|
||||
// turbo-all
|
||||
|
||||
**⚠️ CRITICAL**: The JSON output of `gh issue list` can be truncated by the tool, silently hiding issues. You MUST use the two-step approach below to guarantee **all** issues are fetched.
|
||||
|
||||
**Step 3a — Get Issue numbers only** (small output, never truncated):
|
||||
|
||||
- Run: `gh issue list --repo <owner>/<repo> --state open --limit 500 --json number --jq '.[].number'`
|
||||
- This outputs one issue number per line. Count them and confirm total.
|
||||
|
||||
**Step 3b — Fetch full metadata for each Issue** (one call per issue):
|
||||
|
||||
- For each issue number from step 3a, run:
|
||||
`gh issue view <NUMBER> --repo <owner>/<repo> --json number,title,labels,body,comments,createdAt,author`
|
||||
- You may batch these into parallel calls (up to 4 at a time).
|
||||
- Sort by oldest first (FIFO).
|
||||
|
||||
### 4. Classify Each Issue
|
||||
|
||||
For each issue, determine its type:
|
||||
|
||||
- **Bug** — Has `bug` label, or body contains error messages, stack traces, "doesn't work", "broken", "crash", "error"
|
||||
- **Feature Request** — Has `enhancement`/`feature` label, or body describes new functionality
|
||||
- **Question** — Has `question` label, or is asking "how to" something
|
||||
- **Other** — Anything else
|
||||
|
||||
Focus ONLY on **Bugs** for resolution. Feature requests and questions should be skipped with a note in the final report.
|
||||
|
||||
### 5. Deep-Read Each Bug Issue (One-by-One Analysis)
|
||||
|
||||
**IMPORTANT**: Read each bug issue thoroughly, one at a time, before moving to the next. This is NOT a batch process — each issue needs focused attention.
|
||||
|
||||
#### 5a. Understand the Problem
|
||||
|
||||
For each bug issue, perform the full analysis:
|
||||
|
||||
1. **Read the entire body** — including Description, Steps to Reproduce, Expected/Actual Behavior, Error Logs, and Screenshots
|
||||
2. **Read ALL comments** — including bot triage comments (Kilo, etc.) and owner/community responses. Pay attention to:
|
||||
- Whether someone already responded with a fix
|
||||
- Whether a community member confirmed the issue is resolved
|
||||
- Whether the issue was marked as duplicate by a bot. **WARNING: DO NOT blindly trust bot duplicate labels (e.g., kilo-duplicate). Bots make mistakes. You MUST read the full conversation and do your own independent analysis to determine if it is truly a duplicate or a distinct bug.**
|
||||
3. **Identify the claimed error** — extract the exact error message, status code, and provider/model involved
|
||||
|
||||
#### 5b. Check Information Sufficiency
|
||||
|
||||
Verify the issue contains enough to act on:
|
||||
|
||||
- [ ] Clear description of the problem
|
||||
- [ ] Steps to reproduce OR error logs
|
||||
- [ ] Provider/model/version information
|
||||
- [ ] Expected vs actual behavior
|
||||
|
||||
#### 5c. Determine Issue Disposition
|
||||
|
||||
For each bug, classify into one of 5 actions:
|
||||
|
||||
| Disposition | When to Apply | Action |
|
||||
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
|
||||
| **✅ CLOSE — Already Fixed** | Owner responded with fix + no user follow-up, OR community confirmed fix | Close with comment citing which version fixed it |
|
||||
| **✅ CLOSE — Duplicate** | You have independently verified the issue is a duplicate (do NOT rely solely on bot flags) + user provides no new info | Close referencing the original issue |
|
||||
| **✅ CLOSE — Stale** | We requested logs/info > 7 days ago with no reply | Close thanking the user, invite to reopen if needed |
|
||||
| **📝 RESPOND — Needs Info** | Issue is real but missing critical reproduction details | Comment asking for specifics per `/issue-triage` |
|
||||
| **📝 RESPOND — User Config** | Error is caused by unsupported env (Node version, wrong model path, missing API enablement) | Comment explaining the user-side fix |
|
||||
| **🔧 FIX — Code Change** | Root cause is confirmed in the codebase | Research, propose solution in report, wait for approval |
|
||||
|
||||
#### 5d. For "FIX — Code Change" Issues
|
||||
|
||||
Before coding, perform deep source analysis to formulate a plan:
|
||||
|
||||
1. **Search the codebase** — `grep_search` for error strings, relevant function names, affected files
|
||||
2. **Search the web** — for upstream API changes, SDK updates, or breaking changes that explain the bug
|
||||
3. **Read the full source file** — don't rely on grep snippets; understand the surrounding logic
|
||||
4. **Verify the root cause** — confirm the bug is reproducible based on the code, not just a user misconfiguration
|
||||
5. **Formulate a proposed solution** — detail the exact files and lines you will change and how you will solve it.
|
||||
6. **Create an Implementation Plan file** — write your proposed solution to `_tasks/features-vX.Y.Z/<ISSUE_NUMBER>-<short-description>.plan.md` (e.g. `_tasks/features-v3.7.6/1810-auto-restore-probe-failed-db.plan.md`) where `vX.Y.Z` is the current branch version. The plan should contain an Overview, Pre-Implementation Checklist, and detailed Implementation Steps (Files, Changes).
|
||||
7. **DO NOT modify the codebase yet** — wait for user approval on your report and plan first.
|
||||
|
||||
#### 5e. For "RESPOND" Issues
|
||||
|
||||
Post a substantive comment that:
|
||||
|
||||
- Acknowledges the specific error they reported
|
||||
- Explains the likely root cause
|
||||
- Provides concrete steps to resolve (version upgrade, env var fix, model path correction)
|
||||
- Asks for follow-up info if needed
|
||||
|
||||
**Do NOT post generic template responses.** Every comment should reference the user's specific error messages and environment.
|
||||
|
||||
### 6. Generate Report & Wait for Validation
|
||||
|
||||
Present a summary report to the user detailing your proposed actions. For any bugs that need fixing, explicitly explain your proposed solution (files to change and logic) and point out that it will be implemented on the release branch (`release/vX.Y.Z`) after approval.
|
||||
|
||||
| Issue | Title | Status | Proposed Action / Version |
|
||||
| ----- | ----- | ------------- | ----------------------------------------- |
|
||||
| #N | Title | ✅ Close | Already fixed / duplicate (explain why) |
|
||||
| #N | Title | 🔧 Propose | Explanation of the code fix to be applied |
|
||||
| #N | Title | 📝 Respond | Guidance comment to be posted |
|
||||
| #N | Title | ❓ Needs Info | Triage comment to be posted |
|
||||
| #N | Title | ⏭️ Skip | Feature request / not a bug |
|
||||
|
||||
> **⚠️ IMPORTANT**: Do NOT implement code changes, commit, push, or close issues at this step.
|
||||
> Wait for the user to review the proposed fixes and respond with **OK** before proceeding.
|
||||
|
||||
- If the user says **OK** or approves → Proceed to step 7
|
||||
- If the user requests changes → Adjust the proposed solution and present the report again
|
||||
- If the user rejects → Revert any accidental changes and stop
|
||||
|
||||
### 7. Implement Fixes, Run Tests & Commit (only after user approval)
|
||||
|
||||
After the user validates and gives the OK:
|
||||
|
||||
1. **Implement the fixes** — modify the codebase according to the approved plan.
|
||||
2. **Run tests** — `npm run test:all` (or the specific test file) to ensure 100% pass.
|
||||
3. **Update CHANGELOG.md** with all new bug fix entries.
|
||||
4. **Commit** each fix individually on the release branch with message format: `fix: <description> (#<issue_number>)`.
|
||||
5. **Push** the release branch: `git push origin release/vX.Y.Z`.
|
||||
6. **Close resolved issues immediately**. For each issue that was marked as Fixed, run:
|
||||
`gh issue close <NUMBER> --repo <owner>/<repo> --comment "Thank you for reporting! This issue has been fixed and will be included in the next release (vX.Y.Z)."`
|
||||
7. Likewise, close `Duplicate` issues referencing the original, close `Needs Info` if stale, and post the required comments.
|
||||
8. If the project runs automatic releases or needs a PR, proceed to run `/generate-release` workflow Phase 1 steps 7–10 (tests → commit → push → open PR to main → wait for user).
|
||||
|
||||
If NO fixes were committed, skip closing and source control steps and just conclude the workflow.
|
||||
126
.agents/skills/review-discussions/SKILL.md
Normal file
126
.agents/skills/review-discussions/SKILL.md
Normal file
@@ -0,0 +1,126 @@
|
||||
---
|
||||
name: review-discussions-cx
|
||||
description: Read all open GitHub Discussions, summarize them, respond to pending ones, and create issues from actionable feature requests
|
||||
---
|
||||
|
||||
# /review-discussions — GitHub Discussions Review & Response Workflow
|
||||
|
||||
## Overview
|
||||
|
||||
This workflow reads all open GitHub Discussions, generates a categorized summary, identifies which ones need a response, drafts and posts replies, and optionally creates issues from actionable feature requests. It follows the same flow used for Issues but adapted for the Discussions forum.
|
||||
|
||||
> **Tool mapping note (v3.8):** Where steps below say `browser_subagent`, modern runtimes should substitute with the `gh` CLI — `gh api graphql` for reading discussions and mutations for posting comments. `WebFetch` is acceptable for read-only HTML scraping when GraphQL is overkill, but prefer `gh` for any write actions.
|
||||
|
||||
## Codex Execution Notes
|
||||
|
||||
- Treat `// turbo` / `// turbo-all` as instructions to use `multi_tool_use.parallel` for independent reads and GitHub/browser fetches.
|
||||
- The summary report is a hard stop. Do not post discussion replies or create issues until the user explicitly approves.
|
||||
|
||||
// turbo-all
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Identify the GitHub Repository
|
||||
|
||||
- Run: `git -C <project_root> remote get-url origin` to extract the owner/repo
|
||||
- Parse the owner and repo name from the URL
|
||||
|
||||
### 2. Fetch All Open Discussions
|
||||
|
||||
- Use `WebFetch` to fetch `https://github.com/<owner>/<repo>/discussions`
|
||||
- Parse the discussion list to get all discussion titles, IDs, authors, categories, and dates
|
||||
- For each discussion, fetch the individual page to read the full content and all comments/replies
|
||||
|
||||
### 3. Summarize All Discussions
|
||||
|
||||
For each discussion, extract:
|
||||
|
||||
- **Title** and **#Number**
|
||||
- **Author** (GitHub username)
|
||||
- **Category** (Announcements, General, Ideas, Q&A, Show and tell)
|
||||
- **Date** created
|
||||
- **Summary** of the original post (1-2 sentences)
|
||||
- **Comments count** and key participants
|
||||
- **Your previous response** (if any)
|
||||
- **Pending action** — whether a response or follow-up is needed
|
||||
|
||||
### 4. Present Summary Report to User
|
||||
|
||||
Present the full summary to the user organized by category, using a table:
|
||||
|
||||
| # | Category | Title | Author | Date | Status |
|
||||
| --- | -------- | ----- | ------ | ------ | ----------------- |
|
||||
| #N | Ideas | Title | @user | Mar 23 | ⚠️ Needs response |
|
||||
| #N | Q&A | Title | @user | Mar 9 | ✅ Answered |
|
||||
| #N | General | Title | @user | Mar 19 | ⚠️ Needs response |
|
||||
|
||||
Highlight:
|
||||
|
||||
- **⚠️ Needs response** — No reply from maintainer, or a follow-up comment was left unanswered
|
||||
- **✅ Answered** — Maintainer already responded
|
||||
- **🐛 Bug reported** — A bug was mentioned that needs tracking
|
||||
- **💡 Actionable** — Contains a concrete feature request that could become an issue
|
||||
|
||||
### 5. Draft & Post Responses
|
||||
|
||||
For each discussion that needs a response, draft a reply following these guidelines:
|
||||
|
||||
#### Response Style
|
||||
|
||||
- **Friendly and professional** — Start with "Hey @username!"
|
||||
- **Acknowledge the contribution** — Thank the user for their input
|
||||
- **Be specific** — Reference existing features, settings, or dashboard pages if the feature already exists
|
||||
- **Provide workarounds** — If the request isn't implemented yet, suggest current alternatives
|
||||
- **Commit to action** — If the request is valid, state that you'll open an issue or add it to the roadmap
|
||||
- **Keep it concise** — 3-5 paragraphs max
|
||||
|
||||
#### Posting via Browser
|
||||
|
||||
- Use `browser_subagent` to navigate to each discussion and post the comment
|
||||
- **IMPORTANT**: When typing text in GitHub comment boxes via the browser, use only plain ASCII characters:
|
||||
- Use regular hyphens `-` instead of em-dashes
|
||||
- Use `->` instead of arrow symbols
|
||||
- Do NOT use emoji Unicode characters (the browser keyboard may fail on them)
|
||||
- Use `**bold**` and `\`code\`` markdown formatting
|
||||
- Click the green "Comment" button (or "Reply" for threaded replies) after typing
|
||||
- Verify the comment was posted by checking the page shows the new comment
|
||||
|
||||
### 6. Create Issues from Actionable Feature Requests
|
||||
|
||||
For discussions that contain concrete, actionable feature requests:
|
||||
|
||||
1. Ask the user which ones should become issues
|
||||
2. For each approved request, create a GitHub issue via `browser_subagent`:
|
||||
- Navigate to `https://github.com/<owner>/<repo>/issues/new`
|
||||
- **Title**: `<Feature Name> - <Short description>`
|
||||
- **Body** should include:
|
||||
- `## Feature Request` header
|
||||
- `**Source:** Discussion #N by @author`
|
||||
- `## Problem` — What limitation the user hit
|
||||
- `## Proposed Solution` — How it could work
|
||||
- `### Implementation Ideas` — Technical approach
|
||||
- `### Current Workarounds` — What users can do today
|
||||
- `## Additional Context` — Links to related issues/discussions
|
||||
- Add `enhancement` label
|
||||
- Click "Submit new issue" / "Create"
|
||||
3. After creation, go back to the original discussion and post a comment linking to the new issue:
|
||||
- "I've opened Issue #N to track this feature request. Follow along there for updates!"
|
||||
|
||||
### 7. Final Report
|
||||
|
||||
Present a final summary to the user:
|
||||
|
||||
| Discussion | Action Taken |
|
||||
| ---------- | ---------------------------------- |
|
||||
| #N — Title | Responded with workarounds |
|
||||
| #N — Title | Responded + created Issue #N |
|
||||
| #N — Title | Already answered, no action needed |
|
||||
| #N — Title | Responded to follow-up comment |
|
||||
|
||||
## Notes
|
||||
|
||||
- This workflow is **interactive** — always present the summary and wait for user approval before posting responses or creating issues
|
||||
- If the user says "pode responder" (or similar approval), proceed with posting all drafted responses
|
||||
- For discussions in non-English languages, respond in the same language as the original post
|
||||
- Always reference specific dashboard paths, config options, or code files when explaining existing features
|
||||
- When a discussion reveals a bug, note it separately from feature requests
|
||||
268
.agents/skills/review-prs/SKILL.md
Normal file
268
.agents/skills/review-prs/SKILL.md
Normal file
@@ -0,0 +1,268 @@
|
||||
---
|
||||
name: review-prs-cx
|
||||
description: Analyze open Pull Requests from the project's GitHub repository, generate a critical report, and optionally implement approved changes
|
||||
---
|
||||
|
||||
# /review-prs — PR Review & Analysis Workflow
|
||||
|
||||
## ⛔ ABSOLUTE PROHIBITION — Read Before Anything Else
|
||||
|
||||
> **NEVER close a contributor's PR if you intend to use ANY of their code, ideas, or fixes.**
|
||||
>
|
||||
> **NEVER manually integrate contributor code into a release branch and then close their PR.**
|
||||
>
|
||||
> These actions are **STRICTLY FORBIDDEN** under all circumstances:
|
||||
>
|
||||
> 1. ❌ Closing a PR and cherry-picking/copying its code into a release branch
|
||||
> 2. ❌ Closing a PR "because of conflicts" and re-implementing the same fix yourself
|
||||
> 3. ❌ Closing a PR and committing a "similar" solution inspired by it
|
||||
> 4. ❌ Using `gh pr close` on any PR whose content was or will be used
|
||||
>
|
||||
> **Why**: Closing a PR after taking the contributor's work means they get ZERO credit on GitHub — no "Merged" badge, no contribution graph entry, no public record. This is effectively stealing their contribution. An audit found this happened to **37 PRs** in the past.
|
||||
>
|
||||
> **The ONLY acceptable flow**: Resolve conflicts IN the contributor's branch, push fixes TO their branch, then merge THEIR PR via `gh pr merge`. See Step 7 and Step 8 for the exact procedure.
|
||||
>
|
||||
> **When to close a PR**: ONLY when the user (repository owner) explicitly requests it, OR when the PR is clearly spam/malicious, OR when the author themselves asks to close it. In ALL other cases, leave it open.
|
||||
|
||||
## Overview
|
||||
|
||||
This workflow fetches all open PRs from the project's GitHub repository, performs a critical analysis of each one, generates a detailed report, and waits for user approval before proceeding with implementation. **All improvements are committed on the current release branch** (`release/vX.Y.Z`).
|
||||
|
||||
> **BRANCH RULE**: PRs are ALWAYS merged into the current `release/vX.Y.Z` branch, NEVER directly into `main`. The release branch acts as a staging area — only after all PRs are integrated and tests pass does the release branch get merged into `main` via the `/generate-release` workflow.
|
||||
|
||||
## Codex Execution Notes
|
||||
|
||||
The source Claude command uses `// turbo` and `// turbo-all` as execution hints. In Codex, treat them explicitly as follows:
|
||||
|
||||
- `// turbo`: batch independent local reads and small `gh`/`git` calls with `multi_tool_use.parallel`.
|
||||
- `// turbo-all`: fan out independent per-PR/per-issue calls in practical batches, usually up to 4 GitHub calls at a time.
|
||||
- Do not expand Step 4 into exhaustive CI-log debugging before Step 6. Fetch numbers, metadata, diffs/review comments, quick merge/conflict status, and only inspect extra logs when they directly affect the verdict.
|
||||
- Step 6 is a hard stop. In Codex, present the report in the final response and wait for the user before Step 7/8.
|
||||
- Do not checkout PR branches, edit files, post PR comments, close PRs, merge, cherry-pick, or run broad fix/test loops until the user explicitly approves the report.
|
||||
- If `gh pr diff` is too large, record the limit and use `gh pr view --json files` plus `git fetch refs/pull/...` with `git diff --stat` / `git diff --name-status`; only read targeted hunks needed for confirmed findings.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Identify the GitHub Repository
|
||||
|
||||
- Read `package.json` to get the repository URL, or use the git remote origin URL
|
||||
// turbo
|
||||
- Run: `git -C <project_root> remote get-url origin` to extract the owner/repo
|
||||
|
||||
### 2. Ensure Release Branch Exists
|
||||
|
||||
// turbo
|
||||
|
||||
Before doing any work, ensure you are on the current release branch:
|
||||
|
||||
```bash
|
||||
# Check current branch
|
||||
git branch --show-current
|
||||
|
||||
# If on main, determine next version and create the release branch
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
# Bump patch: e.g. 3.3.11 → 3.3.12
|
||||
NEXT=$(node -p "const [a,b,c]=('$VERSION').split('.').map(Number); c>=9?a+'.'+(b+1)+'.0':a+'.'+b+'.'+(c+1)")
|
||||
git checkout -b release/v$NEXT
|
||||
npm version patch --no-git-tag-version
|
||||
npm install
|
||||
```
|
||||
|
||||
If already on a `release/vX.Y.Z` branch, continue working there.
|
||||
|
||||
### 3. Fetch Open Pull Requests
|
||||
|
||||
// turbo-all
|
||||
|
||||
**⚠️ CRITICAL**: The JSON output of `gh pr list` can be truncated by the tool, silently hiding PRs. You MUST use the two-step approach below to guarantee **all** PRs are fetched.
|
||||
|
||||
**Step 3a — Get PR numbers only** (small output, never truncated):
|
||||
|
||||
- Run: `gh pr list --repo <owner>/<repo> --state open --limit 500 --json number --jq '.[].number'`
|
||||
- This outputs one PR number per line. Count them and confirm total.
|
||||
|
||||
**Step 3b — Fetch full metadata for each PR** (one call per PR):
|
||||
|
||||
- For each PR number from step 3a, run:
|
||||
`gh pr view <NUMBER> --repo <owner>/<repo> --json number,title,author,headRefName,baseRefName,body,createdAt,additions,deletions,files`
|
||||
- You may batch these into parallel calls (up to 4 at a time).
|
||||
|
||||
**Step 3c — Fetch diffs for each PR** (one call per PR, saved to /tmp):
|
||||
|
||||
- For each PR number, run:
|
||||
`gh pr diff <NUMBER> --repo <owner>/<repo> > /tmp/pr<NUMBER>.diff`
|
||||
- Then read each diff file with the appropriate file-read tool (`Read` in Claude Code; equivalent in your agent runtime).
|
||||
|
||||
- For each open PR, collect:
|
||||
- PR number, title, author, branch, number of commits, date
|
||||
- PR description/body
|
||||
- Files changed (diff)
|
||||
- Existing review comments (from bots or humans)
|
||||
|
||||
**Verification**: Confirm the count of PRs analyzed matches the count from step 3a before proceeding.
|
||||
|
||||
### 3.5 Redirect PR Base Branches to Release Branch
|
||||
|
||||
// turbo-all
|
||||
|
||||
**⚠️ CRITICAL**: Contributors typically open PRs targeting `main`. Before analyzing or merging, redirect ALL open PRs to target the current release branch instead.
|
||||
|
||||
```bash
|
||||
# Get the current release branch name
|
||||
RELEASE_BRANCH=$(git branch --show-current) # e.g. release/v3.5.4
|
||||
|
||||
# For each open PR that targets main, change its base to the release branch
|
||||
for PR_NUM in $(gh pr list --repo <owner>/<repo> --state open --json number,baseRefName --jq '.[] | select(.baseRefName == "main") | .number'); do
|
||||
echo "Redirecting PR #$PR_NUM → $RELEASE_BRANCH"
|
||||
gh pr edit "$PR_NUM" --repo <owner>/<repo> --base "$RELEASE_BRANCH"
|
||||
done
|
||||
```
|
||||
|
||||
This ensures:
|
||||
|
||||
1. PRs merge into the release branch, not directly into `main`
|
||||
2. Merge conflict detection is accurate against the release branch
|
||||
3. The release branch accumulates all changes before the final merge to `main`
|
||||
4. If the release branch doesn't exist on remote yet, push it first: `git push origin $RELEASE_BRANCH`
|
||||
|
||||
### 4. Analyze Each PR — For each open PR, perform the following analysis:
|
||||
|
||||
#### 4a. Feature Assessment
|
||||
|
||||
- **Does it make sense?** Evaluate if the feature fills a real gap or solves a valid problem
|
||||
- **Alignment** — Check if it aligns with the project's architecture and roadmap
|
||||
- **Complexity** — Assess if the scope is reasonable or if it should be split
|
||||
|
||||
#### 4b. Code Quality Review
|
||||
|
||||
- Check for code duplication
|
||||
- Evaluate error handling patterns (consistent with existing codebase?)
|
||||
- Check naming conventions and code style
|
||||
- Verify TypeScript types (any `any` usage, missing types?)
|
||||
|
||||
#### 4c. Security Review
|
||||
|
||||
- Check for missing authentication/authorization on new endpoints
|
||||
- Check for injection vulnerabilities (URL params, SQL, XSS)
|
||||
- Verify input validation on all user-controlled data
|
||||
- Check for hardcoded secrets or credentials
|
||||
|
||||
#### 4d. Architecture Review
|
||||
|
||||
- Does the change follow existing patterns?
|
||||
- Are there any breaking changes to public APIs?
|
||||
- Is the database schema affected? Migration needed?
|
||||
- Impact on performance (N+1 queries, missing indexes?)
|
||||
|
||||
#### 4e. Test Coverage
|
||||
|
||||
- Does the PR include tests?
|
||||
- Are edge cases covered?
|
||||
- Would existing tests break?
|
||||
|
||||
#### 4f. Cross-Layer (Global) Analysis
|
||||
|
||||
Perform a **global impact assessment** to verify whether the PR changes are complete across all layers of the application:
|
||||
|
||||
- **Backend → Frontend check**: If the PR adds or modifies backend-only resources (new endpoints, services, data models), evaluate whether corresponding frontend changes are missing:
|
||||
- Does a new endpoint require a new screen/page in the dashboard?
|
||||
- Should there be a new action button, menu item, or navigation link?
|
||||
- Are there new data fields that should be displayed or editable in the UI?
|
||||
- Does a new feature need a toggle, configuration panel, or status indicator?
|
||||
- **Frontend → Backend check**: If the PR adds frontend elements, verify the backend support exists:
|
||||
- Are the required API endpoints implemented?
|
||||
- Is the data model sufficient for the new UI components?
|
||||
- **Cross-cutting concerns**: Check shared layers (types, DTOs, validation schemas, routes, middleware) for completeness
|
||||
- **Document gaps** — If missing layers are detected, list them as **IMPORTANT** issues in the report with concrete suggestions for what should be added
|
||||
|
||||
### 5. Generate Report — Create a markdown report for each PR including:
|
||||
|
||||
- **PR Summary** — What it does, files affected, commit count
|
||||
- **Improvements/Benefits** — Numbered list with impact level (HIGH/MEDIUM/LOW)
|
||||
- **Risks & Issues** — Categorized as CRITICAL / IMPORTANT / MINOR
|
||||
- **Scoring Table** — Rate across: Feature Relevance, Code Quality, Security, Robustness, Tests
|
||||
- **Verdict** — Ready to merge? With mandatory vs optional fixes
|
||||
- **Next Steps** — What will happen if approved
|
||||
|
||||
### 6. Present to User
|
||||
|
||||
- Show the report in the final response and stop. Mark this as a blocking checkpoint awaiting explicit user approval before continuing.
|
||||
- Wait for user decision:
|
||||
- **Approved** → Proceed to step 7
|
||||
- **Approved with changes** → Implement the fixes and corrections before merging
|
||||
- **Rejected** → Close the PR or leave a review comment
|
||||
|
||||
### 7. Pre-Merge Fixes & CI Green-Lighting (if approved)
|
||||
|
||||
> **⚠️ Fixes and Conflict Resolutions MUST be pushed back to the PR branch before merging.** We want the PR itself to be green and fully valid before it integrates.
|
||||
|
||||
- **Sync latest fixes & Resolve Conflicts:** Merge the current `release` branch into the PR branch. If there are merge conflicts, you MUST resolve them inside the author's PR branch. NEVER resolve conflicts by closing their PR and doing the work in a separate branch, as this steals credit from the original author.
|
||||
- **Implement improvements:** Apply the required fixes identified in the analysis directly on the PR branch (e.g., adding missing API routes, fixing SSRF, applying comments from other agents).
|
||||
- **Pushing changes to PR branches:**
|
||||
|
||||
```bash
|
||||
# Checkout the PR locally
|
||||
gh pr checkout <NUMBER>
|
||||
|
||||
# Apply fixes, commit your changes
|
||||
git commit -m "chore: apply review suggestions and missing layers"
|
||||
|
||||
# Attempt to push directly to the PR branch
|
||||
git push
|
||||
```
|
||||
|
||||
- **Fallback (ONLY for external forks without maintainer edit access):**
|
||||
Using `cherry-pick` instead of fixing the contributor's PR directly is a **LAST RESORT**. You MUST ALWAYS attempt to `git push` your fixes to their branch first.
|
||||
**ONLY if `git push` explicitly fails with a permission/access error** (meaning the contributor unchecked "Allow edits from maintainers" or it's a locked fork), you may use `git cherry-pick` to bring their changes into the release branch and fix the issues locally.
|
||||
Even then, ensure you preserve the contributor's authorship (`git commit --author="Contributor Name <email>"` if creating new commits).
|
||||
Once you have integrated their work into the release branch, **DO NOT close their PR**. Leave it open so the contributor retains credit. Under NO CIRCUMSTANCES should you use `gh pr close`.
|
||||
|
||||
- Run the project's test suite locally to verify nothing breaks:
|
||||
// turbo
|
||||
- Run: `npm test` or equivalent test command
|
||||
|
||||
### 8. Merge into Release Branch (NEVER CLOSE!)
|
||||
|
||||
> **⚠️ CRITICAL**: NEVER use `gh pr close` for a PR whose idea or code was accepted. Closing a PR in a contributor's face after taking their idea—or closing it just because it had conflicts—is unacceptable.
|
||||
> You MUST ALWAYS resolve conflicts and apply fixes ON THE AUTHOR'S PR BRANCH (unless explicitly locked from edits), and then merge the PR using GitHub so the contributor gets the official "Merged" badge and proper credit on their profile. **Do not use cherry-pick just because it is "easier" than resolving conflicts on their branch.**
|
||||
|
||||
Even if the PR had severe conflicts or required significant architectural adjustments, you MUST:
|
||||
|
||||
1. Resolve any conflicts and apply the fixes directly to their PR branch (as detailed in step 7) or use cherry-picking into the release branch.
|
||||
2. If you managed to fix their branch, merge it into the release branch using the GitHub CLI:
|
||||
`gh pr merge <NUMBER> --repo <owner>/<repo> --squash --body "Integrated into release/vX.Y.Z"`
|
||||
3. If you had to use cherry-picking because you couldn't push to their branch, DO NOT close the PR. GitHub will sometimes auto-detect the cherry-picked commits and mark it as Merged. If it doesn't, leave it open. The repository owner will handle it. NEVER run `gh pr close`.
|
||||
|
||||
In ALL cases:
|
||||
|
||||
- Post a **thank-you comment** on the PR via the GitHub API before or immediately after merging.
|
||||
- The message should:
|
||||
- Thank the author by name/username for their contribution.
|
||||
- Explain what was adjusted or improved (if we pushed fixes to their branch or cherry-picked).
|
||||
- Note it will be included in the upcoming release.
|
||||
- Be friendly, professional, and encouraging.
|
||||
|
||||
> **⚠️ MANDATORY CHANGELOG CREDIT**: When cherry-picking is used (because the PR branch couldn't be pushed to or `gh pr merge` failed), the contributor does NOT get the automatic GitHub "Merged" badge. In this case, you MUST compensate by adding an explicit entry to `CHANGELOG.md` in the `[Unreleased]` section with `(#PR_NUMBER — thanks @username)` format. This ensures the contributor gets public credit in the release notes even if GitHub doesn't auto-detect the cherry-pick. This is NOT optional — skipping it effectively erases the contributor's work from the release record.
|
||||
|
||||
### 9. Sync Local Release Branch
|
||||
|
||||
After merging PRs, sync the local release branch to include the new changes:
|
||||
|
||||
```bash
|
||||
git fetch origin
|
||||
git pull origin release/vX.Y.Z
|
||||
```
|
||||
|
||||
### 10. Continue or Finalize
|
||||
|
||||
After processing all approved PRs:
|
||||
|
||||
- If more PRs remain, go back to step 7
|
||||
- When all PRs are processed, **update CHANGELOG.md** on the release branch with all new entries
|
||||
- Run **test coverage** to verify the gate (≥75% statements/lines/functions, ≥70% branches — measured ~82%):
|
||||
```bash
|
||||
npm run test:coverage
|
||||
```
|
||||
- Fix any test regressions introduced by merged PRs
|
||||
- Run `/generate-release` workflow Phase 1 steps 7–10 (tests → commit → push → open PR to main → wait for user)
|
||||
- The `/generate-release` workflow handles the final merge from `release/vX.Y.Z` → `main`
|
||||
347
.agents/skills/version-bump/SKILL.md
Normal file
347
.agents/skills/version-bump/SKILL.md
Normal file
@@ -0,0 +1,347 @@
|
||||
---
|
||||
name: version-bump-cx
|
||||
description: Bump version, auto-generate CHANGELOG from git commits, update all versioned files, and refresh root + docs/ documentation to reflect the current project state
|
||||
---
|
||||
|
||||
# Version Bump Workflow
|
||||
|
||||
Automatically bump the project version, generate CHANGELOG entries from git history since the last tag, update every file that references the version, and refresh project documentation to reflect the current state.
|
||||
|
||||
## Codex Execution Notes
|
||||
|
||||
- Treat `// turbo` / `// turbo-all` as instructions to use `multi_tool_use.parallel` for independent reads, checks, and GitHub calls.
|
||||
- Any user-approval phase is a hard stop: present the report/status in the final response and wait before committing, pushing, tagging, publishing, or deploying.
|
||||
|
||||
> **VERSION RULE: Always use PATCH bumps (3.x.y → 3.x.y+1)**
|
||||
> NEVER use `npm version minor` or `npm version major`.
|
||||
> Always use: `npm version patch --no-git-tag-version`
|
||||
> The threshold rule: when `y` reaches 10, bump to `3.(x+1).0` — e.g. `3.4.10` → `3.5.0`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Determine Version
|
||||
|
||||
### 1. Read current version and last tag
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute
|
||||
CURRENT_VERSION=$(node -p "require('./package.json').version")
|
||||
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
|
||||
CURRENT_BRANCH=$(git branch --show-current)
|
||||
echo "Current version: $CURRENT_VERSION"
|
||||
echo "Last tag: $LAST_TAG"
|
||||
echo "Current branch: $CURRENT_BRANCH"
|
||||
```
|
||||
|
||||
### 2. Calculate new version
|
||||
|
||||
Apply the patch bump rule:
|
||||
|
||||
- If the current patch number is `9`, the new version is `3.(minor+1).0`
|
||||
- Otherwise, increment patch: `3.x.y` → `3.x.(y+1)`
|
||||
|
||||
If the version was ALREADY bumped (e.g. you are on a release branch and package.json already has the new version), **skip the npm version bump** and use the existing version.
|
||||
|
||||
### 3. Bump package.json (if needed)
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
# Only if version hasn't been bumped yet
|
||||
npm version patch --no-git-tag-version
|
||||
```
|
||||
|
||||
Or for threshold (y=10):
|
||||
|
||||
```bash
|
||||
# Manual threshold bump
|
||||
VERSION="3.X.0" # compute manually
|
||||
npm version "$VERSION" --no-git-tag-version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Generate CHANGELOG from Git History
|
||||
|
||||
### 4. Collect commits since last tag
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute
|
||||
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null)
|
||||
echo "=== Commits since $LAST_TAG ==="
|
||||
git log "$LAST_TAG"..HEAD --pretty=format:"%h %s" --no-merges | head -100
|
||||
echo ""
|
||||
echo "=== Merge commits ==="
|
||||
git log "$LAST_TAG"..HEAD --merges --pretty=format:"%h %s" | head -50
|
||||
```
|
||||
|
||||
### 5. Classify commits and generate CHANGELOG section
|
||||
|
||||
Analyze each commit message and classify into categories based on the conventional-commit prefix and content:
|
||||
|
||||
| Category | Patterns |
|
||||
| ------------------- | ------------------------------------------------ |
|
||||
| ✨ New Features | `feat:`, `feat(*):` |
|
||||
| 🐛 Bug Fixes | `fix:`, `fix(*):` |
|
||||
| ⚠️ Breaking Changes | `BREAKING CHANGE`, `!:` suffix |
|
||||
| 🛠️ Maintenance | `chore:`, `refactor:`, `perf:`, `build:` |
|
||||
| 🧪 Tests | `test:`, `tests:` |
|
||||
| 📝 Documentation | `docs:` |
|
||||
| 🔒 Security | `security:`, CVE references, vulnerability fixes |
|
||||
| 🌍 i18n | translation updates, locale changes |
|
||||
|
||||
For each category with entries, create a markdown section with descriptive bullet points. Use the commit messages but rewrite them to be human-readable and descriptive (not raw commit messages).
|
||||
|
||||
**If a commit references a PR number** (e.g. `#880`, `PR #885`), include it in the description.
|
||||
|
||||
### 6. Update CHANGELOG.md
|
||||
|
||||
Replace the `## [Unreleased]` section content with the generated entries, then add the new versioned section:
|
||||
|
||||
```markdown
|
||||
## [Unreleased]
|
||||
|
||||
---
|
||||
|
||||
## [NEW_VERSION] — YYYY-MM-DD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **Feature name:** Description (#PR)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **Fix name:** Description (#PR)
|
||||
|
||||
### 🛠️ Maintenance
|
||||
|
||||
- **Item:** Description
|
||||
|
||||
---
|
||||
|
||||
## [PREVIOUS_VERSION] — YYYY-MM-DD
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
The date must be today's date in `YYYY-MM-DD` format.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Sync Version Across All Files
|
||||
|
||||
### 7. Update workspace package.json files and openapi.yaml
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
|
||||
# Update docs/reference/openapi.yaml version
|
||||
sed -i "s/ version: .*/ version: $VERSION/" docs/reference/openapi.yaml
|
||||
echo "✓ docs/reference/openapi.yaml → $VERSION"
|
||||
|
||||
# Update workspace packages (open-sse, electron)
|
||||
for dir in electron open-sse; do
|
||||
if [ -d "$dir" ] && [ -f "$dir/package.json" ]; then
|
||||
(cd "$dir" && npm version "$VERSION" --no-git-tag-version --allow-same-version > /dev/null)
|
||||
echo "✓ $dir/package.json → $VERSION"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "✓ All workspace packages synced to $VERSION"
|
||||
```
|
||||
|
||||
### 8. Update llm.txt version references
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
OLD_VERSION_PATTERN='[0-9]\+\.[0-9]\+\.[0-9]\+'
|
||||
|
||||
# Update "Current version:" line
|
||||
sed -i "s/\*\*Current version:\*\* $OLD_VERSION_PATTERN/**Current version:** $VERSION/" llm.txt
|
||||
|
||||
# Update "Key Features (vX.Y.Z)" header
|
||||
sed -i "s/## Key Features (v$OLD_VERSION_PATTERN)/## Key Features (v$VERSION)/" llm.txt
|
||||
|
||||
echo "✓ llm.txt → $VERSION"
|
||||
```
|
||||
|
||||
### 9. Regenerate lock file
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute
|
||||
npm install
|
||||
echo "✓ Lock file regenerated"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Update Root Documentation
|
||||
|
||||
Based on the CHANGELOG entries generated in Phase 2, review and update these root-level files if relevant changes warrant updates:
|
||||
|
||||
### 10. Review and update root documentation files
|
||||
|
||||
For each file below, read the current content and determine if the CHANGELOG entries require any updates. Only modify files where substantive changes have occurred:
|
||||
|
||||
| File | When to update |
|
||||
| ----------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `README.md` | New providers, major features, stats changes (test count, provider count), badges, installation instructions, feature table |
|
||||
| `AGENTS.md` | Architecture changes, new modules, new commands, new providers, new services/handlers/executors |
|
||||
| `CONTRIBUTING.md` | Dev workflow changes, new tooling, test infrastructure changes |
|
||||
| `SECURITY.md` | Security fixes, new auth mechanisms, vulnerability disclosures |
|
||||
| `llm.txt` | Provider count changes, new features, architecture changes |
|
||||
|
||||
**Update rules:**
|
||||
|
||||
- **README.md**: Update provider count, test count, feature highlights table, badges if any numbers changed. If a new provider was added, add it to the provider table. If a major feature was added, add it to the features section.
|
||||
- **AGENTS.md**: If new architecture components (handlers, executors, services, DB modules) were added, update the Architecture section. If new commands were added, update the Build/Test table.
|
||||
- **SECURITY.md**: Add new vulnerability fixes or security improvements to the relevant section.
|
||||
- **llm.txt**: Update provider count, feature list, version references.
|
||||
|
||||
### 11. Review and update docs/ files (excluding i18n/)
|
||||
|
||||
For each file in `docs/` (excluding `docs/i18n/`), review if CHANGELOG changes affect it:
|
||||
|
||||
| File | When to update |
|
||||
| --------------------------------------------- | ------------------------------------------------------------------ |
|
||||
| `docs/reference/API_REFERENCE.md` | New API endpoints, changed request/response formats |
|
||||
| `docs/architecture/ARCHITECTURE.md` | New modules, new services, changed data flow |
|
||||
| `docs/architecture/CODEBASE_DOCUMENTATION.md` | New files, architectural changes, module reorganization |
|
||||
| `docs/architecture/REPOSITORY_MAP.md` | New folders / files / one-line descriptions |
|
||||
| `docs/reference/CLI-TOOLS.md` | New CLI tool integrations, config format changes |
|
||||
| `docs/guides/USER_GUIDE.md` | UX changes, new dashboard pages, settings changes |
|
||||
| `docs/reference/PROVIDER_REFERENCE.md` | New providers (regenerate via `scripts/gen-provider-reference.ts`) |
|
||||
| `docs/frameworks/MCP-SERVER.md` | New MCP tools, changed tool signatures, scope changes |
|
||||
| `docs/frameworks/A2A-SERVER.md` | New A2A skills, protocol changes |
|
||||
| `docs/frameworks/AGENT_PROTOCOLS_GUIDE.md` | New external agent protocols supported |
|
||||
| `docs/frameworks/CLOUD_AGENT.md` | Cloud agent additions (codex-cloud, devin, jules) or API changes |
|
||||
| `docs/architecture/AUTHZ_GUIDE.md` | New route classifications, policy changes |
|
||||
| `docs/security/GUARDRAILS.md` | New guardrails registered, priority/order changes |
|
||||
| `docs/security/COMPLIANCE.md` | Audit log / retention / no-log policy changes |
|
||||
| `docs/frameworks/SKILLS.md` | Skill framework / registry / built-in skill changes |
|
||||
| `docs/frameworks/MEMORY.md` | Memory pipeline / extraction / injection / Qdrant changes |
|
||||
| `docs/frameworks/EVALS.md` | Evaluation framework changes, new evaluators |
|
||||
| `docs/frameworks/WEBHOOKS.md` | New webhook events, payload schema changes |
|
||||
| `docs/routing/REASONING_REPLAY.md` | Reasoning capture/replay pipeline changes |
|
||||
| `docs/routing/AUTO-COMBO.md` | Routing changes, new strategies, scoring weight changes |
|
||||
| `docs/architecture/RESILIENCE_GUIDE.md` | Circuit breaker / cooldown / lockout behavior changes |
|
||||
| `docs/security/STEALTH_GUIDE.md` | TLS / CLI fingerprint changes |
|
||||
| `docs/ops/TUNNELS_GUIDE.md` | Cloudflare tunnel feature changes |
|
||||
| `docs/guides/ELECTRON_GUIDE.md` | Electron build / signing / packaging changes |
|
||||
| `docs/guides/TROUBLESHOOTING.md` | New known issues, resolved problems |
|
||||
| `docs/ops/RELEASE_CHECKLIST.md` | Process changes |
|
||||
| `docs/ops/COVERAGE_PLAN.md` | Coverage gate adjustments, target metrics |
|
||||
| `docs/reference/openapi.yaml` | Already updated in step 7 |
|
||||
|
||||
**Only update files where the CHANGELOG entries directly affect the documented content.** Do NOT update files just to bump a version number — only when the documented behavior, features, or architecture has actually changed.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Verify
|
||||
|
||||
### 12. Run lint check
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### 13. Run tests
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute
|
||||
npm test
|
||||
```
|
||||
|
||||
### 14. Verify version sync across all files
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
echo "Expected version: $VERSION"
|
||||
echo ""
|
||||
|
||||
echo "--- package.json ---"
|
||||
grep '"version"' package.json | head -1
|
||||
|
||||
echo "--- open-sse/package.json ---"
|
||||
grep '"version"' open-sse/package.json | head -1
|
||||
|
||||
echo "--- electron/package.json ---"
|
||||
[ -f electron/package.json ] && grep '"version"' electron/package.json | head -1
|
||||
|
||||
echo "--- docs/reference/openapi.yaml ---"
|
||||
grep " version:" docs/reference/openapi.yaml | head -1
|
||||
|
||||
echo "--- llm.txt ---"
|
||||
grep "Current version:" llm.txt
|
||||
|
||||
echo "--- CHANGELOG.md (first versioned entry) ---"
|
||||
grep "^## \[" CHANGELOG.md | head -2
|
||||
```
|
||||
|
||||
### 15. 🛑 STOP — Present Summary to User
|
||||
|
||||
**STOP** and present a summary to the user including:
|
||||
|
||||
- Old version → New version
|
||||
- CHANGELOG entries generated
|
||||
- Files modified
|
||||
- Test results
|
||||
- Any documentation updates made
|
||||
|
||||
**Wait for the user to confirm before committing.**
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Commit (only after user approval)
|
||||
|
||||
### 16. Stage and commit
|
||||
|
||||
// turbo-all
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute
|
||||
git add -A
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
git commit -m "chore(release): bump to v$VERSION — changelog, docs, version sync"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- This workflow does **NOT** create tags, releases, or deploy. Use `/generate-release` for the full release cycle after this.
|
||||
- This workflow does **NOT** update `docs/i18n/` translations. Translation updates are handled manually or via release tooling — there is no `/update-i18n` workflow shipped in this repo.
|
||||
- The CHANGELOG generation is based on git commits since the last tag. If there are no new commits, the workflow should inform the user and stop.
|
||||
- Always verify the generated CHANGELOG entries make sense — raw commit messages may need rewriting for clarity.
|
||||
- If the version was already bumped (e.g. you're on a `release/vX.Y.Z` branch), skip the `npm version` step and use the existing version.
|
||||
|
||||
## Version Touchpoints Checklist
|
||||
|
||||
| File | Field/Pattern |
|
||||
| ----------------------------- | ----------------------------------------------------------- |
|
||||
| `package.json` | `"version": "X.Y.Z"` |
|
||||
| `open-sse/package.json` | `"version": "X.Y.Z"` |
|
||||
| `electron/package.json` | `"version": "X.Y.Z"` |
|
||||
| `docs/reference/openapi.yaml` | `version: X.Y.Z` |
|
||||
| `llm.txt` | `**Current version:** X.Y.Z` and `## Key Features (vX.Y.Z)` |
|
||||
| `CHANGELOG.md` | `## [X.Y.Z] — YYYY-MM-DD` |
|
||||
51
.agents/workflows/capture-release-evidences-ag.md
Normal file
51
.agents/workflows/capture-release-evidences-ag.md
Normal file
@@ -0,0 +1,51 @@
|
||||
---
|
||||
description: Automatically run a browser-automation agent to visually validate all new UI features from the current release and capture evidence WebP recordings of the changes.
|
||||
---
|
||||
|
||||
# Capture Release Evidences Workflow
|
||||
|
||||
Use this workflow to automatically drive a browser-automation agent to explore the newly deployed or locally running application and record evidence of the UI changes introduced in the latest release.
|
||||
|
||||
> **Tool mapping note (v3.8):** The `browser_subagent` tool referenced below is specific to an earlier agent runtime. In Claude Code, substitute with the available browser MCP tools (e.g. `mcp__claude-in-chrome__*`) for navigation/screenshots, plus the `Write` tool for saving artifacts. The high-level steps remain the same regardless of the browser-automation surface in use.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- OmniRoute must be actively running and accessible (e.g. locally at `http://localhost:20128` or on the Local VPS at `http://192.168.0.15:20128`).
|
||||
- The user must provide the target URL to be tested, or default to `http://192.168.0.15:20128`.
|
||||
|
||||
## Workflow Steps
|
||||
|
||||
### 1. Identify Target Features
|
||||
|
||||
Review the `CHANGELOG.md` for the latest version to map out the new UI elements. For example:
|
||||
|
||||
- **CLI Tools Settings**
|
||||
- **New Provider/Model Listings (e.g., Gemini 3.1, Qoder PAT)**
|
||||
- **New Feature Modals**
|
||||
|
||||
### 2. Run the Browser Subagent
|
||||
|
||||
For each identified feature, invoke the `browser_subagent` using the `default_api:browser_subagent` tool.
|
||||
**Important Task Guidelines for the Subagent:**
|
||||
|
||||
- `TaskName`: Give it a clear name like "Validate CLIProxyAPI Tool Tab".
|
||||
- `TaskSummary`: "Navigate to the CLI Tools tab and verify the new Integration settings."
|
||||
- `Task`: Provide unambiguous instructions for the subagent, such as: "Navigate to http://192.168.0.15:20128/dashboard. Click on the 'Settings' or 'CLI Tools' nav link. Scroll down to find the CLIProxyAPI integration card. Hover over it to trigger UI state. Verify the components render correctly and exit."
|
||||
- `RecordingName`: Ensure it describes the feature (e.g. `v3_4_5_cli_proxy_api`). This is required and strictly automatically saved as a WebP artifacts video by the system.
|
||||
|
||||
_(Note: The `browser_subagent` automatically creates a WebP recording named by the `RecordingName` parameter. No additional tools for screenshots are needed.)_
|
||||
|
||||
### 3. Generate Report Artifact
|
||||
|
||||
After the `browser_subagent` finishes its sessions, generate a final Markdown artifact (using `Write` and `IsArtifact=true`) to present the recordings inline to the user using the `` syntax.
|
||||
|
||||
### Example Invocation
|
||||
|
||||
\```json
|
||||
{
|
||||
"TaskName": "Validating Qoder PAT Configuration UI",
|
||||
"TaskSummary": "Validates the Qoder provider configuration modal",
|
||||
"Task": "Go to http://192.168.0.15:20128/dashboard. Click on the 'Providers' tab. Find 'Qoder' in the list. Click 'Add Token' or 'Configure'. Type 'test_token' and submit. Return when done.",
|
||||
"RecordingName": "qoder_pat_ui_validation"
|
||||
}
|
||||
\```
|
||||
39
.agents/workflows/deploy-vps-local-ag.md
Normal file
39
.agents/workflows/deploy-vps-local-ag.md
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
description: Deploy the latest OmniRoute code to the Local VPS (192.168.0.15)
|
||||
---
|
||||
|
||||
# Deploy to Local VPS Workflow
|
||||
|
||||
Deploy OmniRoute to the Local VPS using `npm pack + scp` + PM2.
|
||||
|
||||
**Local VPS:** `192.168.0.15`
|
||||
**Process manager:** PM2 (`omniroute`)
|
||||
**Port:** `20128`
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Build + pack locally
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute && rm -f omniroute-*.tgz && rm -rf .next/cache app/.next/cache && npm run build:cli && rm -rf app/logs app/coverage app/.git app/.app-build-backup* && npm pack --ignore-scripts
|
||||
```
|
||||
|
||||
### 2. Copy to Local VPS and install
|
||||
|
||||
// turbo-all
|
||||
|
||||
```bash
|
||||
scp omniroute-*.tgz root@192.168.0.15:/tmp/
|
||||
```
|
||||
|
||||
```bash
|
||||
ssh root@192.168.0.15 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && pm2 delete omniroute 2>/dev/null; pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Local done'"
|
||||
```
|
||||
|
||||
### 3. Verify the deployment
|
||||
|
||||
```bash
|
||||
curl -s -o /dev/null -w 'LOCAL HTTP %{http_code}\n' http://192.168.0.15:20128/
|
||||
```
|
||||
@@ -1,76 +0,0 @@
|
||||
---
|
||||
description: Deploy the latest OmniRoute code to the Akamai VPS (69.164.221.35) via npm
|
||||
---
|
||||
|
||||
# Deploy to VPS Workflow
|
||||
|
||||
Deploy OmniRoute to the production VPS using `npm install -g` + PM2.
|
||||
|
||||
**VPS:** `69.164.221.35` (Akamai, Ubuntu 24.04, 1GB RAM + 2.5GB swap)
|
||||
**Local VPS:** `192.168.0.15` (same setup)
|
||||
**Process manager:** PM2 (`omniroute`)
|
||||
**Port:** `20128`
|
||||
|
||||
> [!IMPORTANT]
|
||||
> PM2 runs from the global npm package at `/usr/lib/node_modules/omniroute`.
|
||||
> **DO NOT** use git clone or local copies. The `npm install -g` command handles
|
||||
> building, publishing, and installing the standalone app in one step.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Publish to npm
|
||||
|
||||
Ensure the version in `package.json` is bumped and the package is published:
|
||||
|
||||
```bash
|
||||
npm publish
|
||||
```
|
||||
|
||||
### 2. Install on VPS and restart PM2
|
||||
|
||||
// turbo-all
|
||||
|
||||
```bash
|
||||
ssh root@69.164.221.35 "npm install -g omniroute@latest && pm2 restart omniroute && pm2 save && echo '✅ Deploy complete!'"
|
||||
```
|
||||
|
||||
For the local VPS:
|
||||
|
||||
```bash
|
||||
ssh root@192.168.0.15 "npm install -g omniroute@latest && pm2 restart omniroute && pm2 save && echo '✅ Deploy complete!'"
|
||||
```
|
||||
|
||||
### 3. Verify the deployment
|
||||
|
||||
```bash
|
||||
ssh root@69.164.221.35 "pm2 list && cat \$(npm root -g)/omniroute/package.json | grep version | head -1 && curl -s -o /dev/null -w 'HTTP %{http_code}' http://localhost:20128/"
|
||||
```
|
||||
|
||||
Expected: PM2 shows `online`, version matches published, HTTP returns `307` (redirect to login).
|
||||
|
||||
## How it works
|
||||
|
||||
1. `npm publish` builds Next.js standalone + bundles everything into the npm package
|
||||
2. `npm install -g omniroute@latest` downloads and installs to `/usr/lib/node_modules/omniroute/`
|
||||
3. PM2 is registered to run `npm start` from that directory (cwd: `/usr/lib/node_modules/omniroute`)
|
||||
4. `pm2 restart omniroute` picks up the new code immediately
|
||||
|
||||
## PM2 Setup (one-time)
|
||||
|
||||
If PM2 needs to be reconfigured from scratch:
|
||||
|
||||
```bash
|
||||
ssh root@<VPS> "
|
||||
cd /usr/lib/node_modules/omniroute &&
|
||||
PORT=20128 pm2 start app/server.js --name omniroute --env PORT=20128 &&
|
||||
pm2 save &&
|
||||
pm2 startup
|
||||
"
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- The `.env` file is at `/usr/lib/node_modules/omniroute/.env`. Back it up before major npm updates.
|
||||
- PM2 is configured with `pm2 startup` to auto-restart on reboot.
|
||||
- Nginx proxies `omniroute.online` → `localhost:20128`.
|
||||
- The VPS has only 1GB RAM — builds happen locally via `npm publish`, not on the VPS.
|
||||
356
.agents/workflows/generate-release-ag.md
Normal file
356
.agents/workflows/generate-release-ag.md
Normal file
@@ -0,0 +1,356 @@
|
||||
---
|
||||
description: Create a new release, bump version up to the .10 patch threshold, update changelog, and manage Pull Requests
|
||||
---
|
||||
|
||||
# Generate Release Workflow
|
||||
|
||||
Bump version, finalize CHANGELOG, commit, open a **PR to main** and wait for user confirmation before tagging, publishing, and deploying.
|
||||
|
||||
> **VERSION RULE: Always use PATCH bumps (3.x.y → 3.x.y+1)**
|
||||
> NEVER use `npm version minor` or `npm version major`.
|
||||
> Always use: `npm version patch --no-git-tag-version`
|
||||
> The threshold rule: when `y` reaches 10, bump to `3.(x+1).0` — e.g. `3.8.10` → `3.9.0`.
|
||||
|
||||
> **🔴 SINGLE BRANCH RULE**: The `release/vX.Y.Z` branch is the **ONLY** development branch for the entire release cycle. ALL work — bug fixes, feature implementations, PR integrations, issue resolutions — MUST be committed directly on this branch. Never create separate `fix/`, `feat/`, or topic branches. When running `/resolve-issues`, `/implement-features`, or `/review-prs`, always work on the current release branch.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Two-Phase Flow
|
||||
|
||||
```
|
||||
Phase 1 (automated): bump → docs → i18n → commit → push → open PR
|
||||
↕ 🛑 STOP: Notify user, wait for PR confirmation
|
||||
Phase 2 (post-merge): tag → publish → GitHub release → Docker → deploy
|
||||
```
|
||||
|
||||
**NEVER push directly to main or create tags before the user confirms the PR.**
|
||||
|
||||
---
|
||||
|
||||
## Phase 0: Security Verification (MANDATORY)
|
||||
|
||||
Before creating the release, you must ensure the codebase and supply chain are secure and free of known vulnerabilities.
|
||||
|
||||
1. **Run Local Dependencies Audit:**
|
||||
|
||||
```bash
|
||||
npm audit
|
||||
```
|
||||
|
||||
_Fix any `high` or `critical` vulnerabilities identified._
|
||||
|
||||
2. **Check GitHub CodeQL & Dependabot Alerts:**
|
||||
Navigate to the repository's **Security** tab on GitHub, or use the project's `vulnerability-scanner` skill to analyze active alerts. Ensure all static analysis findings (e.g., prototype pollution, insecure randomness, ReDoS, shell injections) are addressed and logically committed on a target branch.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Pre-Merge
|
||||
|
||||
### 1. Create release branch
|
||||
|
||||
```bash
|
||||
git checkout -b release/v3.x.y
|
||||
```
|
||||
|
||||
### 2. Determine and sync version
|
||||
|
||||
Check current version in `package.json`:
|
||||
|
||||
```bash
|
||||
grep '"version"' package.json
|
||||
```
|
||||
|
||||
> **🔴 BRANCH-VERSION PARITY RULE**: The logical version in `package.json` MUST exactly match the release branch name. For example, if you are on `release/v3.7.0`, the version in `package.json` MUST be `3.7.0`.
|
||||
>
|
||||
> - If this is the FIRST time generating a release for a new minor/major branch (e.g., bumping from 3.6.9 to 3.7.0), you MUST ensure the version is bumped to match the new branch logic.
|
||||
> - If you are just bumping a patch on the current branch (e.g., 3.6.9 to 3.6.10), use:
|
||||
> `npm version patch --no-git-tag-version`
|
||||
|
||||
> **⚠️ ATOMIC COMMIT RULE — Version bump MUST happen before committing feature files.**
|
||||
>
|
||||
> **CORRECT order:**
|
||||
>
|
||||
> 1. `npm version patch --no-git-tag-version` ← bump first
|
||||
> 2. implement features / fix bugs
|
||||
> 3. `git add -A && git commit -m "chore(release): v3.x.y — all changes in ONE commit"`
|
||||
>
|
||||
> **OR if features are already staged:**
|
||||
>
|
||||
> 1. implement features (do NOT commit yet)
|
||||
> 2. `npm version patch --no-git-tag-version` ← bump before committing
|
||||
> 3. `git add -A && git commit -m "chore(release): v3.x.y — all changes in ONE commit"`
|
||||
>
|
||||
> **NEVER do this (creates version mismatch in git history):**
|
||||
>
|
||||
> - ~~commit features → then bump version → commit package.json separately~~
|
||||
>
|
||||
> This ensures that `git show v3.x.y` always contains both code changes and the version bump together.
|
||||
> The GitHub release tag will point to a commit that includes ALL changes for that version.
|
||||
|
||||
### 3. Regenerate lock file (REQUIRED after version bump)
|
||||
|
||||
**Mandatory** — skipping causes `@swc/helpers` lock mismatch and CI failures:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 4. Finalize CHANGELOG.md
|
||||
|
||||
> **🔴 NO MIXUPS RULE**: Ensure you do NOT mix the backlog of the previous version with the new one. The new version section must ONLY contain the features and fixes for the current release.
|
||||
|
||||
Replace the `[Unreleased]` header with the new version and date.
|
||||
Keep an empty `## [Unreleased]` section above it, separated by a horizontal rule (`---`).
|
||||
|
||||
```markdown
|
||||
## [Unreleased]
|
||||
|
||||
---
|
||||
|
||||
## [3.7.0] — 2026-04-19
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- ...
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- ...
|
||||
|
||||
### 🏆 Hall de Contribuidores
|
||||
|
||||
Um agradecimento especial a todos que contribuíram com código, revisões e testes para este release:
|
||||
@user1, @user2
|
||||
|
||||
---
|
||||
|
||||
## [3.6.9] — 2026-04-19
|
||||
```
|
||||
|
||||
> **🔴 HALL DE CONTRIBUIDORES RULE**: You MUST parse all the PR author mentions (e.g., `(thanks @username)`) from the new version's changelog items, deduplicate them, and append them as a "Hall de Contribuidores" section at the end of the new release block, exactly as shown above.
|
||||
|
||||
### 5. Update openapi.yaml version ⚠️ MANDATORY
|
||||
|
||||
> **CI will fail** if `docs/reference/openapi.yaml` version ≠ `package.json` version (`check:docs-sync` enforces this).
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
sed -i "s/ version: .*/ version: $VERSION/" docs/reference/openapi.yaml
|
||||
echo "✓ openapi.yaml → $VERSION"
|
||||
|
||||
for dir in electron open-sse; do
|
||||
if [ -d "$dir" ] && [ -f "$dir/package.json" ]; then
|
||||
(cd "$dir" && npm version "$VERSION" --no-git-tag-version --allow-same-version > /dev/null)
|
||||
echo "✓ $dir/package.json → $VERSION"
|
||||
fi
|
||||
done
|
||||
# Re-run install to assert the workspace lockfile is updated
|
||||
npm install
|
||||
```
|
||||
|
||||
### 6. Update README.md and i18n docs
|
||||
|
||||
Manually perform these documentation updates (there is no `/update-docs` workflow — it was deprecated in v3.8):
|
||||
|
||||
- Update feature table rows and "What's new in vX.Y.Z" section in `README.md`
|
||||
- Sync feature changes to all 40 language `docs/i18n/*/README.md` files (use the same row edits across each translated README)
|
||||
- Update the relevant `docs/<AREA>.md` if architecture or counts changed
|
||||
- Re-run `npm run check:docs-sync` and `npm run check:docs-all` to catch drift
|
||||
|
||||
### 7. Run tests
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
All tests must pass before creating the PR.
|
||||
|
||||
### 8. Stage, commit, and push
|
||||
|
||||
// turbo-all
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "chore(release): v3.x.y — summary of changes"
|
||||
git push origin release/v3.x.y
|
||||
```
|
||||
|
||||
### 9. Open PR to main
|
||||
|
||||
### 9. Open PR to main
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
|
||||
# Extract the exact changelog entry for this version from the root CHANGELOG.md
|
||||
awk "/^## \\[$VERSION\\]/{flag=1; print; next} /^---/{if(flag) {flag=0; exit}} flag" CHANGELOG.md > /tmp/changelog_body.txt
|
||||
|
||||
# Append test status and next steps
|
||||
echo "" >> /tmp/changelog_body.txt
|
||||
echo "### Tests" >> /tmp/changelog_body.txt
|
||||
echo "- All tests pass" >> /tmp/changelog_body.txt
|
||||
echo "" >> /tmp/changelog_body.txt
|
||||
echo "### ⚠️ After merging: run Phase 2 steps to tag, publish, and deploy." >> /tmp/changelog_body.txt
|
||||
|
||||
gh pr create \
|
||||
--repo diegosouzapw/OmniRoute \
|
||||
--base main \
|
||||
--head release/v$VERSION \
|
||||
--title "Release v$VERSION" \
|
||||
--body-file /tmp/changelog_body.txt
|
||||
```
|
||||
|
||||
### 10. 🛑 STOP — Notify User & Await PR Confirmation
|
||||
|
||||
**This is a mandatory stop point.** Present the report in the final response and stop. Do not continue to the next phase until the user explicitly approves.
|
||||
|
||||
Inform the user:
|
||||
|
||||
- PR URL
|
||||
- Summary of changes
|
||||
- Test results
|
||||
- List of files changed
|
||||
|
||||
**DO NOT proceed to Phase 2 until the user confirms the PR looks good and merges it.**
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Post-Merge Validation (Local VPS)
|
||||
|
||||
> Run these steps only AFTER the user has merged the PR into `main` and all CI jobs have passed.
|
||||
|
||||
### 11. Deploy to Local VPS for Final Validation (MANDATORY)
|
||||
|
||||
Before cutting the official git tag and publishing to the world, deploy the `main` branch to the Local VPS for a final homologation test.
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
git pull origin main
|
||||
|
||||
# Build and pack locally
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute && rm -f omniroute-*.tgz && rm -rf .next/cache app/.next/cache && npm run build:cli && rm -rf app/logs app/coverage app/.git app/.app-build-backup* && npm pack --ignore-scripts
|
||||
|
||||
# Deploy to LOCAL VPS (192.168.0.15)
|
||||
scp omniroute-*.tgz root@192.168.0.15:/tmp/
|
||||
ssh root@192.168.0.15 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && pm2 delete omniroute 2>/dev/null; pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Local done'"
|
||||
|
||||
# Verify
|
||||
curl -s -o /dev/null -w "LOCAL: HTTP %{http_code}\n" http://192.168.0.15:20128/
|
||||
```
|
||||
|
||||
### 12. 🛑 STOP — Notify User & Await Final OK
|
||||
|
||||
**This is a mandatory stop point.**
|
||||
Inform the user that the `main` branch is now running on the Local VPS.
|
||||
Wait for the user to manually test and give the **OK**.
|
||||
**DO NOT proceed to Phase 3 until the user confirms the local deploy is stable.**
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Official Launch
|
||||
|
||||
> Run these steps only AFTER the user gives the final OK from the Phase 2 local validation.
|
||||
|
||||
### 13. Create Git Tag and GitHub Release (MANDATORY)
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
git pull origin main
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
|
||||
# Extracts the changelog section for this version
|
||||
NOTES=$(awk "/^## \\[$VERSION\\]/{flag=1; next} /^---/{if(flag) {flag=0; exit}} flag" CHANGELOG.md | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
|
||||
if [ -z "$NOTES" ]; then NOTES="OmniRoute v$VERSION Release"; fi
|
||||
|
||||
git tag -a "v$VERSION" -m "Release v$VERSION"
|
||||
git push origin "v$VERSION"
|
||||
gh release create "v$VERSION" --repo diegosouzapw/OmniRoute --title "v$VERSION" --notes "$NOTES" --target main || gh release edit "v$VERSION" --repo diegosouzapw/OmniRoute --title "v$VERSION" --notes "$NOTES"
|
||||
```
|
||||
|
||||
### 14. 🐳 Trigger Docker Hub build (MANDATORY — keep npm and Docker in sync)
|
||||
|
||||
> **CRITICAL**: Docker Hub and npm MUST always publish the same version.
|
||||
> The Docker image is built automatically via GitHub Actions when a new tag is pushed.
|
||||
> After pushing the tag in step 13, **verify the workflow runs**:
|
||||
|
||||
```bash
|
||||
# Verify the Docker workflow triggered
|
||||
gh run list --repo diegosouzapw/OmniRoute --workflow docker-publish.yml --limit 3
|
||||
|
||||
# Wait for the Docker build to complete (usually 5–10 min)
|
||||
gh run watch --repo diegosouzapw/OmniRoute
|
||||
```
|
||||
|
||||
### 15. Publish to NPM (Optional/Automated)
|
||||
|
||||
Normally handled by CI, but if manual publish is required:
|
||||
|
||||
```bash
|
||||
npm publish
|
||||
```
|
||||
|
||||
## Phase 4: Release Monitoring & Artifact Validation
|
||||
|
||||
> After triggering the official release, actively monitor the CI pipelines until all artifacts are successfully generated. If any pipeline fails, stop and apply the necessary corrections before continuing.
|
||||
|
||||
### 16. Monitor CI Pipelines
|
||||
|
||||
Wait for and verify the successful completion of the following automated jobs:
|
||||
|
||||
1. **Docker Hub Publish**
|
||||
2. **Electron Build**
|
||||
3. **NPM Registry Publish** (Check with `npm info omniroute version`)
|
||||
|
||||
```bash
|
||||
# Monitor Docker Hub workflow
|
||||
gh run list --repo diegosouzapw/OmniRoute --workflow docker-publish.yml --limit 1
|
||||
gh run watch <RUN_ID>
|
||||
|
||||
# Monitor Electron build
|
||||
gh run list --repo diegosouzapw/OmniRoute --workflow electron-release.yml --limit 1
|
||||
gh run watch <RUN_ID>
|
||||
|
||||
# Verify NPM version
|
||||
npm info omniroute version
|
||||
```
|
||||
|
||||
### 17. Handle Failures (If Any)
|
||||
|
||||
If a workflow fails:
|
||||
|
||||
- Use `gh run view <RUN_ID> --log-failed` to identify the error.
|
||||
- Apply the fix on the `main` branch.
|
||||
- If necessary, re-trigger the workflow using `gh workflow run <workflow_name.yml> --repo diegosouzapw/OmniRoute --ref v3.x.y`
|
||||
|
||||
### 18. Preserve release branch
|
||||
|
||||
```bash
|
||||
# Branch is kept for historical purposes. Do not delete.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Ensure CHANGELOG, README and `docs/*` are current BEFORE this workflow — run `npm run check:docs-all` and `/version-bump` first (there is no `/update-docs` workflow anymore)
|
||||
- The `prepublishOnly` script runs `npm run build:cli` automatically during `npm publish`
|
||||
- After npm publish, verify with `npm info omniroute version`
|
||||
- Lock file sync errors are caused by skipping `npm install` after version bump
|
||||
- Use `gh auth switch -u diegosouzapw` if git push fails with wrong account
|
||||
|
||||
## Known CI Pitfalls
|
||||
|
||||
| CI failure | Cause | Fix |
|
||||
| ------------------------------------------------------------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------- |
|
||||
| `[docs-sync] FAIL - OpenAPI version differs from package.json` | Skipped step 5 — `docs/reference/openapi.yaml` version not updated | Run step 5 (`sed -i ...`) and commit |
|
||||
| `[docs-sync] FAIL - CHANGELOG.md first section must be "## [Unreleased]"` | `## [Unreleased]` missing or not at top of CHANGELOG | Add `## [Unreleased]\n\n---\n` before the first versioned `## [x.y.z]` |
|
||||
| Electron Linux `.deb` build fails (`FpmTarget` error) | `fpm` Ruby gem not installed on `ubuntu-latest` runner | Already fixed in `electron-release.yml` (`gem install fpm` step) |
|
||||
| Docker Hub `502 error writing layer blob` | Transient Docker Hub network error during ARM64 push | Re-run the Docker publish workflow; no code change needed |
|
||||
@@ -1,110 +0,0 @@
|
||||
---
|
||||
description: Create a new release, bump version up to 1.x.10 threshold, update changelog, and manage Pull Requests
|
||||
---
|
||||
|
||||
# Generate Release Workflow
|
||||
|
||||
Bump version, finalize CHANGELOG, commit, tag, push, publish to npm, and create GitHub release.
|
||||
|
||||
> **VERSION RULE: Always use PATCH bumps (2.x.y → 2.x.y+1)**
|
||||
> NEVER use `npm version minor` or `npm version major`.
|
||||
> Always use: `npm version patch --no-git-tag-version`
|
||||
> The threshold rule: when `y` reaches 10, bump to `2.(x+1).0` — e.g. `2.1.10` → `2.2.0`.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Determine new version
|
||||
|
||||
Check current version in `package.json` and increment the **patch** number only:
|
||||
|
||||
```bash
|
||||
grep '"version"' package.json
|
||||
```
|
||||
|
||||
Version format: `2.x.y` — examples:
|
||||
|
||||
- `2.1.2` → `2.1.3` (patch)
|
||||
- `2.1.9` → `2.1.10` (patch)
|
||||
- `2.1.10` → `2.2.0` (minor threshold — do manually with `sed`)
|
||||
|
||||
```bash
|
||||
# ALWAYS use patch:
|
||||
npm version patch --no-git-tag-version
|
||||
```
|
||||
|
||||
### 2. Regenerate lock file (REQUIRED after version bump)
|
||||
|
||||
**Mandatory** — skipping causes `@swc/helpers` lock mismatch and CI failures:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 3. Finalize CHANGELOG.md
|
||||
|
||||
Replace `[Unreleased]` header with the new version and date.
|
||||
Keep an empty `## [Unreleased]` section above it.
|
||||
|
||||
```markdown
|
||||
## [Unreleased]
|
||||
|
||||
---
|
||||
|
||||
## [2.x.y] — YYYY-MM-DD
|
||||
```
|
||||
|
||||
### 4. Update openapi.yaml version ⚠️ MANDATORY
|
||||
|
||||
> **CI will fail** if `docs/openapi.yaml` version ≠ `package.json` version (`check:docs-sync` enforces this).
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
VERSION=$(node -p "require('./package.json').version") && sed -i "s/ version: .*/ version: $VERSION/" docs/openapi.yaml && echo "✓ openapi.yaml → $VERSION"
|
||||
```
|
||||
|
||||
### 5. Stage, commit, and tag
|
||||
|
||||
// turbo-all
|
||||
|
||||
```bash
|
||||
git add package.json package-lock.json CHANGELOG.md docs/openapi.yaml
|
||||
git commit -m "chore(release): v2.x.y — summary of changes"
|
||||
git tag -a v2.x.y -m "Release v2.x.y"
|
||||
```
|
||||
|
||||
### 6. Push to GitHub
|
||||
|
||||
```bash
|
||||
git push origin main --tags
|
||||
```
|
||||
|
||||
### 7. Create GitHub release
|
||||
|
||||
```bash
|
||||
gh release create v2.x.y --title "v2.x.y — summary" --notes "..."
|
||||
```
|
||||
|
||||
### 8. Deploy to VPS (if requested)
|
||||
|
||||
See `/deploy-vps` workflow for Akamai VPS or use npm for local VPS:
|
||||
|
||||
```bash
|
||||
ssh root@<VPS_IP> "npm install -g omniroute@2.x.y && pm2 restart omniroute"
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Always run `/update-docs` BEFORE this workflow (ensures CHANGELOG and README are current)
|
||||
- The `prepublishOnly` script runs `npm run build:cli` automatically during `npm publish`
|
||||
- After npm publish, verify with `npm info omniroute version`
|
||||
- Lock file sync errors are caused by skipping `npm install` after version bump
|
||||
|
||||
## Known CI Pitfalls
|
||||
|
||||
| CI failure | Cause | Fix |
|
||||
| ------------------------------------------------------------------------- | -------------------------------------------------------- | ---------------------------------------------------------------------- |
|
||||
| `[docs-sync] FAIL - OpenAPI version differs from package.json` | Skipped step 4 — `docs/openapi.yaml` version not updated | Run step 4 (`sed -i ...`) and commit |
|
||||
| `[docs-sync] FAIL - CHANGELOG.md first section must be "## [Unreleased]"` | `## [Unreleased]` missing or not at top of CHANGELOG | Add `## [Unreleased]\n\n---\n` before the first versioned `## [x.y.z]` |
|
||||
| Electron Linux `.deb` build fails (`FpmTarget` error) | `fpm` Ruby gem not installed on `ubuntu-latest` runner | Already fixed in `electron-release.yml` (`gem install fpm` step) |
|
||||
| Docker Hub `502 error writing layer blob` | Transient Docker Hub network error during ARM64 push | Re-run the Docker publish workflow; no code change needed |
|
||||
706
.agents/workflows/implement-features-ag.md
Normal file
706
.agents/workflows/implement-features-ag.md
Normal file
@@ -0,0 +1,706 @@
|
||||
---
|
||||
description: Analyze open feature request issues, implement viable ones on dedicated branches, and respond to authors
|
||||
---
|
||||
|
||||
# /implement-features — Feature Request Harvest, Research & Implementation Workflow
|
||||
|
||||
## Overview
|
||||
|
||||
A **5-phase** workflow that systematically harvests feature requests from GitHub issues, creates structured idea files, researches solutions across the internet and Git repositories, presents a consolidated report for user approval, then generates detailed implementation plans and executes them.
|
||||
|
||||
**Output directory structure:**
|
||||
|
||||
```
|
||||
_ideia/
|
||||
├── viable/ # Features approved for implementation
|
||||
│ ├── need_details/ # ❓ Good idea but waiting for author clarification (issues stay OPEN)
|
||||
│ │ └── 1015-warp-terminal-mitm.md
|
||||
│ ├── 1046-native-playground.md # ✅ Ready — researched and planned
|
||||
│ └── 1046-native-playground.requirements.md
|
||||
├── defer/ # ⏭️ Good ideas deferred for future cycles (issues CLOSED)
|
||||
│ └── 1041-smart-auto-combos.md
|
||||
└── notfit/ # ❌ Out of scope / already exists (issues CLOSED)
|
||||
└── 945-telegram-integration.md
|
||||
|
||||
_tasks/features-vX.Y.Z/ # Implementation plans (per-release)
|
||||
└── 1046-native-playground.plan.md
|
||||
```
|
||||
|
||||
> **LIFECYCLE RULE:** `viable/` files are **DELETED** once the feature is implemented — they are not moved. Only unimplemented features live in `viable/` (or `viable/need_details/`). Files in `defer/` and `notfit/` remain as permanent reference.
|
||||
|
||||
> **BRANCH RULE**: All implementation work MUST happen on the current `release/vX.Y.Z` branch. Never create separate `feat/` branches. If no release branch exists yet, create one first using `/generate-release` Phase 1 steps 1–5.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Harvest: Collect & Catalog Feature Ideas
|
||||
|
||||
### 1.1 Identify the Repository
|
||||
|
||||
// turbo
|
||||
|
||||
- Run: `git -C <project_root> remote get-url origin` to extract owner/repo.
|
||||
|
||||
### 1.2 Ensure Release Branch Exists
|
||||
|
||||
// turbo
|
||||
|
||||
Before doing any work, ensure you are on the current release branch:
|
||||
|
||||
```bash
|
||||
# Check current branch
|
||||
git branch --show-current
|
||||
|
||||
# If on main, determine next version and create the release branch
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
NEXT=$(node -p "const [a,b,c]=('$VERSION').split('.').map(Number); c>=9?a+'.'+(b+1)+'.0':a+'.'+b+'.'+(c+1)")
|
||||
git checkout -b release/v$NEXT
|
||||
npm version patch --no-git-tag-version
|
||||
npm install
|
||||
```
|
||||
|
||||
If already on a `release/vX.Y.Z` branch, continue working there.
|
||||
|
||||
### 1.3 Fetch ALL Open Feature Requests
|
||||
|
||||
// turbo-all
|
||||
|
||||
**⚠️ CRITICAL**: The JSON output of `gh issue list` can be truncated by the tool, silently hiding issues. You MUST use the two-step approach below.
|
||||
|
||||
**Step 1 — Get Issue numbers only** (small output, never truncated):
|
||||
|
||||
```bash
|
||||
# Fetch issues with feature/enhancement labels
|
||||
gh issue list --repo <owner>/<repo> --state open -l "enhancement" --limit 500 --json number --jq '.[].number'
|
||||
|
||||
# Also check for [Feature] in title (common pattern when no labels are set)
|
||||
gh issue list --repo <owner>/<repo> --state open --limit 500 --json number,title --jq '.[] | select(.title | test("\\[Feature\\]|\\[feature\\]|feature request"; "i")) | .number'
|
||||
```
|
||||
|
||||
- Merge both lists, deduplicate. Count and confirm the total.
|
||||
|
||||
**Step 2 — Fetch full metadata for each Issue** (one call per issue):
|
||||
|
||||
```bash
|
||||
gh issue view <NUMBER> --repo <owner>/<repo> --json number,title,labels,body,comments,createdAt,author,assignees
|
||||
```
|
||||
|
||||
- Read the **entire body** — including description, use cases, screenshots, mockups, and any embedded images.
|
||||
- Read **ALL comments** — community discussion, agreements, restrictions, owner responses, and linked PRs.
|
||||
- **Images**: If the body or comments contain image URLs (`` or `https://...png/jpg/gif`), note them — they may contain UI mockups, wireframes, or architecture diagrams that are essential to understanding the request.
|
||||
- You may batch these into parallel calls (up to 4 at a time).
|
||||
- Sort by oldest first (FIFO).
|
||||
|
||||
### 1.4 Create Idea Files (initially in `_ideia/` root)
|
||||
|
||||
For each feature request, create a structured idea file in `<project_root>/_ideia/`:
|
||||
|
||||
**Filename convention**: `<NUMBER>-<kebab-case-short-title>.md`
|
||||
Example: `1046-native-playground.md`, `1041-smart-auto-combos.md`
|
||||
|
||||
#### 1.4a — If the idea file does NOT exist yet, create it:
|
||||
|
||||
```markdown
|
||||
# Feature: <Title from Issue>
|
||||
|
||||
> GitHub Issue: #<NUMBER> — opened by @<author> on <date>
|
||||
> Status: 📋 Cataloged | Priority: TBD
|
||||
|
||||
## 📝 Original Request
|
||||
|
||||
<Paste the FULL issue body here, preserving all formatting, images, and code blocks>
|
||||
|
||||
## 💬 Community Discussion
|
||||
|
||||
<Summarize ALL comments chronologically, noting who said what and any decisions or objections raised>
|
||||
|
||||
### Participants
|
||||
|
||||
- @<author> — Original requester
|
||||
- @<commenter1> — <brief role/opinion>
|
||||
- ...
|
||||
|
||||
### Key Points
|
||||
|
||||
- <bullet list of the most important discussion points>
|
||||
- <agreements reached>
|
||||
- <objections raised>
|
||||
|
||||
## 🎯 Refined Feature Description
|
||||
|
||||
<YOUR interpretation and enrichment of the feature request. Expand on what was asked, fill in logical gaps, provide concrete examples of how it would work. This section should be MORE detailed and clearer than the original request.>
|
||||
|
||||
### What it solves
|
||||
|
||||
- <problem 1>
|
||||
- <problem 2>
|
||||
|
||||
### How it should work (high level)
|
||||
|
||||
1. <step 1>
|
||||
2. <step 2>
|
||||
3. ...
|
||||
|
||||
### Affected areas
|
||||
|
||||
- <list of codebase areas, modules, files likely affected>
|
||||
|
||||
## 📎 Attachments & References
|
||||
|
||||
- <any image URLs, mockup links, or external references from the issue>
|
||||
|
||||
## 🔗 Related Ideas
|
||||
|
||||
- <links to related \_ideia/ files if any overlap found>
|
||||
```
|
||||
|
||||
#### 1.4b — If the idea file ALREADY exists, update it:
|
||||
|
||||
- Append new comments from the issue to the **Community Discussion** section.
|
||||
- Update the **Refined Feature Description** if new information changes the understanding.
|
||||
- Add any new **Related Ideas** cross-references found.
|
||||
- **Do NOT overwrite** existing content — append and enrich it.
|
||||
|
||||
### 1.5 Cross-Reference & Deduplication
|
||||
|
||||
After processing all issues:
|
||||
|
||||
- Scan all `_ideia/*.md` files for overlapping features.
|
||||
- If two features are substantially the same, add `🔗 Related Ideas` cross-references to both.
|
||||
- If one is a strict subset of another, note it in the smaller file: `> ℹ️ This feature is a subset of #<OTHER_NUMBER>. Consider implementing together.`
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Research: Find Solutions & Build Requirements
|
||||
|
||||
For each cataloged idea that is **viable** (aligns with the project's goals):
|
||||
|
||||
### 2.1 Viability Pre-Check
|
||||
|
||||
Before investing in research, quickly assess:
|
||||
|
||||
- [ ] Does this feature align with the project's goals and architecture?
|
||||
- [ ] Is it technically feasible with the current codebase?
|
||||
- [ ] Does it duplicate existing functionality?
|
||||
- [ ] Would it introduce breaking changes or security risks?
|
||||
- [ ] Is there enough detail to understand what's needed?
|
||||
|
||||
**Verdict options:**
|
||||
|
||||
| Verdict | When | Action |
|
||||
| --------------------- | ------------------------------------- | --------------------------- |
|
||||
| ✅ **VIABLE** | Good idea, enough context | Proceed to Research |
|
||||
| ❓ **NEEDS DETAIL** | Good idea, insufficient spec | Skip research, ask author |
|
||||
| ⏭️ **DEFER** | Good idea, too complex for this cycle | Catalog only, skip research |
|
||||
| ❌ **NOT FIT** | Doesn't fit the project | Explain why |
|
||||
| 🔁 **ALREADY EXISTS** | Feature already implemented | Point to existing feature |
|
||||
|
||||
### 2.2 Internet Research (for VIABLE features)
|
||||
|
||||
For each viable feature, perform systematic research:
|
||||
|
||||
**Step 1 — Web search for similar implementations:**
|
||||
|
||||
```
|
||||
WebSearch("how to implement <feature description> in <tech stack>")
|
||||
WebSearch("<feature keyword> implementation nextjs typescript 2025 2026")
|
||||
WebSearch("<feature keyword> open source library npm")
|
||||
```
|
||||
|
||||
**Step 2 — Find reference Git repositories:**
|
||||
|
||||
```
|
||||
WebSearch("site:github.com <feature keyword> <tech stack> stars:>100")
|
||||
WebSearch("github <feature keyword> implementation recently updated 2026")
|
||||
```
|
||||
|
||||
- Find **up to 10 relevant repositories**, sorted by most recently updated.
|
||||
- For each repository:
|
||||
- Note the repo URL, star count, last commit date
|
||||
- Read its README and relevant source files via `WebFetch`
|
||||
- Extract the architectural approach, patterns used, and key code snippets
|
||||
|
||||
**Step 3 — Read API docs and standards:**
|
||||
|
||||
If the feature involves an external API, protocol, or standard:
|
||||
|
||||
- Find and read the official documentation
|
||||
- Note version requirements, authentication patterns, rate limits
|
||||
|
||||
### 2.3 Create Requirements File
|
||||
|
||||
For each researched feature, create a requirements file alongside its idea file:
|
||||
|
||||
**Filename**: `<NUMBER>-<kebab-case-short-title>.requirements.md`
|
||||
|
||||
```markdown
|
||||
# Requirements: <Feature Title>
|
||||
|
||||
> Feature Idea: [#<NUMBER>](./<NUMBER>-<kebab-case-short-title>.md)
|
||||
> Research Date: <YYYY-MM-DD>
|
||||
> Verdict: ✅ VIABLE
|
||||
|
||||
## 🔍 Research Summary
|
||||
|
||||
<Brief summary of what was found during research>
|
||||
|
||||
## 📚 Reference Implementations
|
||||
|
||||
| # | Repository | Stars | Last Updated | Approach | Relevance |
|
||||
| --- | ---------------- | ----- | ------------ | -------- | ------------ |
|
||||
| 1 | [repo/name](url) | ⭐ N | YYYY-MM-DD | <brief> | High/Med/Low |
|
||||
| 2 | ... | | | | |
|
||||
|
||||
### Key Patterns Found
|
||||
|
||||
- <pattern 1 with code snippet or link>
|
||||
- <pattern 2>
|
||||
|
||||
## 📐 Proposed Solution Architecture
|
||||
|
||||
### Approach
|
||||
|
||||
<Describe the chosen approach based on research findings>
|
||||
|
||||
### New Files
|
||||
|
||||
| File | Purpose |
|
||||
| --------------------- | ------------- |
|
||||
| `path/to/new/file.ts` | <description> |
|
||||
|
||||
### Modified Files
|
||||
|
||||
| File | Changes |
|
||||
| -------------------------- | -------------- |
|
||||
| `path/to/existing/file.ts` | <what changes> |
|
||||
|
||||
### Database Changes
|
||||
|
||||
- <migrations needed, if any>
|
||||
|
||||
### API Changes
|
||||
|
||||
- <new/modified endpoints, if any>
|
||||
|
||||
### UI Changes
|
||||
|
||||
- <new/modified pages/components, if any>
|
||||
|
||||
## ⚙️ Implementation Effort
|
||||
|
||||
- **Estimated complexity**: Low / Medium / High / Very High
|
||||
- **Estimated files changed**: ~N
|
||||
- **Dependencies needed**: <new npm packages, if any>
|
||||
- **Breaking changes**: Yes/No — <details>
|
||||
- **i18n impact**: <number of new translation keys>
|
||||
- **Test coverage needed**: <brief description>
|
||||
|
||||
## ⚠️ Open Questions
|
||||
|
||||
- <question 1>
|
||||
- <question 2>
|
||||
|
||||
## 🔗 External References
|
||||
|
||||
- <documentation URLs>
|
||||
- <API references>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2.5 — Organize & Respond: Sort Files and Post GitHub Comments
|
||||
|
||||
### 2.5.1 Create Directory Structure
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
mkdir -p <project_root>/_ideia/viable
|
||||
mkdir -p <project_root>/_ideia/viable/need_details
|
||||
mkdir -p <project_root>/_ideia/defer
|
||||
mkdir -p <project_root>/_ideia/notfit
|
||||
```
|
||||
|
||||
### 2.5.2 Move Idea Files to Category Subdirectories
|
||||
|
||||
After classification, move EVERY idea file to its correct subdirectory:
|
||||
|
||||
```bash
|
||||
# ✅ VIABLE — move idea + requirements files
|
||||
mv _ideia/<NUMBER>-*.md _ideia/viable/
|
||||
mv _ideia/<NUMBER>-*.requirements.md _ideia/viable/
|
||||
|
||||
# ❓ NEEDS DETAIL — viable but waiting for author response
|
||||
mv _ideia/<NUMBER>-*.md _ideia/viable/need_details/
|
||||
|
||||
# ⏭️ DEFER — move idea files only
|
||||
mv _ideia/<NUMBER>-*.md _ideia/defer/
|
||||
|
||||
# ❌ NOT FIT & 🔁 ALREADY EXISTS — move idea files only
|
||||
mv _ideia/<NUMBER>-*.md _ideia/notfit/
|
||||
```
|
||||
|
||||
No files should remain in `_ideia/` root after this step (except subdirectories).
|
||||
|
||||
### 2.5.3 Post GitHub Comments by Category
|
||||
|
||||
**Each category has a specific comment template and action:**
|
||||
|
||||
---
|
||||
|
||||
#### For 🔁 ALREADY EXISTS — Comment + CLOSE issue
|
||||
|
||||
// turbo
|
||||
|
||||
The feature already exists in the system. Explain WHERE it is and HOW to use it.
|
||||
|
||||
```markdown
|
||||
Hi @<author>! Thanks for the suggestion! 🙏
|
||||
|
||||
Great news — this functionality **already exists** in OmniRoute:
|
||||
|
||||
**📍 Where to find it:** <exact dashboard path or settings location>
|
||||
|
||||
**🔧 How to use it:**
|
||||
|
||||
1. <step 1>
|
||||
2. <step 2>
|
||||
3. <step 3>
|
||||
|
||||
If you have any trouble finding or using it, feel free to ask in a Discussion. We're always happy to help!
|
||||
|
||||
Closing this as the feature is already available. 🎉
|
||||
```
|
||||
|
||||
```bash
|
||||
gh issue close <NUMBER> --repo <owner>/<repo> --comment "<comment above>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### For ⏭️ DEFER — Comment + CLOSE issue
|
||||
|
||||
// turbo
|
||||
|
||||
Thank the user, explain the idea was cataloged, and that we'll study it before implementing.
|
||||
|
||||
```markdown
|
||||
Hi @<author>! Thanks for this thoughtful feature request! 🙏
|
||||
|
||||
We really appreciate the detailed proposal. We've **cataloged your idea** and it's now part of our improvement backlog.
|
||||
|
||||
Due to the **significant architectural impact** of this feature, we'll need to conduct thorough use-case studies and architectural analysis before we start development. This ensures we build it right and don't introduce regressions.
|
||||
|
||||
**What happens next:**
|
||||
|
||||
- Your idea is saved in our internal feature backlog
|
||||
- We'll conduct architecture studies when this area is prioritized
|
||||
- We'll notify you here when development begins
|
||||
|
||||
Thank you for contributing to OmniRoute's roadmap! Your input helps shape the product. 🚀
|
||||
```
|
||||
|
||||
```bash
|
||||
gh issue close <NUMBER> --repo <owner>/<repo> --comment "<comment above>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### For ❌ NOT FIT — Comment + CLOSE issue
|
||||
|
||||
// turbo
|
||||
|
||||
Politely explain why the feature doesn't fit the project scope.
|
||||
|
||||
```markdown
|
||||
Hi @<author>! Thanks for the suggestion! 🙏
|
||||
|
||||
After careful analysis, we've determined that this feature **falls outside OmniRoute's core scope** as a proxy/router.
|
||||
|
||||
**Reason:** <explain why — e.g., "Telegram integration belongs in the application/orchestrator layer that consumes OmniRoute's API, not inside the router itself.">
|
||||
|
||||
**Alternative:** <suggest an alternative approach if possible>
|
||||
|
||||
We appreciate you thinking of ways to improve OmniRoute! If you'd like to discuss this further, feel free to open a Discussion. 🙏
|
||||
```
|
||||
|
||||
```bash
|
||||
gh issue close <NUMBER> --repo <owner>/<repo> --comment "<comment above>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### For ❓ NEEDS DETAIL — Comment (keep OPEN)
|
||||
|
||||
// turbo
|
||||
|
||||
Ask for the specific missing details needed.
|
||||
|
||||
```markdown
|
||||
Hi @<author>! Thanks for the feature request — it's an interesting idea and we'd love to explore it further. 🙏
|
||||
|
||||
To move forward, we need a few more details:
|
||||
|
||||
1. <specific question 1>
|
||||
2. <specific question 2>
|
||||
3. <specific question 3>
|
||||
|
||||
If you know of any **open-source projects or repositories** that implement something similar, please share links — it would help us design the best solution.
|
||||
|
||||
Looking forward to your response! 🚀
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### For ✅ VIABLE — Comment (keep OPEN)
|
||||
|
||||
// turbo
|
||||
|
||||
Thank the user, confirm we've cataloged their idea, and explain it may be implemented in future versions.
|
||||
|
||||
```markdown
|
||||
Hi @<author>! Thanks for the great feature suggestion! 🙏
|
||||
|
||||
We've analyzed your request and it aligns well with OmniRoute's roadmap. We've **cataloged this feature** and it's in our implementation backlog.
|
||||
|
||||
**Status:** 📋 Cataloged for future implementation
|
||||
|
||||
This feature may be included in upcoming releases. We'll **respond to this issue and tag you** as soon as implementation begins so you can test it.
|
||||
|
||||
Thank you for helping improve OmniRoute! 🚀
|
||||
```
|
||||
|
||||
**⚠️ Do NOT close viable issues — they remain OPEN for tracking.**
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Report: Present Findings to User
|
||||
|
||||
### 3.1 🛑 MANDATORY STOP — Present Consolidated Report
|
||||
|
||||
After completing Phase 1, Phase 2, and Phase 2.5, **STOP and present the following report** in the chat. Do NOT proceed to implementation.
|
||||
|
||||
Present a structured report containing:
|
||||
|
||||
#### 3.1a — Feature Summary Table
|
||||
|
||||
| # | Issue | Title | Verdict | Location | Action |
|
||||
| --- | ----- | ----- | --------------- | ----------------------------- | ----------------------------- |
|
||||
| 1 | #N | Title | ✅ VIABLE | `_ideia/viable/` | Issue OPEN, comment posted |
|
||||
| 2 | #N | Title | ⏭️ DEFER | `_ideia/defer/` | Issue CLOSED with explanation |
|
||||
| 3 | #N | Title | ❌ NOT FIT | `_ideia/notfit/` | Issue CLOSED with explanation |
|
||||
| 4 | #N | Title | 🔁 EXISTS | `_ideia/notfit/` | Issue CLOSED with guidance |
|
||||
| 5 | #N | Title | ❓ NEEDS DETAIL | `_ideia/viable/need_details/` | Issue OPEN, questions posted |
|
||||
|
||||
#### 3.1b — Viable Features Detail
|
||||
|
||||
For each VIABLE feature, provide a brief paragraph:
|
||||
|
||||
- What was found during research
|
||||
- The proposed approach
|
||||
- Key risks or unknowns
|
||||
- Which reference repositories were most useful
|
||||
|
||||
#### 3.1c — Issues Requiring Author Feedback
|
||||
|
||||
For features marked ❓ NEEDS DETAIL, list:
|
||||
|
||||
- What specific information is missing
|
||||
- What examples or repository references would help
|
||||
|
||||
#### 3.1d — Ask for User Confirmation
|
||||
|
||||
End the report with:
|
||||
|
||||
> **Ready to proceed with implementation?**
|
||||
>
|
||||
> - Reply **"sim"** or **"yes"** to generate full implementation plans for all VIABLE features.
|
||||
> - Reply with specific issue numbers to select only certain features.
|
||||
> - Reply **"não"** or **"no"** to stop here.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — Plan: Generate Implementation Plans (after user says "yes")
|
||||
|
||||
> **⚠️ Do NOT enter this phase without explicit user approval from Phase 3.**
|
||||
|
||||
### 4.1 Create Task Directory
|
||||
|
||||
```bash
|
||||
mkdir -p <project_root>/_tasks/features-vX.Y.Z/
|
||||
```
|
||||
|
||||
### 4.2 Generate One Implementation Plan Per Feature
|
||||
|
||||
For each VIABLE feature approved by the user, create:
|
||||
|
||||
**Filename**: `_tasks/features-vX.Y.Z/<NUMBER>-<kebab-case-title>.plan.md`
|
||||
|
||||
```markdown
|
||||
# Implementation Plan: <Feature Title>
|
||||
|
||||
> Issue: #<NUMBER>
|
||||
> Idea: [\_ideia/viable/<NUMBER>-title.md](../../_ideia/viable/<NUMBER>-title.md)
|
||||
> Requirements: [\_ideia/viable/<NUMBER>-title.requirements.md](../../_ideia/viable/<NUMBER>-title.requirements.md)
|
||||
> Branch: `release/vX.Y.Z`
|
||||
|
||||
## Overview
|
||||
|
||||
<Brief description of what will be built>
|
||||
|
||||
## Pre-Implementation Checklist
|
||||
|
||||
- [ ] Read all related source files listed below
|
||||
- [ ] Confirm no conflicts with in-flight PRs
|
||||
- [ ] Verify database migration numbering
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: <Title>
|
||||
|
||||
**Files:**
|
||||
|
||||
- `path/to/file.ts` — <what to change>
|
||||
|
||||
**Details:**
|
||||
<Detailed description of the change, including code patterns to follow, function signatures, etc.>
|
||||
|
||||
### Step 2: <Title>
|
||||
|
||||
...
|
||||
|
||||
### Step N: Tests
|
||||
|
||||
**New test files:**
|
||||
|
||||
- `tests/unit/<test-file>.test.mjs` — <what to test>
|
||||
|
||||
**Test cases:**
|
||||
|
||||
- [ ] <test case 1>
|
||||
- [ ] <test case 2>
|
||||
|
||||
### Step N+1: i18n
|
||||
|
||||
**Translation keys to add:**
|
||||
|
||||
- `<namespace>.<key>` — "<English value>"
|
||||
|
||||
### Step N+2: Documentation
|
||||
|
||||
- [ ] Update CHANGELOG.md
|
||||
- [ ] Update relevant docs/ files
|
||||
|
||||
## Verification Plan
|
||||
|
||||
1. Run `npm run build` — must pass
|
||||
2. Run `npm test` — all tests must pass
|
||||
3. Run `npm run lint` — no new errors
|
||||
4. <Manual verification steps>
|
||||
|
||||
## Commit Plan
|
||||
```
|
||||
|
||||
feat: <description> (#<NUMBER>)
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
### 4.3 Present Plans for Final Approval
|
||||
|
||||
Present a summary of all generated plans:
|
||||
|
||||
> **Implementation plans generated:**
|
||||
>
|
||||
> | # | Feature | Plan File | Steps | Effort |
|
||||
> | --- | ------- | ---------------------------------------- | ------- | ------ |
|
||||
> | 1 | <title> | `_tasks/features-vX.Y.Z/N-title.plan.md` | N steps | Medium |
|
||||
>
|
||||
> Reply **"sim"** or **"yes"** to begin implementation of all features.
|
||||
> Reply with specific issue numbers to implement only certain ones.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — Execute: Implement the Plans (after user says "yes")
|
||||
|
||||
> **⚠️ Do NOT enter this phase without explicit user approval from Phase 4.**
|
||||
|
||||
### 5.1 Implement Each Feature
|
||||
|
||||
For each approved plan, execute it step by step:
|
||||
|
||||
1. **Follow the plan** — implement exactly as specified in the `.plan.md` file
|
||||
2. **Build** — Run `npm run build` after each feature to verify compilation
|
||||
3. **Test** — Run `npm test` to ensure no regressions
|
||||
4. **Commit** — Commit with: `feat: <description> (#<NUMBER>)`
|
||||
5. **Update the plan** — Mark completed steps with `[x]` in the plan file
|
||||
6. **Continue** — Move to the next feature (do NOT switch branches)
|
||||
|
||||
### 5.2 Respond to Authors (Update Viable Issues)
|
||||
|
||||
For each implemented feature, **close the issue with a final comment**:
|
||||
|
||||
````markdown
|
||||
✅ **Implemented in `release/vX.Y.Z`!**
|
||||
|
||||
Hi @<author>! Great news — your feature request has been implemented! 🎉
|
||||
|
||||
**What was done:**
|
||||
|
||||
- <bullet list of what was built>
|
||||
|
||||
**How to try it:**
|
||||
|
||||
```bash
|
||||
git fetch origin && git checkout release/vX.Y.Z
|
||||
npm install && npm run dev
|
||||
```
|
||||
````
|
||||
|
||||
This will be included in the upcoming **vX.Y.Z** release. Feel free to reopen if you spot any issues! 🚀
|
||||
|
||||
````
|
||||
|
||||
```bash
|
||||
gh issue close <NUMBER> --repo <owner>/<repo> --comment "<comment above>"
|
||||
````
|
||||
|
||||
Then **DELETE the idea file** — it has served its purpose:
|
||||
|
||||
```bash
|
||||
# ✅ Implemented files are DELETED (not moved)
|
||||
rm _ideia/viable/<NUMBER>-<title>.md
|
||||
rm _ideia/viable/<NUMBER>-<title>.requirements.md # if exists
|
||||
```
|
||||
|
||||
> **Why delete?** `viable/` only holds features that still NEED to be done. Once implemented, the commit history and CHANGELOG are the source of truth. Keeping the file would be confusing.
|
||||
|
||||
### 5.3 Finalize & Push
|
||||
|
||||
After implementing all approved features:
|
||||
|
||||
1. **Update CHANGELOG.md** on the release branch with all new feature entries
|
||||
2. Push the release branch: `git push origin release/vX.Y.Z`
|
||||
3. Run `/generate-release` workflow Phase 1 steps 7–10 (tests → commit → push → open PR to main → wait for user)
|
||||
|
||||
### 5.4 Final Summary Report
|
||||
|
||||
Present a final summary report to the user:
|
||||
|
||||
| Issue | Title | Verdict | Action | Commit |
|
||||
| ----- | ----- | --------------- | -------------------------------------------------- | --------- |
|
||||
| #N | Title | ✅ Implemented | Issue closed, idea file deleted | `abc1234` |
|
||||
| #N | Title | ⏭️ Deferred | Issue closed + saved in `_ideia/defer/` | — |
|
||||
| #N | Title | ❌ Not Fit | Issue closed + saved in `_ideia/notfit/` | — |
|
||||
| #N | Title | 🔁 Exists | Issue closed + saved in `_ideia/notfit/` | — |
|
||||
| #N | Title | ❓ Needs Detail | Issue OPEN, moved to `_ideia/viable/need_details/` | — |
|
||||
|
||||
Include:
|
||||
|
||||
- Total features harvested
|
||||
- Total ideas cataloged (`viable/need_details/` + `defer/` + `notfit/`)
|
||||
- Total features implemented (idea files deleted, issues closed)
|
||||
- Total features deferred
|
||||
- Total issues closed
|
||||
- Total issues left open (needs detail only — viable are closed after implementation)
|
||||
- Test results (pass/fail count)
|
||||
@@ -1,131 +0,0 @@
|
||||
---
|
||||
description: Analyze open feature request issues, implement viable ones on dedicated branches, and respond to authors
|
||||
---
|
||||
|
||||
# /implement-features — Feature Request Implementation Workflow
|
||||
|
||||
## Overview
|
||||
|
||||
Fetches open feature request issues, analyzes each against the current codebase, implements viable ones on dedicated branches, and responds to authors with results. Does NOT merge to main — leaves branches for author validation.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Identify the Repository
|
||||
|
||||
// turbo
|
||||
|
||||
- Run: `git -C <project_root> remote get-url origin` to extract owner/repo
|
||||
|
||||
### 2. Fetch Open Feature Request Issues
|
||||
|
||||
// turbo
|
||||
|
||||
- Run: `gh issue list --repo <owner>/<repo> --state open --limit 50 --json number,title,labels,body,comments,createdAt,author`
|
||||
- Filter for issues that are feature requests (label `enhancement`/`feature`, or body describes new functionality, or previously classified as feature request)
|
||||
- Sort by oldest first
|
||||
|
||||
### 3. Analyze Each Feature Request
|
||||
|
||||
For each feature request issue, perform a **two-level analysis**:
|
||||
|
||||
#### Level 1 — Viability Assessment
|
||||
|
||||
Ask yourself:
|
||||
|
||||
- Does this feature align with the project's goals and architecture?
|
||||
- Is the request technically feasible with the current codebase?
|
||||
- Does it duplicate existing functionality?
|
||||
- Would it introduce breaking changes or security risks?
|
||||
- Is there enough detail to implement it?
|
||||
|
||||
**Verdict options:**
|
||||
|
||||
1. ✅ **VIABLE** — Makes sense, enough detail to implement → Go to Level 2
|
||||
2. ❓ **NEEDS MORE INFO** — Good idea but insufficient detail → Post comment asking for specifics
|
||||
3. ❌ **NOT VIABLE** — Doesn't fit the project or is fundamentally flawed → Post comment explaining why, close issue
|
||||
|
||||
#### Level 2 — Implementation (only for VIABLE features)
|
||||
|
||||
1. **Research** — Read all related source files to understand the current architecture
|
||||
2. **Design** — Plan the implementation, filling gaps in the original request
|
||||
3. **Create branch** — Name format: `feat/issue-<NUMBER>-<short-slug>`
|
||||
```bash
|
||||
git checkout main
|
||||
git pull origin main
|
||||
git checkout -b feat/issue-<NUMBER>-<short-slug>
|
||||
```
|
||||
4. **Implement** — Build the complete solution following project patterns
|
||||
5. **Build** — Run `npm run build` to verify compilation
|
||||
6. **Commit** — Commit with: `feat: <description> (#<NUMBER>)`
|
||||
7. **Push** — Push the branch: `git push -u origin feat/issue-<NUMBER>-<short-slug>`
|
||||
8. **Return to main** — `git checkout main`
|
||||
|
||||
### 4. Respond to Authors
|
||||
|
||||
#### For VIABLE (implemented) features:
|
||||
|
||||
// turbo
|
||||
Post a comment on the issue:
|
||||
|
||||
````markdown
|
||||
## ✅ Feature Implemented!
|
||||
|
||||
Hi @<author>! We've analyzed your request and implemented it on a dedicated branch.
|
||||
|
||||
**Branch:** `feat/issue-<NUMBER>-<short-slug>`
|
||||
|
||||
### What was implemented:
|
||||
|
||||
- <bullet list of what was done>
|
||||
|
||||
### How to try it:
|
||||
|
||||
```bash
|
||||
git fetch origin
|
||||
git checkout feat/issue-<NUMBER>-<short-slug>
|
||||
npm install && npm run dev
|
||||
```
|
||||
````
|
||||
|
||||
### Next steps:
|
||||
|
||||
1. **Test it** — Please verify it works as you expected
|
||||
2. **Want to improve it?** — You're welcome to contribute! Just:
|
||||
```bash
|
||||
git checkout feat/issue-<NUMBER>-<short-slug>
|
||||
# Make your improvements
|
||||
git add -A && git commit -m "improve: <your changes>"
|
||||
git push origin feat/issue-<NUMBER>-<short-slug>
|
||||
```
|
||||
Then open a Pull Request from your branch to `main` 🎉
|
||||
3. **Not quite right?** — Let us know in this issue what needs to change
|
||||
|
||||
Looking forward to your feedback! 🚀
|
||||
|
||||
```
|
||||
|
||||
#### For NEEDS MORE INFO:
|
||||
// turbo
|
||||
Post a comment asking for specific missing details needed to implement, e.g.:
|
||||
- "Could you describe the exact behavior when X happens?"
|
||||
- "Which API endpoints should be affected?"
|
||||
- "Should this apply to all providers or only specific ones?"
|
||||
|
||||
Add the context of WHY you need each piece of information.
|
||||
|
||||
#### For NOT VIABLE:
|
||||
// turbo
|
||||
Post a polite comment explaining why the feature doesn't fit at this time:
|
||||
- If the idea is decent but timing is wrong: "This is an interesting idea, but it doesn't align with our current priorities. Feel free to open a new issue with more details if you'd like us to reconsider."
|
||||
- If fundamentally flawed: Explain the technical or architectural reasons why it won't work, suggest alternatives if possible.
|
||||
- Close the issue after posting the comment.
|
||||
|
||||
### 5. Summary Report
|
||||
Present a summary report to the user via `notify_user`:
|
||||
|
||||
| Issue | Title | Verdict | Branch / Action |
|
||||
|---|---|---|---|
|
||||
| #N | Title | ✅ Implemented | `feat/issue-N-slug` |
|
||||
| #N | Title | ❓ Needs Info | Comment posted |
|
||||
| #N | Title | ❌ Not Viable | Closed with explanation |
|
||||
```
|
||||
166
.agents/workflows/resolve-issues-ag.md
Normal file
166
.agents/workflows/resolve-issues-ag.md
Normal file
@@ -0,0 +1,166 @@
|
||||
---
|
||||
description: Fetch all open GitHub issues, analyze bugs, resolve what's possible, triage the rest, wait for user validation, then commit and release
|
||||
---
|
||||
|
||||
# /resolve-issues — Automated Issue Resolution Workflow
|
||||
|
||||
## Overview
|
||||
|
||||
This workflow fetches all open issues from the project's GitHub repository, classifies them, analyzes bugs, proposes a resolution plan, waits for user validation, and ONLY THEN implements the fixes, commits, and closes the issues on the current release branch (`release/vX.Y.Z`). It does NOT merge or release automatically — the release branch is later merged via PR to main.
|
||||
|
||||
> **BRANCH RULE**: All work MUST happen on the current `release/vX.Y.Z` branch. Never create separate `fix/` branches. If no release branch exists yet, create one first using `/generate-release` Phase 1 steps 1–5.
|
||||
|
||||
> **⛔ PR PROHIBITION**: If a fix is associated with a contributor's PR, you MUST merge their PR — NEVER close it and re-implement the fix yourself. See `/review-prs` workflow for the full policy. The `gh pr close` command is FORBIDDEN unless the repository owner explicitly requests it.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Identify the GitHub Repository
|
||||
|
||||
// turbo
|
||||
|
||||
- Run: `git -C <project_root> remote get-url origin` to extract the owner/repo
|
||||
- Parse the owner and repo name from the URL
|
||||
|
||||
### 2. Ensure Release Branch Exists
|
||||
|
||||
// turbo
|
||||
|
||||
Before doing any work, ensure you are on the current release branch:
|
||||
|
||||
```bash
|
||||
# Check current branch
|
||||
git branch --show-current
|
||||
|
||||
# If on main, determine next version and create the release branch
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
NEXT=$(node -p "const [a,b,c]=('$VERSION').split('.').map(Number); c>=9?a+'.'+(b+1)+'.0':a+'.'+b+'.'+(c+1)")
|
||||
git checkout -b release/v$NEXT
|
||||
npm version patch --no-git-tag-version
|
||||
npm install
|
||||
```
|
||||
|
||||
If already on a `release/vX.Y.Z` branch, continue working there.
|
||||
|
||||
### 3. Fetch All Open Issues
|
||||
|
||||
// turbo-all
|
||||
|
||||
**⚠️ CRITICAL**: The JSON output of `gh issue list` can be truncated by the tool, silently hiding issues. You MUST use the two-step approach below to guarantee **all** issues are fetched.
|
||||
|
||||
**Step 3a — Get Issue numbers only** (small output, never truncated):
|
||||
|
||||
- Run: `gh issue list --repo <owner>/<repo> --state open --limit 500 --json number --jq '.[].number'`
|
||||
- This outputs one issue number per line. Count them and confirm total.
|
||||
|
||||
**Step 3b — Fetch full metadata for each Issue** (one call per issue):
|
||||
|
||||
- For each issue number from step 3a, run:
|
||||
`gh issue view <NUMBER> --repo <owner>/<repo> --json number,title,labels,body,comments,createdAt,author`
|
||||
- You may batch these into parallel calls (up to 4 at a time).
|
||||
- Sort by oldest first (FIFO).
|
||||
|
||||
### 4. Classify Each Issue
|
||||
|
||||
For each issue, determine its type:
|
||||
|
||||
- **Bug** — Has `bug` label, or body contains error messages, stack traces, "doesn't work", "broken", "crash", "error"
|
||||
- **Feature Request** — Has `enhancement`/`feature` label, or body describes new functionality
|
||||
- **Question** — Has `question` label, or is asking "how to" something
|
||||
- **Other** — Anything else
|
||||
|
||||
Focus ONLY on **Bugs** for resolution. Feature requests and questions should be skipped with a note in the final report.
|
||||
|
||||
### 5. Deep-Read Each Bug Issue (One-by-One Analysis)
|
||||
|
||||
**IMPORTANT**: Read each bug issue thoroughly, one at a time, before moving to the next. This is NOT a batch process — each issue needs focused attention.
|
||||
|
||||
#### 5a. Understand the Problem
|
||||
|
||||
For each bug issue, perform the full analysis:
|
||||
|
||||
1. **Read the entire body** — including Description, Steps to Reproduce, Expected/Actual Behavior, Error Logs, and Screenshots
|
||||
2. **Read ALL comments** — including bot triage comments (Kilo, etc.) and owner/community responses. Pay attention to:
|
||||
- Whether someone already responded with a fix
|
||||
- Whether a community member confirmed the issue is resolved
|
||||
- Whether the issue was marked as duplicate by a bot. **WARNING: DO NOT blindly trust bot duplicate labels (e.g., kilo-duplicate). Bots make mistakes. You MUST read the full conversation and do your own independent analysis to determine if it is truly a duplicate or a distinct bug.**
|
||||
3. **Identify the claimed error** — extract the exact error message, status code, and provider/model involved
|
||||
|
||||
#### 5b. Check Information Sufficiency
|
||||
|
||||
Verify the issue contains enough to act on:
|
||||
|
||||
- [ ] Clear description of the problem
|
||||
- [ ] Steps to reproduce OR error logs
|
||||
- [ ] Provider/model/version information
|
||||
- [ ] Expected vs actual behavior
|
||||
|
||||
#### 5c. Determine Issue Disposition
|
||||
|
||||
For each bug, classify into one of 5 actions:
|
||||
|
||||
| Disposition | When to Apply | Action |
|
||||
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
|
||||
| **✅ CLOSE — Already Fixed** | Owner responded with fix + no user follow-up, OR community confirmed fix | Close with comment citing which version fixed it |
|
||||
| **✅ CLOSE — Duplicate** | You have independently verified the issue is a duplicate (do NOT rely solely on bot flags) + user provides no new info | Close referencing the original issue |
|
||||
| **✅ CLOSE — Stale** | We requested logs/info > 7 days ago with no reply | Close thanking the user, invite to reopen if needed |
|
||||
| **📝 RESPOND — Needs Info** | Issue is real but missing critical reproduction details | Comment asking for specifics per `/issue-triage` |
|
||||
| **📝 RESPOND — User Config** | Error is caused by unsupported env (Node version, wrong model path, missing API enablement) | Comment explaining the user-side fix |
|
||||
| **🔧 FIX — Code Change** | Root cause is confirmed in the codebase | Research, propose solution in report, wait for approval |
|
||||
|
||||
#### 5d. For "FIX — Code Change" Issues
|
||||
|
||||
Before coding, perform deep source analysis to formulate a plan:
|
||||
|
||||
1. **Search the codebase** — `grep_search` for error strings, relevant function names, affected files
|
||||
2. **Search the web** — for upstream API changes, SDK updates, or breaking changes that explain the bug
|
||||
3. **Read the full source file** — don't rely on grep snippets; understand the surrounding logic
|
||||
4. **Verify the root cause** — confirm the bug is reproducible based on the code, not just a user misconfiguration
|
||||
5. **Formulate a proposed solution** — detail the exact files and lines you will change and how you will solve it.
|
||||
6. **Create an Implementation Plan file** — write your proposed solution to `_tasks/features-vX.Y.Z/<ISSUE_NUMBER>-<short-description>.plan.md` (e.g. `_tasks/features-v3.7.6/1810-auto-restore-probe-failed-db.plan.md`) where `vX.Y.Z` is the current branch version. The plan should contain an Overview, Pre-Implementation Checklist, and detailed Implementation Steps (Files, Changes).
|
||||
7. **DO NOT modify the codebase yet** — wait for user approval on your report and plan first.
|
||||
|
||||
#### 5e. For "RESPOND" Issues
|
||||
|
||||
Post a substantive comment that:
|
||||
|
||||
- Acknowledges the specific error they reported
|
||||
- Explains the likely root cause
|
||||
- Provides concrete steps to resolve (version upgrade, env var fix, model path correction)
|
||||
- Asks for follow-up info if needed
|
||||
|
||||
**Do NOT post generic template responses.** Every comment should reference the user's specific error messages and environment.
|
||||
|
||||
### 6. Generate Report & Wait for Validation
|
||||
|
||||
Present a summary report to the user detailing your proposed actions. For any bugs that need fixing, explicitly explain your proposed solution (files to change and logic) and point out that it will be implemented on the release branch (`release/vX.Y.Z`) after approval.
|
||||
|
||||
| Issue | Title | Status | Proposed Action / Version |
|
||||
| ----- | ----- | ------------- | ----------------------------------------- |
|
||||
| #N | Title | ✅ Close | Already fixed / duplicate (explain why) |
|
||||
| #N | Title | 🔧 Propose | Explanation of the code fix to be applied |
|
||||
| #N | Title | 📝 Respond | Guidance comment to be posted |
|
||||
| #N | Title | ❓ Needs Info | Triage comment to be posted |
|
||||
| #N | Title | ⏭️ Skip | Feature request / not a bug |
|
||||
|
||||
> **⚠️ IMPORTANT**: Do NOT implement code changes, commit, push, or close issues at this step.
|
||||
> Wait for the user to review the proposed fixes and respond with **OK** before proceeding.
|
||||
|
||||
- If the user says **OK** or approves → Proceed to step 7
|
||||
- If the user requests changes → Adjust the proposed solution and present the report again
|
||||
- If the user rejects → Revert any accidental changes and stop
|
||||
|
||||
### 7. Implement Fixes, Run Tests & Commit (only after user approval)
|
||||
|
||||
After the user validates and gives the OK:
|
||||
|
||||
1. **Implement the fixes** — modify the codebase according to the approved plan.
|
||||
2. **Run tests** — `npm run test:all` (or the specific test file) to ensure 100% pass.
|
||||
3. **Update CHANGELOG.md** with all new bug fix entries.
|
||||
4. **Commit** each fix individually on the release branch with message format: `fix: <description> (#<issue_number>)`.
|
||||
5. **Push** the release branch: `git push origin release/vX.Y.Z`.
|
||||
6. **Close resolved issues immediately**. For each issue that was marked as Fixed, run:
|
||||
`gh issue close <NUMBER> --repo <owner>/<repo> --comment "Thank you for reporting! This issue has been fixed and will be included in the next release (vX.Y.Z)."`
|
||||
7. Likewise, close `Duplicate` issues referencing the original, close `Needs Info` if stale, and post the required comments.
|
||||
8. If the project runs automatic releases or needs a PR, proceed to run `/generate-release` workflow Phase 1 steps 7–10 (tests → commit → push → open PR to main → wait for user).
|
||||
|
||||
If NO fixes were committed, skip closing and source control steps and just conclude the workflow.
|
||||
@@ -1,120 +0,0 @@
|
||||
---
|
||||
description: Fetch all open GitHub issues, analyze bugs, resolve what's possible, triage the rest, wait for user validation, then commit and release
|
||||
---
|
||||
|
||||
# /resolve-issues — Automated Issue Resolution Workflow
|
||||
|
||||
## Overview
|
||||
|
||||
This workflow fetches all open issues from the project's GitHub repository, classifies them, analyzes bugs, resolves what can be fixed, and triages issues with insufficient information. **It does NOT merge or release automatically** — it creates a PR and waits for user validation before merging.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Identify the GitHub Repository
|
||||
|
||||
// turbo
|
||||
|
||||
- Run: `git -C <project_root> remote get-url origin` to extract the owner/repo
|
||||
- Parse the owner and repo name from the URL
|
||||
|
||||
### 2. Fetch All Open Issues
|
||||
|
||||
// turbo
|
||||
|
||||
- Run: `gh issue list --repo <owner>/<repo> --state open --limit 100 --json number,title,labels,body,comments,createdAt,author`
|
||||
- Parse the JSON output to get a list of all open issues
|
||||
- Sort by oldest first (FIFO)
|
||||
|
||||
### 3. Classify Each Issue
|
||||
|
||||
For each issue, determine its type:
|
||||
|
||||
- **Bug** — Has `bug` label, or body contains error messages, stack traces, "doesn't work", "broken", "crash", "error"
|
||||
- **Feature Request** — Has `enhancement`/`feature` label, or body describes new functionality
|
||||
- **Question** — Has `question` label, or is asking "how to" something
|
||||
- **Other** — Anything else
|
||||
|
||||
Focus ONLY on **Bugs** for resolution. Feature requests and questions should be skipped with a note in the final report.
|
||||
|
||||
### 4. Analyze Each Bug — For each bug issue:
|
||||
|
||||
#### 4a. Check Information Sufficiency
|
||||
|
||||
Verify the issue contains enough information to reproduce and fix:
|
||||
|
||||
- [ ] Clear description of the problem
|
||||
- [ ] Steps to reproduce
|
||||
- [ ] Error messages or logs
|
||||
- [ ] Expected vs actual behavior
|
||||
|
||||
#### 4b. If Information Is INSUFFICIENT
|
||||
|
||||
Call the `/issue-triage` workflow (located at `~/.gemini/antigravity/global_workflows/issue-triage.md`):
|
||||
// turbo
|
||||
|
||||
- Post a comment asking for more details using `gh issue comment`
|
||||
- Add `needs-info` label using `gh issue edit`
|
||||
- Mark this issue as **DEFERRED** and move to the next one
|
||||
|
||||
#### 4c. If Information Is SUFFICIENT
|
||||
|
||||
Proceed with resolution:
|
||||
|
||||
1. **Create a fix branch** — `git checkout -b fix/issue-<NUMBER>-<short-description>`
|
||||
2. **Research** — Search the codebase for files related to the issue
|
||||
3. **Root Cause** — Identify the root cause by reading the relevant source files
|
||||
4. **Implement Fix** — Apply the fix following existing code patterns and conventions
|
||||
5. **Test** — Build the project and run tests to verify the fix
|
||||
6. **Commit** — Commit with message format: `fix: <description> (#<issue_number>)`
|
||||
|
||||
### 5. Generate Report & Wait for Validation
|
||||
|
||||
Present a summary report to the user via `notify_user` with `BlockedOnUser: true`:
|
||||
|
||||
| Issue | Title | Status | Action |
|
||||
| ----- | ----- | ------------- | ----------------------------- |
|
||||
| #N | Title | ✅ Ready | Files changed (not committed) |
|
||||
| #N | Title | ❓ Needs Info | Triage comment posted |
|
||||
| #N | Title | ⏭️ Skipped | Feature request / not a bug |
|
||||
|
||||
> **⚠️ IMPORTANT**: Do NOT commit, close issues, or generate releases at this step.
|
||||
> Wait for the user to review the changes and respond with **OK** before proceeding.
|
||||
|
||||
- If the user says **OK** or approves → Proceed to step 6
|
||||
- If the user requests changes → Apply the requested adjustments first, then present the report again
|
||||
- If the user rejects → Revert the changes and stop
|
||||
|
||||
### 6. Commit & Push Fix Branch (only after user approval)
|
||||
|
||||
After the user validates:
|
||||
|
||||
- Commit each fix individually with message format: `fix: <description> (#<issue_number>)`
|
||||
- Push the fix branch: `git push origin fix/issue-<NUMBER>-<short-description>`
|
||||
- Create a PR: `gh pr create --title "fix: <description> (#<issue_number>)" --body "<details>" --base main`
|
||||
|
||||
### 7. 🛑 WAIT — Notify User & Await PR Verification
|
||||
|
||||
**This is a mandatory stop point.** Use `notify_user` with `BlockedOnUser: true`:
|
||||
|
||||
- Inform the user that the PR was created and is **awaiting their verification**
|
||||
- Include the PR number, URL, and a summary of what was changed
|
||||
- **DO NOT merge, close issues, generate releases, or deploy until the user confirms**
|
||||
|
||||
Wait for the user to respond:
|
||||
|
||||
- **User confirms** → Proceed to step 8
|
||||
- **User requests changes** → Apply changes, push to the same branch, notify again
|
||||
- **User rejects** → Close the PR and stop
|
||||
|
||||
### 8. Merge, Close Issues & Release (only after user confirms PR)
|
||||
|
||||
After the user confirms the PR:
|
||||
|
||||
1. **Merge** the PR: `gh pr merge <NUMBER> --merge --repo <owner>/<repo>` or via local merge
|
||||
2. **Close** resolved issues with a comment: `gh issue close <NUMBER> --repo <owner>/<repo> --comment "Fixed in <commit_hash>. The fix will be included in the next release."`
|
||||
3. **Switch to main**: `git checkout main && git pull`
|
||||
4. Run the `/update-docs` workflow (at `~/.gemini/antigravity/global_workflows/update-docs.md`) to update CHANGELOG and README
|
||||
5. Run the `/generate-release` workflow (at `.agents/workflows/generate-release.md`) to bump version, tag, and publish
|
||||
6. Deploy to local VPS: `ssh root@192.168.0.15 "npm install -g omniroute@<VERSION> && pm2 restart omniroute"`
|
||||
|
||||
If NO fixes were committed, skip this step and just present the report.
|
||||
120
.agents/workflows/review-discussions-ag.md
Normal file
120
.agents/workflows/review-discussions-ag.md
Normal file
@@ -0,0 +1,120 @@
|
||||
---
|
||||
description: Read all open GitHub Discussions, summarize them, respond to pending ones, and create issues from actionable feature requests
|
||||
---
|
||||
|
||||
# /review-discussions — GitHub Discussions Review & Response Workflow
|
||||
|
||||
## Overview
|
||||
|
||||
This workflow reads all open GitHub Discussions, generates a categorized summary, identifies which ones need a response, drafts and posts replies, and optionally creates issues from actionable feature requests. It follows the same flow used for Issues but adapted for the Discussions forum.
|
||||
|
||||
> **Tool mapping note (v3.8):** Where steps below say `browser_subagent`, in modern Claude Code substitute with the `gh` CLI via Bash — `gh api graphql` for reading discussions and mutations for posting comments. `WebFetch` is acceptable for read-only HTML scraping when GraphQL is overkill, but prefer `gh` for any write actions.
|
||||
|
||||
// turbo-all
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Identify the GitHub Repository
|
||||
|
||||
- Run: `git -C <project_root> remote get-url origin` to extract the owner/repo
|
||||
- Parse the owner and repo name from the URL
|
||||
|
||||
### 2. Fetch All Open Discussions
|
||||
|
||||
- Use `WebFetch` to fetch `https://github.com/<owner>/<repo>/discussions`
|
||||
- Parse the discussion list to get all discussion titles, IDs, authors, categories, and dates
|
||||
- For each discussion, fetch the individual page to read the full content and all comments/replies
|
||||
|
||||
### 3. Summarize All Discussions
|
||||
|
||||
For each discussion, extract:
|
||||
|
||||
- **Title** and **#Number**
|
||||
- **Author** (GitHub username)
|
||||
- **Category** (Announcements, General, Ideas, Q&A, Show and tell)
|
||||
- **Date** created
|
||||
- **Summary** of the original post (1-2 sentences)
|
||||
- **Comments count** and key participants
|
||||
- **Your previous response** (if any)
|
||||
- **Pending action** — whether a response or follow-up is needed
|
||||
|
||||
### 4. Present Summary Report to User
|
||||
|
||||
Present the full summary to the user organized by category, using a table:
|
||||
|
||||
| # | Category | Title | Author | Date | Status |
|
||||
| --- | -------- | ----- | ------ | ------ | ----------------- |
|
||||
| #N | Ideas | Title | @user | Mar 23 | ⚠️ Needs response |
|
||||
| #N | Q&A | Title | @user | Mar 9 | ✅ Answered |
|
||||
| #N | General | Title | @user | Mar 19 | ⚠️ Needs response |
|
||||
|
||||
Highlight:
|
||||
|
||||
- **⚠️ Needs response** — No reply from maintainer, or a follow-up comment was left unanswered
|
||||
- **✅ Answered** — Maintainer already responded
|
||||
- **🐛 Bug reported** — A bug was mentioned that needs tracking
|
||||
- **💡 Actionable** — Contains a concrete feature request that could become an issue
|
||||
|
||||
### 5. Draft & Post Responses
|
||||
|
||||
For each discussion that needs a response, draft a reply following these guidelines:
|
||||
|
||||
#### Response Style
|
||||
|
||||
- **Friendly and professional** — Start with "Hey @username!"
|
||||
- **Acknowledge the contribution** — Thank the user for their input
|
||||
- **Be specific** — Reference existing features, settings, or dashboard pages if the feature already exists
|
||||
- **Provide workarounds** — If the request isn't implemented yet, suggest current alternatives
|
||||
- **Commit to action** — If the request is valid, state that you'll open an issue or add it to the roadmap
|
||||
- **Keep it concise** — 3-5 paragraphs max
|
||||
|
||||
#### Posting via Browser
|
||||
|
||||
- Use `browser_subagent` to navigate to each discussion and post the comment
|
||||
- **IMPORTANT**: When typing text in GitHub comment boxes via the browser, use only plain ASCII characters:
|
||||
- Use regular hyphens `-` instead of em-dashes
|
||||
- Use `->` instead of arrow symbols
|
||||
- Do NOT use emoji Unicode characters (the browser keyboard may fail on them)
|
||||
- Use `**bold**` and `\`code\`` markdown formatting
|
||||
- Click the green "Comment" button (or "Reply" for threaded replies) after typing
|
||||
- Verify the comment was posted by checking the page shows the new comment
|
||||
|
||||
### 6. Create Issues from Actionable Feature Requests
|
||||
|
||||
For discussions that contain concrete, actionable feature requests:
|
||||
|
||||
1. Ask the user which ones should become issues
|
||||
2. For each approved request, create a GitHub issue via `browser_subagent`:
|
||||
- Navigate to `https://github.com/<owner>/<repo>/issues/new`
|
||||
- **Title**: `<Feature Name> - <Short description>`
|
||||
- **Body** should include:
|
||||
- `## Feature Request` header
|
||||
- `**Source:** Discussion #N by @author`
|
||||
- `## Problem` — What limitation the user hit
|
||||
- `## Proposed Solution` — How it could work
|
||||
- `### Implementation Ideas` — Technical approach
|
||||
- `### Current Workarounds` — What users can do today
|
||||
- `## Additional Context` — Links to related issues/discussions
|
||||
- Add `enhancement` label
|
||||
- Click "Submit new issue" / "Create"
|
||||
3. After creation, go back to the original discussion and post a comment linking to the new issue:
|
||||
- "I've opened Issue #N to track this feature request. Follow along there for updates!"
|
||||
|
||||
### 7. Final Report
|
||||
|
||||
Present a final summary to the user:
|
||||
|
||||
| Discussion | Action Taken |
|
||||
| ---------- | ---------------------------------- |
|
||||
| #N — Title | Responded with workarounds |
|
||||
| #N — Title | Responded + created Issue #N |
|
||||
| #N — Title | Already answered, no action needed |
|
||||
| #N — Title | Responded to follow-up comment |
|
||||
|
||||
## Notes
|
||||
|
||||
- This workflow is **interactive** — always present the summary and wait for user approval before posting responses or creating issues
|
||||
- If the user says "pode responder" (or similar approval), proceed with posting all drafted responses
|
||||
- For discussions in non-English languages, respond in the same language as the original post
|
||||
- Always reference specific dashboard paths, config options, or code files when explaining existing features
|
||||
- When a discussion reveals a bug, note it separately from feature requests
|
||||
256
.agents/workflows/review-prs-ag.md
Normal file
256
.agents/workflows/review-prs-ag.md
Normal file
@@ -0,0 +1,256 @@
|
||||
---
|
||||
description: Analyze open Pull Requests from the project's GitHub repository, generate a critical report, and optionally implement approved changes
|
||||
---
|
||||
|
||||
# /review-prs — PR Review & Analysis Workflow
|
||||
|
||||
## ⛔ ABSOLUTE PROHIBITION — Read Before Anything Else
|
||||
|
||||
> **NEVER close a contributor's PR if you intend to use ANY of their code, ideas, or fixes.**
|
||||
>
|
||||
> **NEVER manually integrate contributor code into a release branch and then close their PR.**
|
||||
>
|
||||
> These actions are **STRICTLY FORBIDDEN** under all circumstances:
|
||||
>
|
||||
> 1. ❌ Closing a PR and cherry-picking/copying its code into a release branch
|
||||
> 2. ❌ Closing a PR "because of conflicts" and re-implementing the same fix yourself
|
||||
> 3. ❌ Closing a PR and committing a "similar" solution inspired by it
|
||||
> 4. ❌ Using `gh pr close` on any PR whose content was or will be used
|
||||
>
|
||||
> **Why**: Closing a PR after taking the contributor's work means they get ZERO credit on GitHub — no "Merged" badge, no contribution graph entry, no public record. This is effectively stealing their contribution. An audit found this happened to **37 PRs** in the past.
|
||||
>
|
||||
> **The ONLY acceptable flow**: Resolve conflicts IN the contributor's branch, push fixes TO their branch, then merge THEIR PR via `gh pr merge`. See Step 7 and Step 8 for the exact procedure.
|
||||
>
|
||||
> **When to close a PR**: ONLY when the user (repository owner) explicitly requests it, OR when the PR is clearly spam/malicious, OR when the author themselves asks to close it. In ALL other cases, leave it open.
|
||||
|
||||
## Overview
|
||||
|
||||
This workflow fetches all open PRs from the project's GitHub repository, performs a critical analysis of each one, generates a detailed report, and waits for user approval before proceeding with implementation. **All improvements are committed on the current release branch** (`release/vX.Y.Z`).
|
||||
|
||||
> **BRANCH RULE**: PRs are ALWAYS merged into the current `release/vX.Y.Z` branch, NEVER directly into `main`. The release branch acts as a staging area — only after all PRs are integrated and tests pass does the release branch get merged into `main` via the `/generate-release` workflow.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Identify the GitHub Repository
|
||||
|
||||
- Read `package.json` to get the repository URL, or use the git remote origin URL
|
||||
// turbo
|
||||
- Run: `git -C <project_root> remote get-url origin` to extract the owner/repo
|
||||
|
||||
### 2. Ensure Release Branch Exists
|
||||
|
||||
// turbo
|
||||
|
||||
Before doing any work, ensure you are on the current release branch:
|
||||
|
||||
```bash
|
||||
# Check current branch
|
||||
git branch --show-current
|
||||
|
||||
# If on main, determine next version and create the release branch
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
# Bump patch: e.g. 3.3.11 → 3.3.12
|
||||
NEXT=$(node -p "const [a,b,c]=('$VERSION').split('.').map(Number); c>=9?a+'.'+(b+1)+'.0':a+'.'+b+'.'+(c+1)")
|
||||
git checkout -b release/v$NEXT
|
||||
npm version patch --no-git-tag-version
|
||||
npm install
|
||||
```
|
||||
|
||||
If already on a `release/vX.Y.Z` branch, continue working there.
|
||||
|
||||
### 3. Fetch Open Pull Requests
|
||||
|
||||
// turbo-all
|
||||
|
||||
**⚠️ CRITICAL**: The JSON output of `gh pr list` can be truncated by the tool, silently hiding PRs. You MUST use the two-step approach below to guarantee **all** PRs are fetched.
|
||||
|
||||
**Step 3a — Get PR numbers only** (small output, never truncated):
|
||||
|
||||
- Run: `gh pr list --repo <owner>/<repo> --state open --limit 500 --json number --jq '.[].number'`
|
||||
- This outputs one PR number per line. Count them and confirm total.
|
||||
|
||||
**Step 3b — Fetch full metadata for each PR** (one call per PR):
|
||||
|
||||
- For each PR number from step 3a, run:
|
||||
`gh pr view <NUMBER> --repo <owner>/<repo> --json number,title,author,headRefName,baseRefName,body,createdAt,additions,deletions,files`
|
||||
- You may batch these into parallel calls (up to 4 at a time).
|
||||
|
||||
**Step 3c — Fetch diffs for each PR** (one call per PR, saved to /tmp):
|
||||
|
||||
- For each PR number, run:
|
||||
`gh pr diff <NUMBER> --repo <owner>/<repo> > /tmp/pr<NUMBER>.diff`
|
||||
- Then read each diff file with the appropriate file-read tool (`Read` in Claude Code; equivalent in your agent runtime).
|
||||
|
||||
- For each open PR, collect:
|
||||
- PR number, title, author, branch, number of commits, date
|
||||
- PR description/body
|
||||
- Files changed (diff)
|
||||
- Existing review comments (from bots or humans)
|
||||
|
||||
**Verification**: Confirm the count of PRs analyzed matches the count from step 3a before proceeding.
|
||||
|
||||
### 3.5 Redirect PR Base Branches to Release Branch
|
||||
|
||||
// turbo-all
|
||||
|
||||
**⚠️ CRITICAL**: Contributors typically open PRs targeting `main`. Before analyzing or merging, redirect ALL open PRs to target the current release branch instead.
|
||||
|
||||
```bash
|
||||
# Get the current release branch name
|
||||
RELEASE_BRANCH=$(git branch --show-current) # e.g. release/v3.5.4
|
||||
|
||||
# For each open PR that targets main, change its base to the release branch
|
||||
for PR_NUM in $(gh pr list --repo <owner>/<repo> --state open --json number,baseRefName --jq '.[] | select(.baseRefName == "main") | .number'); do
|
||||
echo "Redirecting PR #$PR_NUM → $RELEASE_BRANCH"
|
||||
gh pr edit "$PR_NUM" --repo <owner>/<repo> --base "$RELEASE_BRANCH"
|
||||
done
|
||||
```
|
||||
|
||||
This ensures:
|
||||
|
||||
1. PRs merge into the release branch, not directly into `main`
|
||||
2. Merge conflict detection is accurate against the release branch
|
||||
3. The release branch accumulates all changes before the final merge to `main`
|
||||
4. If the release branch doesn't exist on remote yet, push it first: `git push origin $RELEASE_BRANCH`
|
||||
|
||||
### 4. Analyze Each PR — For each open PR, perform the following analysis:
|
||||
|
||||
#### 4a. Feature Assessment
|
||||
|
||||
- **Does it make sense?** Evaluate if the feature fills a real gap or solves a valid problem
|
||||
- **Alignment** — Check if it aligns with the project's architecture and roadmap
|
||||
- **Complexity** — Assess if the scope is reasonable or if it should be split
|
||||
|
||||
#### 4b. Code Quality Review
|
||||
|
||||
- Check for code duplication
|
||||
- Evaluate error handling patterns (consistent with existing codebase?)
|
||||
- Check naming conventions and code style
|
||||
- Verify TypeScript types (any `any` usage, missing types?)
|
||||
|
||||
#### 4c. Security Review
|
||||
|
||||
- Check for missing authentication/authorization on new endpoints
|
||||
- Check for injection vulnerabilities (URL params, SQL, XSS)
|
||||
- Verify input validation on all user-controlled data
|
||||
- Check for hardcoded secrets or credentials
|
||||
|
||||
#### 4d. Architecture Review
|
||||
|
||||
- Does the change follow existing patterns?
|
||||
- Are there any breaking changes to public APIs?
|
||||
- Is the database schema affected? Migration needed?
|
||||
- Impact on performance (N+1 queries, missing indexes?)
|
||||
|
||||
#### 4e. Test Coverage
|
||||
|
||||
- Does the PR include tests?
|
||||
- Are edge cases covered?
|
||||
- Would existing tests break?
|
||||
|
||||
#### 4f. Cross-Layer (Global) Analysis
|
||||
|
||||
Perform a **global impact assessment** to verify whether the PR changes are complete across all layers of the application:
|
||||
|
||||
- **Backend → Frontend check**: If the PR adds or modifies backend-only resources (new endpoints, services, data models), evaluate whether corresponding frontend changes are missing:
|
||||
- Does a new endpoint require a new screen/page in the dashboard?
|
||||
- Should there be a new action button, menu item, or navigation link?
|
||||
- Are there new data fields that should be displayed or editable in the UI?
|
||||
- Does a new feature need a toggle, configuration panel, or status indicator?
|
||||
- **Frontend → Backend check**: If the PR adds frontend elements, verify the backend support exists:
|
||||
- Are the required API endpoints implemented?
|
||||
- Is the data model sufficient for the new UI components?
|
||||
- **Cross-cutting concerns**: Check shared layers (types, DTOs, validation schemas, routes, middleware) for completeness
|
||||
- **Document gaps** — If missing layers are detected, list them as **IMPORTANT** issues in the report with concrete suggestions for what should be added
|
||||
|
||||
### 5. Generate Report — Create a markdown report for each PR including:
|
||||
|
||||
- **PR Summary** — What it does, files affected, commit count
|
||||
- **Improvements/Benefits** — Numbered list with impact level (HIGH/MEDIUM/LOW)
|
||||
- **Risks & Issues** — Categorized as CRITICAL / IMPORTANT / MINOR
|
||||
- **Scoring Table** — Rate across: Feature Relevance, Code Quality, Security, Robustness, Tests
|
||||
- **Verdict** — Ready to merge? With mandatory vs optional fixes
|
||||
- **Next Steps** — What will happen if approved
|
||||
|
||||
### 6. Present to User
|
||||
|
||||
- Show the report in the final response and stop. Mark this as a blocking checkpoint awaiting explicit user approval.
|
||||
- Wait for user decision:
|
||||
- **Approved** → Proceed to step 7
|
||||
- **Approved with changes** → Implement the fixes and corrections before merging
|
||||
- **Rejected** → Close the PR or leave a review comment
|
||||
|
||||
### 7. Pre-Merge Fixes & CI Green-Lighting (if approved)
|
||||
|
||||
> **⚠️ Fixes and Conflict Resolutions MUST be pushed back to the PR branch before merging.** We want the PR itself to be green and fully valid before it integrates.
|
||||
|
||||
- **Sync latest fixes & Resolve Conflicts:** Merge the current `release` branch into the PR branch. If there are merge conflicts, you MUST resolve them inside the author's PR branch. NEVER resolve conflicts by closing their PR and doing the work in a separate branch, as this steals credit from the original author.
|
||||
- **Implement improvements:** Apply the required fixes identified in the analysis directly on the PR branch (e.g., adding missing API routes, fixing SSRF, applying comments from other agents).
|
||||
- **Pushing changes to PR branches:**
|
||||
|
||||
```bash
|
||||
# Checkout the PR locally
|
||||
gh pr checkout <NUMBER>
|
||||
|
||||
# Apply fixes, commit your changes
|
||||
git commit -m "chore: apply review suggestions and missing layers"
|
||||
|
||||
# Attempt to push directly to the PR branch
|
||||
git push
|
||||
```
|
||||
|
||||
- **Fallback (ONLY for external forks without maintainer edit access):**
|
||||
Using `cherry-pick` instead of fixing the contributor's PR directly is a **LAST RESORT**. You MUST ALWAYS attempt to `git push` your fixes to their branch first.
|
||||
**ONLY if `git push` explicitly fails with a permission/access error** (meaning the contributor unchecked "Allow edits from maintainers" or it's a locked fork), you may use `git cherry-pick` to bring their changes into the release branch and fix the issues locally.
|
||||
Even then, ensure you preserve the contributor's authorship (`git commit --author="Contributor Name <email>"` if creating new commits).
|
||||
Once you have integrated their work into the release branch, **DO NOT close their PR**. Leave it open so the contributor retains credit. Under NO CIRCUMSTANCES should you use `gh pr close`.
|
||||
|
||||
- Run the project's test suite locally to verify nothing breaks:
|
||||
// turbo
|
||||
- Run: `npm test` or equivalent test command
|
||||
|
||||
### 8. Merge into Release Branch (NEVER CLOSE!)
|
||||
|
||||
> **⚠️ CRITICAL**: NEVER use `gh pr close` for a PR whose idea or code was accepted. Closing a PR in a contributor's face after taking their idea—or closing it just because it had conflicts—is unacceptable.
|
||||
> You MUST ALWAYS resolve conflicts and apply fixes ON THE AUTHOR'S PR BRANCH (unless explicitly locked from edits), and then merge the PR using GitHub so the contributor gets the official "Merged" badge and proper credit on their profile. **Do not use cherry-pick just because it is "easier" than resolving conflicts on their branch.**
|
||||
|
||||
Even if the PR had severe conflicts or required significant architectural adjustments, you MUST:
|
||||
|
||||
1. Resolve any conflicts and apply the fixes directly to their PR branch (as detailed in step 7) or use cherry-picking into the release branch.
|
||||
2. If you managed to fix their branch, merge it into the release branch using the GitHub CLI:
|
||||
`gh pr merge <NUMBER> --repo <owner>/<repo> --squash --body "Integrated into release/vX.Y.Z"`
|
||||
3. If you had to use cherry-picking because you couldn't push to their branch, DO NOT close the PR. GitHub will sometimes auto-detect the cherry-picked commits and mark it as Merged. If it doesn't, leave it open. The repository owner will handle it. NEVER run `gh pr close`.
|
||||
|
||||
In ALL cases:
|
||||
|
||||
- Post a **thank-you comment** on the PR via the GitHub API before or immediately after merging.
|
||||
- The message should:
|
||||
- Thank the author by name/username for their contribution.
|
||||
- Explain what was adjusted or improved (if we pushed fixes to their branch or cherry-picked).
|
||||
- Note it will be included in the upcoming release.
|
||||
- Be friendly, professional, and encouraging.
|
||||
|
||||
> **⚠️ MANDATORY CHANGELOG CREDIT**: When cherry-picking is used (because the PR branch couldn't be pushed to or `gh pr merge` failed), the contributor does NOT get the automatic GitHub "Merged" badge. In this case, you MUST compensate by adding an explicit entry to `CHANGELOG.md` in the `[Unreleased]` section with `(#PR_NUMBER — thanks @username)` format. This ensures the contributor gets public credit in the release notes even if GitHub doesn't auto-detect the cherry-pick. This is NOT optional — skipping it effectively erases the contributor's work from the release record.
|
||||
|
||||
### 9. Sync Local Release Branch
|
||||
|
||||
After merging PRs, sync the local release branch to include the new changes:
|
||||
|
||||
```bash
|
||||
git fetch origin
|
||||
git pull origin release/vX.Y.Z
|
||||
```
|
||||
|
||||
### 10. Continue or Finalize
|
||||
|
||||
After processing all approved PRs:
|
||||
|
||||
- If more PRs remain, go back to step 7
|
||||
- When all PRs are processed, **update CHANGELOG.md** on the release branch with all new entries
|
||||
- Run **test coverage** to verify the gate (≥75% statements/lines/functions, ≥70% branches — measured ~82%):
|
||||
```bash
|
||||
npm run test:coverage
|
||||
```
|
||||
- Fix any test regressions introduced by merged PRs
|
||||
- Run `/generate-release` workflow Phase 1 steps 7–10 (tests → commit → push → open PR to main → wait for user)
|
||||
- The `/generate-release` workflow handles the final merge from `release/vX.Y.Z` → `main`
|
||||
@@ -1,145 +0,0 @@
|
||||
---
|
||||
description: Analyze open Pull Requests from the project's GitHub repository, generate a critical report, and optionally implement approved changes
|
||||
---
|
||||
|
||||
# /review-prs — PR Review & Analysis Workflow
|
||||
|
||||
## Overview
|
||||
|
||||
This workflow fetches all open PRs from the project's GitHub repository, performs a critical analysis of each one, generates a detailed report, and waits for user approval before proceeding with implementation. **All improvements are committed on top of the PR branch** and the user must verify before merge.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Identify the GitHub Repository
|
||||
|
||||
- Read `package.json` to get the repository URL, or use the git remote origin URL
|
||||
// turbo
|
||||
- Run: `git -C <project_root> remote get-url origin` to extract the owner/repo
|
||||
|
||||
### 2. Fetch Open Pull Requests
|
||||
|
||||
- Navigate to `https://github.com/<owner>/<repo>/pulls` and scrape all open PRs
|
||||
- For each open PR, collect:
|
||||
- PR number, title, author, branch, number of commits, date
|
||||
- PR description/body
|
||||
- Files changed (diff)
|
||||
- Existing review comments (from bots or humans)
|
||||
|
||||
### 3. Analyze Each PR — For each open PR, perform the following analysis:
|
||||
|
||||
#### 3a. Feature Assessment
|
||||
|
||||
- **Does it make sense?** Evaluate if the feature fills a real gap or solves a valid problem
|
||||
- **Alignment** — Check if it aligns with the project's architecture and roadmap
|
||||
- **Complexity** — Assess if the scope is reasonable or if it should be split
|
||||
|
||||
#### 3b. Code Quality Review
|
||||
|
||||
- Check for code duplication
|
||||
- Evaluate error handling patterns (consistent with existing codebase?)
|
||||
- Check naming conventions and code style
|
||||
- Verify TypeScript types (any `any` usage, missing types?)
|
||||
|
||||
#### 3c. Security Review
|
||||
|
||||
- Check for missing authentication/authorization on new endpoints
|
||||
- Check for injection vulnerabilities (URL params, SQL, XSS)
|
||||
- Verify input validation on all user-controlled data
|
||||
- Check for hardcoded secrets or credentials
|
||||
|
||||
#### 3d. Architecture Review
|
||||
|
||||
- Does the change follow existing patterns?
|
||||
- Are there any breaking changes to public APIs?
|
||||
- Is the database schema affected? Migration needed?
|
||||
- Impact on performance (N+1 queries, missing indexes?)
|
||||
|
||||
#### 3e. Test Coverage
|
||||
|
||||
- Does the PR include tests?
|
||||
- Are edge cases covered?
|
||||
- Would existing tests break?
|
||||
|
||||
#### 3f. Cross-Layer (Global) Analysis
|
||||
|
||||
Perform a **global impact assessment** to verify whether the PR changes are complete across all layers of the application:
|
||||
|
||||
- **Backend → Frontend check**: If the PR adds or modifies backend-only resources (new endpoints, services, data models), evaluate whether corresponding frontend changes are missing:
|
||||
- Does a new endpoint require a new screen/page in the dashboard?
|
||||
- Should there be a new action button, menu item, or navigation link?
|
||||
- Are there new data fields that should be displayed or editable in the UI?
|
||||
- Does a new feature need a toggle, configuration panel, or status indicator?
|
||||
- **Frontend → Backend check**: If the PR adds frontend elements, verify the backend support exists:
|
||||
- Are the required API endpoints implemented?
|
||||
- Is the data model sufficient for the new UI components?
|
||||
- **Cross-cutting concerns**: Check shared layers (types, DTOs, validation schemas, routes, middleware) for completeness
|
||||
- **Document gaps** — If missing layers are detected, list them as **IMPORTANT** issues in the report with concrete suggestions for what should be added
|
||||
|
||||
### 4. Generate Report — Create a markdown report for each PR including:
|
||||
|
||||
- **PR Summary** — What it does, files affected, commit count
|
||||
- **Improvements/Benefits** — Numbered list with impact level (HIGH/MEDIUM/LOW)
|
||||
- **Risks & Issues** — Categorized as CRITICAL / IMPORTANT / MINOR
|
||||
- **Scoring Table** — Rate across: Feature Relevance, Code Quality, Security, Robustness, Tests
|
||||
- **Verdict** — Ready to merge? With mandatory vs optional fixes
|
||||
- **Next Steps** — What will happen if approved
|
||||
|
||||
### 5. Present to User
|
||||
|
||||
- Show the report via `notify_user` with `BlockedOnUser: true`
|
||||
- Wait for user decision:
|
||||
- **Approved** → Proceed to step 6
|
||||
- **Approved with changes** → Implement the fixes and corrections before merging
|
||||
- **Rejected** → Close the PR or leave a review comment
|
||||
|
||||
### 6. Implementation (if approved)
|
||||
|
||||
- Checkout the PR branch: `gh pr checkout <NUMBER>`
|
||||
- Implement any required fixes identified in the analysis
|
||||
- If the Cross-Layer Analysis (3f) identified missing frontend/backend counterparts, implement them
|
||||
- **Commit improvements on top of the PR branch** with descriptive commit messages
|
||||
- Run the project's test suite to verify nothing breaks
|
||||
// turbo
|
||||
- Run: `npm test` or equivalent test command
|
||||
- Build the project to verify compilation
|
||||
// turbo
|
||||
- Run: `npm run build` or equivalent build command
|
||||
- Push the updated branch: `git push origin <branch-name>`
|
||||
|
||||
### 7. 🛑 WAIT — Notify User & Await PR Verification
|
||||
|
||||
**This is a mandatory stop point.** Use `notify_user` with `BlockedOnUser: true`:
|
||||
|
||||
- Inform the user that the PR has been **improved and pushed**, and is **awaiting their verification**
|
||||
- Include:
|
||||
- PR number and URL
|
||||
- Summary of improvements/fixes applied
|
||||
- Build/test status
|
||||
- List of files changed
|
||||
- **DO NOT merge, generate releases, or deploy until the user confirms**
|
||||
|
||||
Wait for the user to respond:
|
||||
|
||||
- **User confirms** → Proceed to step 8
|
||||
- **User requests more changes** → Apply changes, push to the same branch, notify again
|
||||
- **User rejects** → Leave a review comment and stop
|
||||
|
||||
### 8. Thank the Contributor
|
||||
|
||||
- Post a **thank-you comment** on the PR via the GitHub API
|
||||
- The message should:
|
||||
- Thank the author by name/username for their contribution
|
||||
- Briefly mention what the PR accomplishes and any improvements applied
|
||||
- Be friendly, professional, and encouraging
|
||||
- Example: _"Thanks @author for this great contribution! 🎉 The [feature/fix] is now merged and will be part of the next release. We appreciate your effort!"_
|
||||
|
||||
### 9. Merge & Release (only after user confirms PR)
|
||||
|
||||
After the user confirms the PR:
|
||||
|
||||
1. **Merge** the PR into main (local merge with `--no-ff` or via `gh pr merge`)
|
||||
2. **Push** to main: `git push origin main`
|
||||
3. **Clean up** the feature branch: `git branch -d <branch-name>`
|
||||
4. **Update CHANGELOG.md** with the new feature/fix
|
||||
5. Run the `/generate-release` workflow (at `.agents/workflows/generate-release.md`) to bump version, tag, and publish
|
||||
6. Deploy to local VPS: `ssh root@192.168.0.15 "npm install -g omniroute@<VERSION> && pm2 restart omniroute"`
|
||||
@@ -1,105 +0,0 @@
|
||||
---
|
||||
description: How to automatically summarize recent changes and update README and CHANGELOG
|
||||
---
|
||||
|
||||
# Update Documentation Workflow
|
||||
|
||||
Update CHANGELOG.md, README.md, docs/ files, and all multi-language translations whenever features are added or changed.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Summarize recent changes
|
||||
|
||||
Review git log and identify new features, fixes, or changes since the last release tag:
|
||||
|
||||
```bash
|
||||
git log $(git describe --tags --abbrev=0)..HEAD --oneline
|
||||
```
|
||||
|
||||
### 2. Update English CHANGELOG.md
|
||||
|
||||
Add an `[Unreleased]` section (or version header if releasing) with:
|
||||
|
||||
- `### ✨ New Features` — each feature as a bullet point
|
||||
- `### 🐛 Bug Fixes` — if applicable
|
||||
- `### 🧪 Tests` — test count changes
|
||||
- `### 📁 New Files` — table of new files with purpose
|
||||
|
||||
### 3. Update English README.md
|
||||
|
||||
Update the feature tables in these sections:
|
||||
|
||||
- **🧠 Routing & Intelligence** — for routing/model features
|
||||
- **🛡️ Resilience & Security** — for security/resilience features
|
||||
- **📊 Observability & Analytics** — for monitoring features
|
||||
- **☁️ Deploy & Sync** — for deployment features
|
||||
|
||||
### 4. Update docs/ files
|
||||
|
||||
- `docs/FEATURES.md` — update the Settings section description
|
||||
- `docs/API_REFERENCE.md` — add new API routes if any
|
||||
- `docs/ARCHITECTURE.md` — update architecture if structural changes
|
||||
|
||||
### 5. 🌐 Sync Multi-Language Documentation (CRITICAL)
|
||||
|
||||
// turbo-all
|
||||
|
||||
**This step MUST be run after every README or docs update.**
|
||||
|
||||
The project has **30 language versions** of documentation:
|
||||
|
||||
**README files (root directory):**
|
||||
|
||||
```
|
||||
README.md (English - source of truth)
|
||||
README.pt-BR.md README.pt.md README.es.md README.fr.md README.it.md
|
||||
README.de.md README.nl.md README.sv.md README.no.md README.da.md README.fi.md
|
||||
README.ru.md README.uk-UA.md README.bg.md README.sk.md README.pl.md README.ro.md README.hu.md
|
||||
README.ar.md README.he.md README.th.md README.in.md README.id.md README.ms.md README.vi.md
|
||||
README.ja.md README.ko.md README.zh-CN.md README.phi.md
|
||||
```
|
||||
|
||||
**docs/i18n/ directories (29 languages):**
|
||||
|
||||
```
|
||||
docs/i18n/{ar,bg,da,de,es,fi,fr,he,hu,id,in,it,ja,ko,ms,nl,no,phi,pl,pt,pt-BR,ro,ru,sk,sv,th,uk-UA,vi,zh-CN}/
|
||||
Each contains: API_REFERENCE.md, ARCHITECTURE.md, CODEBASE_DOCUMENTATION.md, FEATURES.md, TROUBLESHOOTING.md, USER_GUIDE.md
|
||||
```
|
||||
|
||||
**Sync approach for feature table updates:**
|
||||
|
||||
a. Identify which feature table rows were added to English README.md
|
||||
b. For each translated README, find the corresponding anchor lines:
|
||||
|
||||
- **Routing section:** Find the `💬` (System Prompt) table row — the line before it is always the last routing feature. Insert new routing features before System Prompt.
|
||||
- **Resilience section:** Find the `📊` Rate Limits table row (the one in lines 590-600, NOT the quota tracking one in lines 560-570). Insert new resilience features after it.
|
||||
c. The new feature entries can stay in English for technical features, matching the pattern used in the existing translations.
|
||||
d. Use `sed` or similar tool to batch-insert across all 29 translated READMEs.
|
||||
|
||||
**Verification:**
|
||||
|
||||
```bash
|
||||
# Verify all READMEs have the new features
|
||||
grep -l "NEW_FEATURE_NAME" README.*.md | wc -l
|
||||
# Should return 30 (all language versions)
|
||||
```
|
||||
|
||||
**FEATURES.md sync:**
|
||||
|
||||
```bash
|
||||
# Update Settings description in all docs/i18n/*/FEATURES.md
|
||||
for dir in docs/i18n/*/; do
|
||||
# Update the Settings section description to mention new features
|
||||
# Check FEATURES.md in each directory
|
||||
done
|
||||
```
|
||||
|
||||
### 6. Verify documentation changes
|
||||
|
||||
```bash
|
||||
# Check all modified files
|
||||
git status --short
|
||||
|
||||
# Verify no broken markdown
|
||||
# Optional: run markdownlint if available
|
||||
```
|
||||
341
.agents/workflows/version-bump-ag.md
Normal file
341
.agents/workflows/version-bump-ag.md
Normal file
@@ -0,0 +1,341 @@
|
||||
---
|
||||
description: Bump version, auto-generate CHANGELOG from git commits, update all versioned files, and refresh root + docs/ documentation to reflect the current project state
|
||||
---
|
||||
|
||||
# Version Bump Workflow
|
||||
|
||||
Automatically bump the project version, generate CHANGELOG entries from git history since the last tag, update every file that references the version, and refresh project documentation to reflect the current state.
|
||||
|
||||
> **VERSION RULE: Always use PATCH bumps (3.x.y → 3.x.y+1)**
|
||||
> NEVER use `npm version minor` or `npm version major`.
|
||||
> Always use: `npm version patch --no-git-tag-version`
|
||||
> The threshold rule: when `y` reaches 10, bump to `3.(x+1).0` — e.g. `3.4.10` → `3.5.0`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Determine Version
|
||||
|
||||
### 1. Read current version and last tag
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute
|
||||
CURRENT_VERSION=$(node -p "require('./package.json').version")
|
||||
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
|
||||
CURRENT_BRANCH=$(git branch --show-current)
|
||||
echo "Current version: $CURRENT_VERSION"
|
||||
echo "Last tag: $LAST_TAG"
|
||||
echo "Current branch: $CURRENT_BRANCH"
|
||||
```
|
||||
|
||||
### 2. Calculate new version
|
||||
|
||||
Apply the patch bump rule:
|
||||
|
||||
- If the current patch number is `9`, the new version is `3.(minor+1).0`
|
||||
- Otherwise, increment patch: `3.x.y` → `3.x.(y+1)`
|
||||
|
||||
If the version was ALREADY bumped (e.g. you are on a release branch and package.json already has the new version), **skip the npm version bump** and use the existing version.
|
||||
|
||||
### 3. Bump package.json (if needed)
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
# Only if version hasn't been bumped yet
|
||||
npm version patch --no-git-tag-version
|
||||
```
|
||||
|
||||
Or for threshold (y=10):
|
||||
|
||||
```bash
|
||||
# Manual threshold bump
|
||||
VERSION="3.X.0" # compute manually
|
||||
npm version "$VERSION" --no-git-tag-version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Generate CHANGELOG from Git History
|
||||
|
||||
### 4. Collect commits since last tag
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute
|
||||
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null)
|
||||
echo "=== Commits since $LAST_TAG ==="
|
||||
git log "$LAST_TAG"..HEAD --pretty=format:"%h %s" --no-merges | head -100
|
||||
echo ""
|
||||
echo "=== Merge commits ==="
|
||||
git log "$LAST_TAG"..HEAD --merges --pretty=format:"%h %s" | head -50
|
||||
```
|
||||
|
||||
### 5. Classify commits and generate CHANGELOG section
|
||||
|
||||
Analyze each commit message and classify into categories based on the conventional-commit prefix and content:
|
||||
|
||||
| Category | Patterns |
|
||||
| ------------------- | ------------------------------------------------ |
|
||||
| ✨ New Features | `feat:`, `feat(*):` |
|
||||
| 🐛 Bug Fixes | `fix:`, `fix(*):` |
|
||||
| ⚠️ Breaking Changes | `BREAKING CHANGE`, `!:` suffix |
|
||||
| 🛠️ Maintenance | `chore:`, `refactor:`, `perf:`, `build:` |
|
||||
| 🧪 Tests | `test:`, `tests:` |
|
||||
| 📝 Documentation | `docs:` |
|
||||
| 🔒 Security | `security:`, CVE references, vulnerability fixes |
|
||||
| 🌍 i18n | translation updates, locale changes |
|
||||
|
||||
For each category with entries, create a markdown section with descriptive bullet points. Use the commit messages but rewrite them to be human-readable and descriptive (not raw commit messages).
|
||||
|
||||
**If a commit references a PR number** (e.g. `#880`, `PR #885`), include it in the description.
|
||||
|
||||
### 6. Update CHANGELOG.md
|
||||
|
||||
Replace the `## [Unreleased]` section content with the generated entries, then add the new versioned section:
|
||||
|
||||
```markdown
|
||||
## [Unreleased]
|
||||
|
||||
---
|
||||
|
||||
## [NEW_VERSION] — YYYY-MM-DD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **Feature name:** Description (#PR)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **Fix name:** Description (#PR)
|
||||
|
||||
### 🛠️ Maintenance
|
||||
|
||||
- **Item:** Description
|
||||
|
||||
---
|
||||
|
||||
## [PREVIOUS_VERSION] — YYYY-MM-DD
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
The date must be today's date in `YYYY-MM-DD` format.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Sync Version Across All Files
|
||||
|
||||
### 7. Update workspace package.json files and openapi.yaml
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
|
||||
# Update docs/reference/openapi.yaml version
|
||||
sed -i "s/ version: .*/ version: $VERSION/" docs/reference/openapi.yaml
|
||||
echo "✓ docs/reference/openapi.yaml → $VERSION"
|
||||
|
||||
# Update workspace packages (open-sse, electron)
|
||||
for dir in electron open-sse; do
|
||||
if [ -d "$dir" ] && [ -f "$dir/package.json" ]; then
|
||||
(cd "$dir" && npm version "$VERSION" --no-git-tag-version --allow-same-version > /dev/null)
|
||||
echo "✓ $dir/package.json → $VERSION"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "✓ All workspace packages synced to $VERSION"
|
||||
```
|
||||
|
||||
### 8. Update llm.txt version references
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
OLD_VERSION_PATTERN='[0-9]\+\.[0-9]\+\.[0-9]\+'
|
||||
|
||||
# Update "Current version:" line
|
||||
sed -i "s/\*\*Current version:\*\* $OLD_VERSION_PATTERN/**Current version:** $VERSION/" llm.txt
|
||||
|
||||
# Update "Key Features (vX.Y.Z)" header
|
||||
sed -i "s/## Key Features (v$OLD_VERSION_PATTERN)/## Key Features (v$VERSION)/" llm.txt
|
||||
|
||||
echo "✓ llm.txt → $VERSION"
|
||||
```
|
||||
|
||||
### 9. Regenerate lock file
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute
|
||||
npm install
|
||||
echo "✓ Lock file regenerated"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Update Root Documentation
|
||||
|
||||
Based on the CHANGELOG entries generated in Phase 2, review and update these root-level files if relevant changes warrant updates:
|
||||
|
||||
### 10. Review and update root documentation files
|
||||
|
||||
For each file below, read the current content and determine if the CHANGELOG entries require any updates. Only modify files where substantive changes have occurred:
|
||||
|
||||
| File | When to update |
|
||||
| ----------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `README.md` | New providers, major features, stats changes (test count, provider count), badges, installation instructions, feature table |
|
||||
| `AGENTS.md` | Architecture changes, new modules, new commands, new providers, new services/handlers/executors |
|
||||
| `CONTRIBUTING.md` | Dev workflow changes, new tooling, test infrastructure changes |
|
||||
| `SECURITY.md` | Security fixes, new auth mechanisms, vulnerability disclosures |
|
||||
| `llm.txt` | Provider count changes, new features, architecture changes |
|
||||
|
||||
**Update rules:**
|
||||
|
||||
- **README.md**: Update provider count, test count, feature highlights table, badges if any numbers changed. If a new provider was added, add it to the provider table. If a major feature was added, add it to the features section.
|
||||
- **AGENTS.md**: If new architecture components (handlers, executors, services, DB modules) were added, update the Architecture section. If new commands were added, update the Build/Test table.
|
||||
- **SECURITY.md**: Add new vulnerability fixes or security improvements to the relevant section.
|
||||
- **llm.txt**: Update provider count, feature list, version references.
|
||||
|
||||
### 11. Review and update docs/ files (excluding i18n/)
|
||||
|
||||
For each file in `docs/` (excluding `docs/i18n/`), review if CHANGELOG changes affect it:
|
||||
|
||||
| File | When to update |
|
||||
| --------------------------------------------- | ------------------------------------------------------------------ |
|
||||
| `docs/reference/API_REFERENCE.md` | New API endpoints, changed request/response formats |
|
||||
| `docs/architecture/ARCHITECTURE.md` | New modules, new services, changed data flow |
|
||||
| `docs/architecture/CODEBASE_DOCUMENTATION.md` | New files, architectural changes, module reorganization |
|
||||
| `docs/architecture/REPOSITORY_MAP.md` | New folders / files / one-line descriptions |
|
||||
| `docs/reference/CLI-TOOLS.md` | New CLI tool integrations, config format changes |
|
||||
| `docs/guides/USER_GUIDE.md` | UX changes, new dashboard pages, settings changes |
|
||||
| `docs/reference/PROVIDER_REFERENCE.md` | New providers (regenerate via `scripts/gen-provider-reference.ts`) |
|
||||
| `docs/frameworks/MCP-SERVER.md` | New MCP tools, changed tool signatures, scope changes |
|
||||
| `docs/frameworks/A2A-SERVER.md` | New A2A skills, protocol changes |
|
||||
| `docs/frameworks/AGENT_PROTOCOLS_GUIDE.md` | New external agent protocols supported |
|
||||
| `docs/frameworks/CLOUD_AGENT.md` | Cloud agent additions (codex-cloud, devin, jules) or API changes |
|
||||
| `docs/architecture/AUTHZ_GUIDE.md` | New route classifications, policy changes |
|
||||
| `docs/security/GUARDRAILS.md` | New guardrails registered, priority/order changes |
|
||||
| `docs/security/COMPLIANCE.md` | Audit log / retention / no-log policy changes |
|
||||
| `docs/frameworks/SKILLS.md` | Skill framework / registry / built-in skill changes |
|
||||
| `docs/frameworks/MEMORY.md` | Memory pipeline / extraction / injection / Qdrant changes |
|
||||
| `docs/frameworks/EVALS.md` | Evaluation framework changes, new evaluators |
|
||||
| `docs/frameworks/WEBHOOKS.md` | New webhook events, payload schema changes |
|
||||
| `docs/routing/REASONING_REPLAY.md` | Reasoning capture/replay pipeline changes |
|
||||
| `docs/routing/AUTO-COMBO.md` | Routing changes, new strategies, scoring weight changes |
|
||||
| `docs/architecture/RESILIENCE_GUIDE.md` | Circuit breaker / cooldown / lockout behavior changes |
|
||||
| `docs/security/STEALTH_GUIDE.md` | TLS / CLI fingerprint changes |
|
||||
| `docs/ops/TUNNELS_GUIDE.md` | Cloudflare tunnel feature changes |
|
||||
| `docs/guides/ELECTRON_GUIDE.md` | Electron build / signing / packaging changes |
|
||||
| `docs/guides/TROUBLESHOOTING.md` | New known issues, resolved problems |
|
||||
| `docs/ops/RELEASE_CHECKLIST.md` | Process changes |
|
||||
| `docs/ops/COVERAGE_PLAN.md` | Coverage gate adjustments, target metrics |
|
||||
| `docs/reference/openapi.yaml` | Already updated in step 7 |
|
||||
|
||||
**Only update files where the CHANGELOG entries directly affect the documented content.** Do NOT update files just to bump a version number — only when the documented behavior, features, or architecture has actually changed.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Verify
|
||||
|
||||
### 12. Run lint check
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### 13. Run tests
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute
|
||||
npm test
|
||||
```
|
||||
|
||||
### 14. Verify version sync across all files
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
echo "Expected version: $VERSION"
|
||||
echo ""
|
||||
|
||||
echo "--- package.json ---"
|
||||
grep '"version"' package.json | head -1
|
||||
|
||||
echo "--- open-sse/package.json ---"
|
||||
grep '"version"' open-sse/package.json | head -1
|
||||
|
||||
echo "--- electron/package.json ---"
|
||||
[ -f electron/package.json ] && grep '"version"' electron/package.json | head -1
|
||||
|
||||
echo "--- docs/reference/openapi.yaml ---"
|
||||
grep " version:" docs/reference/openapi.yaml | head -1
|
||||
|
||||
echo "--- llm.txt ---"
|
||||
grep "Current version:" llm.txt
|
||||
|
||||
echo "--- CHANGELOG.md (first versioned entry) ---"
|
||||
grep "^## \[" CHANGELOG.md | head -2
|
||||
```
|
||||
|
||||
### 15. 🛑 STOP — Present Summary to User
|
||||
|
||||
**STOP** and present a summary to the user including:
|
||||
|
||||
- Old version → New version
|
||||
- CHANGELOG entries generated
|
||||
- Files modified
|
||||
- Test results
|
||||
- Any documentation updates made
|
||||
|
||||
**Wait for the user to confirm before committing.**
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Commit (only after user approval)
|
||||
|
||||
### 16. Stage and commit
|
||||
|
||||
// turbo-all
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute
|
||||
git add -A
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
git commit -m "chore(release): bump to v$VERSION — changelog, docs, version sync"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- This workflow does **NOT** create tags, releases, or deploy. Use `/generate-release` for the full release cycle after this.
|
||||
- This workflow does **NOT** update `docs/i18n/` translations. Translation updates are handled manually or via release tooling — there is no `/update-i18n` workflow shipped in this repo.
|
||||
- The CHANGELOG generation is based on git commits since the last tag. If there are no new commits, the workflow should inform the user and stop.
|
||||
- Always verify the generated CHANGELOG entries make sense — raw commit messages may need rewriting for clarity.
|
||||
- If the version was already bumped (e.g. you're on a `release/vX.Y.Z` branch), skip the `npm version` step and use the existing version.
|
||||
|
||||
## Version Touchpoints Checklist
|
||||
|
||||
| File | Field/Pattern |
|
||||
| ----------------------------- | ----------------------------------------------------------- |
|
||||
| `package.json` | `"version": "X.Y.Z"` |
|
||||
| `open-sse/package.json` | `"version": "X.Y.Z"` |
|
||||
| `electron/package.json` | `"version": "X.Y.Z"` |
|
||||
| `docs/reference/openapi.yaml` | `version: X.Y.Z` |
|
||||
| `llm.txt` | `**Current version:** X.Y.Z` and `## Key Features (vX.Y.Z)` |
|
||||
| `CHANGELOG.md` | `## [X.Y.Z] — YYYY-MM-DD` |
|
||||
51
.claude/commands/capture-release-evidences-cc.md
Normal file
51
.claude/commands/capture-release-evidences-cc.md
Normal file
@@ -0,0 +1,51 @@
|
||||
---
|
||||
description: Automatically run a browser-automation agent to visually validate all new UI features from the current release and capture evidence WebP recordings of the changes.
|
||||
---
|
||||
|
||||
# Capture Release Evidences Workflow
|
||||
|
||||
Use this workflow to automatically drive a browser-automation agent to explore the newly deployed or locally running application and record evidence of the UI changes introduced in the latest release.
|
||||
|
||||
> **Tool mapping note (v3.8):** This workflow references a `browser_subagent` tool that was specific to an earlier agent runtime. In Claude Code, substitute with the available browser MCP tools (e.g. `mcp__claude-in-chrome__*`) for navigation/screenshots, plus the `Write` tool for saving artifacts. The high-level steps below remain the same regardless of which browser-automation surface is used.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- OmniRoute must be actively running and accessible (e.g. locally at `http://localhost:20128` or on the Local VPS at `http://192.168.0.15:20128`).
|
||||
- The user must provide the target URL to be tested, or default to `http://192.168.0.15:20128`.
|
||||
|
||||
## Workflow Steps
|
||||
|
||||
### 1. Identify Target Features
|
||||
|
||||
Review the `CHANGELOG.md` for the latest version to map out the new UI elements. For example:
|
||||
|
||||
- **CLI Tools Settings**
|
||||
- **New Provider/Model Listings (e.g., Gemini 3.1, Qoder PAT)**
|
||||
- **New Feature Modals**
|
||||
|
||||
### 2. Run the Browser Subagent
|
||||
|
||||
For each identified feature, invoke the `browser_subagent` using the `default_api:browser_subagent` tool.
|
||||
**Important Task Guidelines for the Subagent:**
|
||||
|
||||
- `TaskName`: Give it a clear name like "Validate CLIProxyAPI Tool Tab".
|
||||
- `TaskSummary`: "Navigate to the CLI Tools tab and verify the new Integration settings."
|
||||
- `Task`: Provide unambiguous instructions for the subagent, such as: "Navigate to http://192.168.0.15:20128/dashboard. Click on the 'Settings' or 'CLI Tools' nav link. Scroll down to find the CLIProxyAPI integration card. Hover over it to trigger UI state. Verify the components render correctly and exit."
|
||||
- `RecordingName`: Ensure it describes the feature (e.g. `v3_4_5_cli_proxy_api`). This is required and strictly automatically saved as a WebP artifacts video by the system.
|
||||
|
||||
_(Note: The `browser_subagent` automatically creates a WebP recording named by the `RecordingName` parameter. No additional tools for screenshots are needed.)_
|
||||
|
||||
### 3. Generate Report Artifact
|
||||
|
||||
After the `browser_subagent` finishes its sessions, generate a final Markdown artifact (using `Write` and `IsArtifact=true`) to present the recordings inline to the user using the `` syntax.
|
||||
|
||||
### Example Invocation
|
||||
|
||||
\```json
|
||||
{
|
||||
"TaskName": "Validating Qoder PAT Configuration UI",
|
||||
"TaskSummary": "Validates the Qoder provider configuration modal",
|
||||
"Task": "Go to http://192.168.0.15:20128/dashboard. Click on the 'Providers' tab. Find 'Qoder' in the list. Click 'Add Token' or 'Configure'. Type 'test_token' and submit. Return when done.",
|
||||
"RecordingName": "qoder_pat_ui_validation"
|
||||
}
|
||||
\```
|
||||
39
.claude/commands/deploy-vps-akamai-cc.md
Normal file
39
.claude/commands/deploy-vps-akamai-cc.md
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
description: Deploy the latest OmniRoute code to the Akamai VPS (69.164.221.35)
|
||||
---
|
||||
|
||||
# Deploy to Akamai VPS Workflow
|
||||
|
||||
Deploy OmniRoute to the Akamai VPS using `npm pack + scp` + PM2.
|
||||
|
||||
**Akamai VPS:** `69.164.221.35`
|
||||
**Process manager:** PM2 (`omniroute`)
|
||||
**Port:** `20128`
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Build + pack locally
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute && rm -f omniroute-*.tgz && rm -rf .next/cache app/.next/cache && npm run build:cli && rm -rf app/logs app/coverage app/.git app/.app-build-backup* && npm pack --ignore-scripts
|
||||
```
|
||||
|
||||
### 2. Copy to Akamai VPS and install
|
||||
|
||||
// turbo-all
|
||||
|
||||
```bash
|
||||
scp omniroute-*.tgz root@69.164.221.35:/tmp/
|
||||
```
|
||||
|
||||
```bash
|
||||
ssh root@69.164.221.35 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && pm2 delete omniroute 2>/dev/null; pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Akamai done'"
|
||||
```
|
||||
|
||||
### 3. Verify the deployment
|
||||
|
||||
```bash
|
||||
curl -s -o /dev/null -w 'AKAMAI HTTP %{http_code}\n' http://69.164.221.35:20128/
|
||||
```
|
||||
49
.claude/commands/deploy-vps-both-cc.md
Normal file
49
.claude/commands/deploy-vps-both-cc.md
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
description: Deploy the latest OmniRoute code to BOTH the Akamai VPS and the Local VPS
|
||||
---
|
||||
|
||||
# Deploy to VPS (Both) Workflow
|
||||
|
||||
Deploy OmniRoute to the production VPSs using `npm pack + scp` + PM2.
|
||||
|
||||
**Akamai VPS:** `69.164.221.35`
|
||||
**Local VPS:** `192.168.0.15`
|
||||
**Process manager:** PM2 (`omniroute`)
|
||||
**Port:** `20128`
|
||||
**PM2 entry:** `/usr/lib/node_modules/omniroute/app/server.js`
|
||||
|
||||
> [!IMPORTANT]
|
||||
> The npm registry rejects packages > 100MB, so deployment uses **npm pack + scp**.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Build + pack locally
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute && rm -f omniroute-*.tgz && rm -rf .next/cache app/.next/cache && npm run build:cli && rm -rf app/logs app/coverage app/.git app/.app-build-backup* && npm pack --ignore-scripts
|
||||
```
|
||||
|
||||
### 2. Copy to both VPS and install
|
||||
|
||||
// turbo-all
|
||||
|
||||
```bash
|
||||
scp omniroute-*.tgz root@69.164.221.35:/tmp/ && scp omniroute-*.tgz root@192.168.0.15:/tmp/
|
||||
```
|
||||
|
||||
```bash
|
||||
ssh root@69.164.221.35 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && pm2 delete omniroute 2>/dev/null; pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Akamai done'"
|
||||
```
|
||||
|
||||
```bash
|
||||
ssh root@192.168.0.15 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && pm2 delete omniroute 2>/dev/null; pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Local done'"
|
||||
```
|
||||
|
||||
### 3. Verify the deployment
|
||||
|
||||
```bash
|
||||
curl -s -o /dev/null -w 'AKAMAI HTTP %{http_code}\n' http://69.164.221.35:20128/
|
||||
curl -s -o /dev/null -w 'LOCAL HTTP %{http_code}\n' http://192.168.0.15:20128/
|
||||
```
|
||||
39
.claude/commands/deploy-vps-local-cc.md
Normal file
39
.claude/commands/deploy-vps-local-cc.md
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
description: Deploy the latest OmniRoute code to the Local VPS (192.168.0.15)
|
||||
---
|
||||
|
||||
# Deploy to Local VPS Workflow
|
||||
|
||||
Deploy OmniRoute to the Local VPS using `npm pack + scp` + PM2.
|
||||
|
||||
**Local VPS:** `192.168.0.15`
|
||||
**Process manager:** PM2 (`omniroute`)
|
||||
**Port:** `20128`
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Build + pack locally
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute && rm -f omniroute-*.tgz && rm -rf .next/cache app/.next/cache && npm run build:cli && rm -rf app/logs app/coverage app/.git app/.app-build-backup* && npm pack --ignore-scripts
|
||||
```
|
||||
|
||||
### 2. Copy to Local VPS and install
|
||||
|
||||
// turbo-all
|
||||
|
||||
```bash
|
||||
scp omniroute-*.tgz root@192.168.0.15:/tmp/
|
||||
```
|
||||
|
||||
```bash
|
||||
ssh root@192.168.0.15 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && pm2 delete omniroute 2>/dev/null; pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Local done'"
|
||||
```
|
||||
|
||||
### 3. Verify the deployment
|
||||
|
||||
```bash
|
||||
curl -s -o /dev/null -w 'LOCAL HTTP %{http_code}\n' http://192.168.0.15:20128/
|
||||
```
|
||||
362
.claude/commands/generate-release-cc.md
Normal file
362
.claude/commands/generate-release-cc.md
Normal file
@@ -0,0 +1,362 @@
|
||||
---
|
||||
description: Create a new release, bump version up to the .10 patch threshold, update changelog, and manage Pull Requests
|
||||
---
|
||||
|
||||
# Generate Release Workflow
|
||||
|
||||
Bump version, finalize CHANGELOG, commit, open a **PR to main** and wait for user confirmation before tagging, publishing, and deploying.
|
||||
|
||||
> **VERSION RULE: Always use PATCH bumps (3.x.y → 3.x.y+1)**
|
||||
> NEVER use `npm version minor` or `npm version major`.
|
||||
> Always use: `npm version patch --no-git-tag-version`
|
||||
> The threshold rule: when `y` reaches 10, bump to `3.(x+1).0` — e.g. `3.8.10` → `3.9.0`.
|
||||
|
||||
> **🔴 SINGLE BRANCH RULE**: The `release/vX.Y.Z` branch is the **ONLY** development branch for the entire release cycle. ALL work — bug fixes, feature implementations, PR integrations, issue resolutions — MUST be committed directly on this branch. Never create separate `fix/`, `feat/`, or topic branches. When running `/resolve-issues`, `/implement-features`, or `/review-prs`, always work on the current release branch.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Two-Phase Flow
|
||||
|
||||
```
|
||||
Phase 1 (automated): bump → docs → i18n → commit → push → open PR
|
||||
↕ 🛑 STOP: Notify user, wait for PR confirmation
|
||||
Phase 2 (post-merge): tag → publish → GitHub release → Docker → deploy
|
||||
```
|
||||
|
||||
**NEVER push directly to main or create tags before the user confirms the PR.**
|
||||
|
||||
---
|
||||
|
||||
## Phase 0: Security Verification (MANDATORY)
|
||||
|
||||
Before creating the release, you must ensure the codebase and supply chain are secure and free of known vulnerabilities.
|
||||
|
||||
1. **Run Local Dependencies Audit:**
|
||||
|
||||
```bash
|
||||
npm audit
|
||||
```
|
||||
|
||||
_Fix any `high` or `critical` vulnerabilities identified._
|
||||
|
||||
2. **Check GitHub CodeQL & Dependabot Alerts:**
|
||||
Navigate to the repository's **Security** tab on GitHub, or use the project's `vulnerability-scanner` skill to analyze active alerts. Ensure all static analysis findings (e.g., prototype pollution, insecure randomness, ReDoS, shell injections) are addressed and logically committed on a target branch.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Pre-Merge
|
||||
|
||||
### 1. Create release branch
|
||||
|
||||
```bash
|
||||
git checkout -b release/v3.x.y
|
||||
```
|
||||
|
||||
### 2. Determine and sync version
|
||||
|
||||
Check current version in `package.json`:
|
||||
|
||||
```bash
|
||||
grep '"version"' package.json
|
||||
```
|
||||
|
||||
> **🔴 BRANCH-VERSION PARITY RULE**: The logical version in `package.json` MUST exactly match the release branch name. For example, if you are on `release/v3.7.0`, the version in `package.json` MUST be `3.7.0`.
|
||||
>
|
||||
> - If this is the FIRST time generating a release for a new minor/major branch (e.g., bumping from 3.6.9 to 3.7.0), you MUST ensure the version is bumped to match the new branch logic.
|
||||
> - If you are just bumping a patch on the current branch (e.g., 3.6.9 to 3.6.10), use:
|
||||
> `npm version patch --no-git-tag-version`
|
||||
|
||||
> **⚠️ ATOMIC COMMIT RULE — Version bump MUST happen before committing feature files.**
|
||||
>
|
||||
> **CORRECT order:**
|
||||
>
|
||||
> 1. `npm version patch --no-git-tag-version` ← bump first
|
||||
> 2. implement features / fix bugs
|
||||
> 3. `git add -A && git commit -m "chore(release): v3.x.y — all changes in ONE commit"`
|
||||
>
|
||||
> **OR if features are already staged:**
|
||||
>
|
||||
> 1. implement features (do NOT commit yet)
|
||||
> 2. `npm version patch --no-git-tag-version` ← bump before committing
|
||||
> 3. `git add -A && git commit -m "chore(release): v3.x.y — all changes in ONE commit"`
|
||||
>
|
||||
> **NEVER do this (creates version mismatch in git history):**
|
||||
>
|
||||
> - ~~commit features → then bump version → commit package.json separately~~
|
||||
>
|
||||
> This ensures that `git show v3.x.y` always contains both code changes and the version bump together.
|
||||
> The GitHub release tag will point to a commit that includes ALL changes for that version.
|
||||
|
||||
### 3. Regenerate lock file (REQUIRED after version bump)
|
||||
|
||||
**Mandatory** — skipping causes `@swc/helpers` lock mismatch and CI failures:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 4. Finalize CHANGELOG.md
|
||||
|
||||
> **🔴 NO MIXUPS RULE**: Ensure you do NOT mix the backlog of the previous version with the new one. The new version section must ONLY contain the features and fixes for the current release.
|
||||
|
||||
Replace the `[Unreleased]` header with the new version and date.
|
||||
Keep an empty `## [Unreleased]` section above it, separated by a horizontal rule (`---`).
|
||||
|
||||
```markdown
|
||||
## [Unreleased]
|
||||
|
||||
---
|
||||
|
||||
## [3.7.0] — 2026-04-19
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- ...
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- ...
|
||||
|
||||
---
|
||||
|
||||
## [3.6.9] — 2026-04-19
|
||||
```
|
||||
|
||||
### 5. Update openapi.yaml version ⚠️ MANDATORY
|
||||
|
||||
> **CI will fail** if `docs/reference/openapi.yaml` version ≠ `package.json` version (`check:docs-sync` enforces this).
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
sed -i "s/ version: .*/ version: $VERSION/" docs/reference/openapi.yaml
|
||||
echo "✓ openapi.yaml → $VERSION"
|
||||
|
||||
for dir in electron open-sse; do
|
||||
if [ -d "$dir" ] && [ -f "$dir/package.json" ]; then
|
||||
(cd "$dir" && npm version "$VERSION" --no-git-tag-version --allow-same-version > /dev/null)
|
||||
echo "✓ $dir/package.json → $VERSION"
|
||||
fi
|
||||
done
|
||||
# Re-run install to assert the workspace lockfile is updated
|
||||
npm install
|
||||
```
|
||||
|
||||
### 6. Update README.md and i18n docs
|
||||
|
||||
Manually perform these documentation updates (there is no `/update-docs` slash command — it was deprecated in v3.8):
|
||||
|
||||
- Update feature table rows and "What's new in vX.Y.Z" section in `README.md`
|
||||
- Sync feature changes to all 40 language `docs/i18n/*/README.md` files (use the same row edits across each translated README)
|
||||
- Update the relevant `docs/<AREA>.md` if architecture or counts changed (e.g. `docs/frameworks/MCP-SERVER.md` when MCP tools change)
|
||||
- Re-run `npm run check:docs-sync` and `npm run check:docs-all` to catch drift
|
||||
|
||||
### 7. Run tests
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
All tests must pass before creating the PR.
|
||||
|
||||
### 8. Stage, commit, and push
|
||||
|
||||
// turbo-all
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "chore(release): v3.x.y — summary of changes"
|
||||
git push origin release/v3.x.y
|
||||
```
|
||||
|
||||
### 9. Open PR to main
|
||||
|
||||
### 9. Open PR to main
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
|
||||
# Extract the exact changelog entry for this version from the root CHANGELOG.md
|
||||
awk "/^## \\[$VERSION\\]/{flag=1; print; next} /^---/{if(flag) {flag=0; exit}} flag" CHANGELOG.md > /tmp/changelog_body.txt
|
||||
|
||||
# Append test status and next steps
|
||||
echo "" >> /tmp/changelog_body.txt
|
||||
echo "### Tests" >> /tmp/changelog_body.txt
|
||||
echo "- All tests pass" >> /tmp/changelog_body.txt
|
||||
echo "" >> /tmp/changelog_body.txt
|
||||
echo "### ⚠️ After merging: run Phase 2 steps to tag, publish, and deploy." >> /tmp/changelog_body.txt
|
||||
|
||||
gh pr create \
|
||||
--repo diegosouzapw/OmniRoute \
|
||||
--base main \
|
||||
--head release/v$VERSION \
|
||||
--title "Release v$VERSION" \
|
||||
--body-file /tmp/changelog_body.txt
|
||||
```
|
||||
|
||||
### 10. 🛑 STOP — Notify User & Await PR Confirmation
|
||||
|
||||
**This is a mandatory stop point.** Present the report in the final response and stop. Do not continue to the next phase until the user explicitly approves.
|
||||
|
||||
Inform the user:
|
||||
|
||||
- PR URL
|
||||
- Summary of changes
|
||||
- Test results
|
||||
- List of files changed
|
||||
|
||||
**DO NOT proceed to Phase 2 until the user confirms the PR looks good and merges it.**
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Post-Merge Validation (Local VPS)
|
||||
|
||||
> Run these steps only AFTER the user has merged the PR into `main` and all CI jobs have passed.
|
||||
|
||||
### 11. Deploy to Local VPS for Final Validation (MANDATORY)
|
||||
|
||||
Before cutting the official git tag and publishing to the world, deploy the `main` branch to the Local VPS for a final homologation test.
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
git pull origin main
|
||||
|
||||
# Build and pack locally
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute && rm -f omniroute-*.tgz && rm -rf .next/cache app/.next/cache && npm run build:cli && rm -rf app/logs app/coverage app/.git app/.app-build-backup* && npm pack --ignore-scripts
|
||||
|
||||
# Deploy to LOCAL VPS (192.168.0.15)
|
||||
scp omniroute-*.tgz root@192.168.0.15:/tmp/
|
||||
ssh root@192.168.0.15 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && pm2 delete omniroute 2>/dev/null; pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Local done'"
|
||||
|
||||
# Verify
|
||||
curl -s -o /dev/null -w "LOCAL: HTTP %{http_code}\n" http://192.168.0.15:20128/
|
||||
```
|
||||
|
||||
### 12. 🛑 STOP — Notify User & Await Final OK
|
||||
|
||||
**This is a mandatory stop point.**
|
||||
Inform the user that the `main` branch is now running on the Local VPS.
|
||||
Wait for the user to manually test and give the **OK**.
|
||||
**DO NOT proceed to Phase 3 until the user confirms the local deploy is stable.**
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Official Launch
|
||||
|
||||
> Run these steps only AFTER the user gives the final OK from the Phase 2 local validation.
|
||||
|
||||
### 13. Create Git Tag and GitHub Release (MANDATORY)
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
git pull origin main
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
|
||||
# Extracts the changelog section for this version
|
||||
NOTES=$(awk "/^## \\[$VERSION\\]/{flag=1; next} /^---/{if(flag) {flag=0; exit}} flag" CHANGELOG.md | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
|
||||
if [ -z "$NOTES" ]; then NOTES="OmniRoute v$VERSION Release"; fi
|
||||
|
||||
git tag -a "v$VERSION" -m "Release v$VERSION"
|
||||
git push origin "v$VERSION"
|
||||
gh release create "v$VERSION" --repo diegosouzapw/OmniRoute --title "v$VERSION" --notes "$NOTES" --target main || gh release edit "v$VERSION" --repo diegosouzapw/OmniRoute --title "v$VERSION" --notes "$NOTES"
|
||||
```
|
||||
|
||||
### 14. 🐳 Trigger Docker Hub build (MANDATORY — keep npm and Docker in sync)
|
||||
|
||||
> **CRITICAL**: Docker Hub and npm MUST always publish the same version.
|
||||
> The Docker image is built automatically via GitHub Actions when a new tag is pushed.
|
||||
> After pushing the tag in step 13, **verify the workflow runs**:
|
||||
|
||||
```bash
|
||||
# Verify the Docker workflow triggered
|
||||
gh run list --repo diegosouzapw/OmniRoute --workflow docker-publish.yml --limit 3
|
||||
|
||||
# Wait for the Docker build to complete (usually 5–10 min)
|
||||
gh run watch --repo diegosouzapw/OmniRoute
|
||||
```
|
||||
|
||||
### 15. Publish to NPM (Optional/Automated)
|
||||
|
||||
Normally handled by CI, but if manual publish is required:
|
||||
|
||||
```bash
|
||||
npm publish
|
||||
```
|
||||
|
||||
### 16. Deploy to AKAMAI VPS (Production)
|
||||
|
||||
Now that the release is officially cut, deploy it to the Akamai VPS.
|
||||
|
||||
```bash
|
||||
# Deploy to AKAMAI VPS (69.164.221.35)
|
||||
scp omniroute-*.tgz root@69.164.221.35:/tmp/
|
||||
ssh root@69.164.221.35 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && pm2 delete omniroute 2>/dev/null; pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Akamai done'"
|
||||
|
||||
# Verify
|
||||
curl -s -o /dev/null -w "AKAMAI: HTTP %{http_code}\n" http://69.164.221.35:20128/
|
||||
```
|
||||
|
||||
## Phase 4: Release Monitoring & Artifact Validation
|
||||
|
||||
> After triggering the official release, actively monitor the CI pipelines until all artifacts are successfully generated. If any pipeline fails, stop and apply the necessary corrections before continuing.
|
||||
|
||||
### 18. Monitor CI Pipelines
|
||||
|
||||
Wait for and verify the successful completion of the following automated jobs:
|
||||
|
||||
1. **Docker Hub Publish**
|
||||
2. **Electron Build**
|
||||
3. **NPM Registry Publish** (Check with `npm info omniroute version`)
|
||||
|
||||
```bash
|
||||
# Monitor Docker Hub workflow
|
||||
gh run list --repo diegosouzapw/OmniRoute --workflow docker-publish.yml --limit 1
|
||||
gh run watch <RUN_ID>
|
||||
|
||||
# Monitor Electron build
|
||||
gh run list --repo diegosouzapw/OmniRoute --workflow electron-release.yml --limit 1
|
||||
gh run watch <RUN_ID>
|
||||
|
||||
# Verify NPM version
|
||||
npm info omniroute version
|
||||
```
|
||||
|
||||
### 19. Handle Failures (If Any)
|
||||
|
||||
If a workflow fails:
|
||||
|
||||
- Use `gh run view <RUN_ID> --log-failed` to identify the error.
|
||||
- Apply the fix on the `main` branch.
|
||||
- If necessary, re-trigger the workflow using `gh workflow run <workflow_name.yml> --repo diegosouzapw/OmniRoute --ref v3.x.y`
|
||||
|
||||
### 20. Preserve release branch
|
||||
|
||||
```bash
|
||||
# Branch is kept for historical purposes. Do not delete.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Ensure CHANGELOG, README and `docs/*` are current BEFORE this workflow — run `npm run check:docs-all` and `/version-bump` first (there is no `/update-docs` slash command anymore)
|
||||
- The `prepublishOnly` script runs `npm run build:cli` automatically during `npm publish`
|
||||
- After npm publish, verify with `npm info omniroute version`
|
||||
- Lock file sync errors are caused by skipping `npm install` after version bump
|
||||
- Use `gh auth switch -u diegosouzapw` if git push fails with wrong account
|
||||
|
||||
## Known CI Pitfalls
|
||||
|
||||
| CI failure | Cause | Fix |
|
||||
| ------------------------------------------------------------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------- |
|
||||
| `[docs-sync] FAIL - OpenAPI version differs from package.json` | Skipped step 5 — `docs/reference/openapi.yaml` version not updated | Run step 5 (`sed -i ...`) and commit |
|
||||
| `[docs-sync] FAIL - CHANGELOG.md first section must be "## [Unreleased]"` | `## [Unreleased]` missing or not at top of CHANGELOG | Add `## [Unreleased]\n\n---\n` before the first versioned `## [x.y.z]` |
|
||||
| Electron Linux `.deb` build fails (`FpmTarget` error) | `fpm` Ruby gem not installed on `ubuntu-latest` runner | Already fixed in `electron-release.yml` (`gem install fpm` step) |
|
||||
| Docker Hub `502 error writing layer blob` | Transient Docker Hub network error during ARM64 push | Re-run the Docker publish workflow; no code change needed |
|
||||
706
.claude/commands/implement-features-cc.md
Normal file
706
.claude/commands/implement-features-cc.md
Normal file
@@ -0,0 +1,706 @@
|
||||
---
|
||||
description: Analyze open feature request issues, implement viable ones on dedicated branches, and respond to authors
|
||||
---
|
||||
|
||||
# /implement-features — Feature Request Harvest, Research & Implementation Workflow
|
||||
|
||||
## Overview
|
||||
|
||||
A **5-phase** workflow that systematically harvests feature requests from GitHub issues, creates structured idea files, researches solutions across the internet and Git repositories, presents a consolidated report for user approval, then generates detailed implementation plans and executes them.
|
||||
|
||||
**Output directory structure:**
|
||||
|
||||
```
|
||||
_ideia/
|
||||
├── viable/ # Features approved for implementation
|
||||
│ ├── need_details/ # ❓ Good idea but waiting for author clarification (issues stay OPEN)
|
||||
│ │ └── 1015-warp-terminal-mitm.md
|
||||
│ ├── 1046-native-playground.md # ✅ Ready — researched and planned
|
||||
│ └── 1046-native-playground.requirements.md
|
||||
├── defer/ # ⏭️ Good ideas deferred for future cycles (issues CLOSED)
|
||||
│ └── 1041-smart-auto-combos.md
|
||||
└── notfit/ # ❌ Out of scope / already exists (issues CLOSED)
|
||||
└── 945-telegram-integration.md
|
||||
|
||||
_tasks/features-vX.Y.Z/ # Implementation plans (per-release)
|
||||
└── 1046-native-playground.plan.md
|
||||
```
|
||||
|
||||
> **LIFECYCLE RULE:** `viable/` files are **DELETED** once the feature is implemented — they are not moved. Only unimplemented features live in `viable/` (or `viable/need_details/`). Files in `defer/` and `notfit/` remain as permanent reference.
|
||||
|
||||
> **BRANCH RULE**: All implementation work MUST happen on the current `release/vX.Y.Z` branch. Never create separate `feat/` branches. If no release branch exists yet, create one first using `/generate-release` Phase 1 steps 1–5.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Harvest: Collect & Catalog Feature Ideas
|
||||
|
||||
### 1.1 Identify the Repository
|
||||
|
||||
// turbo
|
||||
|
||||
- Run: `git -C <project_root> remote get-url origin` to extract owner/repo.
|
||||
|
||||
### 1.2 Ensure Release Branch Exists
|
||||
|
||||
// turbo
|
||||
|
||||
Before doing any work, ensure you are on the current release branch:
|
||||
|
||||
```bash
|
||||
# Check current branch
|
||||
git branch --show-current
|
||||
|
||||
# If on main, determine next version and create the release branch
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
NEXT=$(node -p "const [a,b,c]=('$VERSION').split('.').map(Number); c>=9?a+'.'+(b+1)+'.0':a+'.'+b+'.'+(c+1)")
|
||||
git checkout -b release/v$NEXT
|
||||
npm version patch --no-git-tag-version
|
||||
npm install
|
||||
```
|
||||
|
||||
If already on a `release/vX.Y.Z` branch, continue working there.
|
||||
|
||||
### 1.3 Fetch ALL Open Feature Requests
|
||||
|
||||
// turbo-all
|
||||
|
||||
**⚠️ CRITICAL**: The JSON output of `gh issue list` can be truncated by the tool, silently hiding issues. You MUST use the two-step approach below.
|
||||
|
||||
**Step 1 — Get Issue numbers only** (small output, never truncated):
|
||||
|
||||
```bash
|
||||
# Fetch issues with feature/enhancement labels
|
||||
gh issue list --repo <owner>/<repo> --state open -l "enhancement" --limit 500 --json number --jq '.[].number'
|
||||
|
||||
# Also check for [Feature] in title (common pattern when no labels are set)
|
||||
gh issue list --repo <owner>/<repo> --state open --limit 500 --json number,title --jq '.[] | select(.title | test("\\[Feature\\]|\\[feature\\]|feature request"; "i")) | .number'
|
||||
```
|
||||
|
||||
- Merge both lists, deduplicate. Count and confirm the total.
|
||||
|
||||
**Step 2 — Fetch full metadata for each Issue** (one call per issue):
|
||||
|
||||
```bash
|
||||
gh issue view <NUMBER> --repo <owner>/<repo> --json number,title,labels,body,comments,createdAt,author,assignees
|
||||
```
|
||||
|
||||
- Read the **entire body** — including description, use cases, screenshots, mockups, and any embedded images.
|
||||
- Read **ALL comments** — community discussion, agreements, restrictions, owner responses, and linked PRs.
|
||||
- **Images**: If the body or comments contain image URLs (`` or `https://...png/jpg/gif`), note them — they may contain UI mockups, wireframes, or architecture diagrams that are essential to understanding the request.
|
||||
- You may batch these into parallel calls (up to 4 at a time).
|
||||
- Sort by oldest first (FIFO).
|
||||
|
||||
### 1.4 Create Idea Files (initially in `_ideia/` root)
|
||||
|
||||
For each feature request, create a structured idea file in `<project_root>/_ideia/`:
|
||||
|
||||
**Filename convention**: `<NUMBER>-<kebab-case-short-title>.md`
|
||||
Example: `1046-native-playground.md`, `1041-smart-auto-combos.md`
|
||||
|
||||
#### 1.4a — If the idea file does NOT exist yet, create it:
|
||||
|
||||
```markdown
|
||||
# Feature: <Title from Issue>
|
||||
|
||||
> GitHub Issue: #<NUMBER> — opened by @<author> on <date>
|
||||
> Status: 📋 Cataloged | Priority: TBD
|
||||
|
||||
## 📝 Original Request
|
||||
|
||||
<Paste the FULL issue body here, preserving all formatting, images, and code blocks>
|
||||
|
||||
## 💬 Community Discussion
|
||||
|
||||
<Summarize ALL comments chronologically, noting who said what and any decisions or objections raised>
|
||||
|
||||
### Participants
|
||||
|
||||
- @<author> — Original requester
|
||||
- @<commenter1> — <brief role/opinion>
|
||||
- ...
|
||||
|
||||
### Key Points
|
||||
|
||||
- <bullet list of the most important discussion points>
|
||||
- <agreements reached>
|
||||
- <objections raised>
|
||||
|
||||
## 🎯 Refined Feature Description
|
||||
|
||||
<YOUR interpretation and enrichment of the feature request. Expand on what was asked, fill in logical gaps, provide concrete examples of how it would work. This section should be MORE detailed and clearer than the original request.>
|
||||
|
||||
### What it solves
|
||||
|
||||
- <problem 1>
|
||||
- <problem 2>
|
||||
|
||||
### How it should work (high level)
|
||||
|
||||
1. <step 1>
|
||||
2. <step 2>
|
||||
3. ...
|
||||
|
||||
### Affected areas
|
||||
|
||||
- <list of codebase areas, modules, files likely affected>
|
||||
|
||||
## 📎 Attachments & References
|
||||
|
||||
- <any image URLs, mockup links, or external references from the issue>
|
||||
|
||||
## 🔗 Related Ideas
|
||||
|
||||
- <links to related \_ideia/ files if any overlap found>
|
||||
```
|
||||
|
||||
#### 1.4b — If the idea file ALREADY exists, update it:
|
||||
|
||||
- Append new comments from the issue to the **Community Discussion** section.
|
||||
- Update the **Refined Feature Description** if new information changes the understanding.
|
||||
- Add any new **Related Ideas** cross-references found.
|
||||
- **Do NOT overwrite** existing content — append and enrich it.
|
||||
|
||||
### 1.5 Cross-Reference & Deduplication
|
||||
|
||||
After processing all issues:
|
||||
|
||||
- Scan all `_ideia/*.md` files for overlapping features.
|
||||
- If two features are substantially the same, add `🔗 Related Ideas` cross-references to both.
|
||||
- If one is a strict subset of another, note it in the smaller file: `> ℹ️ This feature is a subset of #<OTHER_NUMBER>. Consider implementing together.`
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Research: Find Solutions & Build Requirements
|
||||
|
||||
For each cataloged idea that is **viable** (aligns with the project's goals):
|
||||
|
||||
### 2.1 Viability Pre-Check
|
||||
|
||||
Before investing in research, quickly assess:
|
||||
|
||||
- [ ] Does this feature align with the project's goals and architecture?
|
||||
- [ ] Is it technically feasible with the current codebase?
|
||||
- [ ] Does it duplicate existing functionality?
|
||||
- [ ] Would it introduce breaking changes or security risks?
|
||||
- [ ] Is there enough detail to understand what's needed?
|
||||
|
||||
**Verdict options:**
|
||||
|
||||
| Verdict | When | Action |
|
||||
| --------------------- | ------------------------------------- | --------------------------- |
|
||||
| ✅ **VIABLE** | Good idea, enough context | Proceed to Research |
|
||||
| ❓ **NEEDS DETAIL** | Good idea, insufficient spec | Skip research, ask author |
|
||||
| ⏭️ **DEFER** | Good idea, too complex for this cycle | Catalog only, skip research |
|
||||
| ❌ **NOT FIT** | Doesn't fit the project | Explain why |
|
||||
| 🔁 **ALREADY EXISTS** | Feature already implemented | Point to existing feature |
|
||||
|
||||
### 2.2 Internet Research (for VIABLE features)
|
||||
|
||||
For each viable feature, perform systematic research:
|
||||
|
||||
**Step 1 — Web search for similar implementations:**
|
||||
|
||||
```
|
||||
WebSearch("how to implement <feature description> in <tech stack>")
|
||||
WebSearch("<feature keyword> implementation nextjs typescript 2025 2026")
|
||||
WebSearch("<feature keyword> open source library npm")
|
||||
```
|
||||
|
||||
**Step 2 — Find reference Git repositories:**
|
||||
|
||||
```
|
||||
WebSearch("site:github.com <feature keyword> <tech stack> stars:>100")
|
||||
WebSearch("github <feature keyword> implementation recently updated 2026")
|
||||
```
|
||||
|
||||
- Find **up to 10 relevant repositories**, sorted by most recently updated.
|
||||
- For each repository:
|
||||
- Note the repo URL, star count, last commit date
|
||||
- Read its README and relevant source files via `WebFetch`
|
||||
- Extract the architectural approach, patterns used, and key code snippets
|
||||
|
||||
**Step 3 — Read API docs and standards:**
|
||||
|
||||
If the feature involves an external API, protocol, or standard:
|
||||
|
||||
- Find and read the official documentation
|
||||
- Note version requirements, authentication patterns, rate limits
|
||||
|
||||
### 2.3 Create Requirements File
|
||||
|
||||
For each researched feature, create a requirements file alongside its idea file:
|
||||
|
||||
**Filename**: `<NUMBER>-<kebab-case-short-title>.requirements.md`
|
||||
|
||||
```markdown
|
||||
# Requirements: <Feature Title>
|
||||
|
||||
> Feature Idea: [#<NUMBER>](./<NUMBER>-<kebab-case-short-title>.md)
|
||||
> Research Date: <YYYY-MM-DD>
|
||||
> Verdict: ✅ VIABLE
|
||||
|
||||
## 🔍 Research Summary
|
||||
|
||||
<Brief summary of what was found during research>
|
||||
|
||||
## 📚 Reference Implementations
|
||||
|
||||
| # | Repository | Stars | Last Updated | Approach | Relevance |
|
||||
| --- | ---------------- | ----- | ------------ | -------- | ------------ |
|
||||
| 1 | [repo/name](url) | ⭐ N | YYYY-MM-DD | <brief> | High/Med/Low |
|
||||
| 2 | ... | | | | |
|
||||
|
||||
### Key Patterns Found
|
||||
|
||||
- <pattern 1 with code snippet or link>
|
||||
- <pattern 2>
|
||||
|
||||
## 📐 Proposed Solution Architecture
|
||||
|
||||
### Approach
|
||||
|
||||
<Describe the chosen approach based on research findings>
|
||||
|
||||
### New Files
|
||||
|
||||
| File | Purpose |
|
||||
| --------------------- | ------------- |
|
||||
| `path/to/new/file.ts` | <description> |
|
||||
|
||||
### Modified Files
|
||||
|
||||
| File | Changes |
|
||||
| -------------------------- | -------------- |
|
||||
| `path/to/existing/file.ts` | <what changes> |
|
||||
|
||||
### Database Changes
|
||||
|
||||
- <migrations needed, if any>
|
||||
|
||||
### API Changes
|
||||
|
||||
- <new/modified endpoints, if any>
|
||||
|
||||
### UI Changes
|
||||
|
||||
- <new/modified pages/components, if any>
|
||||
|
||||
## ⚙️ Implementation Effort
|
||||
|
||||
- **Estimated complexity**: Low / Medium / High / Very High
|
||||
- **Estimated files changed**: ~N
|
||||
- **Dependencies needed**: <new npm packages, if any>
|
||||
- **Breaking changes**: Yes/No — <details>
|
||||
- **i18n impact**: <number of new translation keys>
|
||||
- **Test coverage needed**: <brief description>
|
||||
|
||||
## ⚠️ Open Questions
|
||||
|
||||
- <question 1>
|
||||
- <question 2>
|
||||
|
||||
## 🔗 External References
|
||||
|
||||
- <documentation URLs>
|
||||
- <API references>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2.5 — Organize & Respond: Sort Files and Post GitHub Comments
|
||||
|
||||
### 2.5.1 Create Directory Structure
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
mkdir -p <project_root>/_ideia/viable
|
||||
mkdir -p <project_root>/_ideia/viable/need_details
|
||||
mkdir -p <project_root>/_ideia/defer
|
||||
mkdir -p <project_root>/_ideia/notfit
|
||||
```
|
||||
|
||||
### 2.5.2 Move Idea Files to Category Subdirectories
|
||||
|
||||
After classification, move EVERY idea file to its correct subdirectory:
|
||||
|
||||
```bash
|
||||
# ✅ VIABLE — move idea + requirements files
|
||||
mv _ideia/<NUMBER>-*.md _ideia/viable/
|
||||
mv _ideia/<NUMBER>-*.requirements.md _ideia/viable/
|
||||
|
||||
# ❓ NEEDS DETAIL — viable but waiting for author response
|
||||
mv _ideia/<NUMBER>-*.md _ideia/viable/need_details/
|
||||
|
||||
# ⏭️ DEFER — move idea files only
|
||||
mv _ideia/<NUMBER>-*.md _ideia/defer/
|
||||
|
||||
# ❌ NOT FIT & 🔁 ALREADY EXISTS — move idea files only
|
||||
mv _ideia/<NUMBER>-*.md _ideia/notfit/
|
||||
```
|
||||
|
||||
No files should remain in `_ideia/` root after this step (except subdirectories).
|
||||
|
||||
### 2.5.3 Post GitHub Comments by Category
|
||||
|
||||
**Each category has a specific comment template and action:**
|
||||
|
||||
---
|
||||
|
||||
#### For 🔁 ALREADY EXISTS — Comment + CLOSE issue
|
||||
|
||||
// turbo
|
||||
|
||||
The feature already exists in the system. Explain WHERE it is and HOW to use it.
|
||||
|
||||
```markdown
|
||||
Hi @<author>! Thanks for the suggestion! 🙏
|
||||
|
||||
Great news — this functionality **already exists** in OmniRoute:
|
||||
|
||||
**📍 Where to find it:** <exact dashboard path or settings location>
|
||||
|
||||
**🔧 How to use it:**
|
||||
|
||||
1. <step 1>
|
||||
2. <step 2>
|
||||
3. <step 3>
|
||||
|
||||
If you have any trouble finding or using it, feel free to ask in a Discussion. We're always happy to help!
|
||||
|
||||
Closing this as the feature is already available. 🎉
|
||||
```
|
||||
|
||||
```bash
|
||||
gh issue close <NUMBER> --repo <owner>/<repo> --comment "<comment above>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### For ⏭️ DEFER — Comment + CLOSE issue
|
||||
|
||||
// turbo
|
||||
|
||||
Thank the user, explain the idea was cataloged, and that we'll study it before implementing.
|
||||
|
||||
```markdown
|
||||
Hi @<author>! Thanks for this thoughtful feature request! 🙏
|
||||
|
||||
We really appreciate the detailed proposal. We've **cataloged your idea** and it's now part of our improvement backlog.
|
||||
|
||||
Due to the **significant architectural impact** of this feature, we'll need to conduct thorough use-case studies and architectural analysis before we start development. This ensures we build it right and don't introduce regressions.
|
||||
|
||||
**What happens next:**
|
||||
|
||||
- Your idea is saved in our internal feature backlog
|
||||
- We'll conduct architecture studies when this area is prioritized
|
||||
- We'll notify you here when development begins
|
||||
|
||||
Thank you for contributing to OmniRoute's roadmap! Your input helps shape the product. 🚀
|
||||
```
|
||||
|
||||
```bash
|
||||
gh issue close <NUMBER> --repo <owner>/<repo> --comment "<comment above>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### For ❌ NOT FIT — Comment + CLOSE issue
|
||||
|
||||
// turbo
|
||||
|
||||
Politely explain why the feature doesn't fit the project scope.
|
||||
|
||||
```markdown
|
||||
Hi @<author>! Thanks for the suggestion! 🙏
|
||||
|
||||
After careful analysis, we've determined that this feature **falls outside OmniRoute's core scope** as a proxy/router.
|
||||
|
||||
**Reason:** <explain why — e.g., "Telegram integration belongs in the application/orchestrator layer that consumes OmniRoute's API, not inside the router itself.">
|
||||
|
||||
**Alternative:** <suggest an alternative approach if possible>
|
||||
|
||||
We appreciate you thinking of ways to improve OmniRoute! If you'd like to discuss this further, feel free to open a Discussion. 🙏
|
||||
```
|
||||
|
||||
```bash
|
||||
gh issue close <NUMBER> --repo <owner>/<repo> --comment "<comment above>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### For ❓ NEEDS DETAIL — Comment (keep OPEN)
|
||||
|
||||
// turbo
|
||||
|
||||
Ask for the specific missing details needed.
|
||||
|
||||
```markdown
|
||||
Hi @<author>! Thanks for the feature request — it's an interesting idea and we'd love to explore it further. 🙏
|
||||
|
||||
To move forward, we need a few more details:
|
||||
|
||||
1. <specific question 1>
|
||||
2. <specific question 2>
|
||||
3. <specific question 3>
|
||||
|
||||
If you know of any **open-source projects or repositories** that implement something similar, please share links — it would help us design the best solution.
|
||||
|
||||
Looking forward to your response! 🚀
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### For ✅ VIABLE — Comment (keep OPEN)
|
||||
|
||||
// turbo
|
||||
|
||||
Thank the user, confirm we've cataloged their idea, and explain it may be implemented in future versions.
|
||||
|
||||
```markdown
|
||||
Hi @<author>! Thanks for the great feature suggestion! 🙏
|
||||
|
||||
We've analyzed your request and it aligns well with OmniRoute's roadmap. We've **cataloged this feature** and it's in our implementation backlog.
|
||||
|
||||
**Status:** 📋 Cataloged for future implementation
|
||||
|
||||
This feature may be included in upcoming releases. We'll **respond to this issue and tag you** as soon as implementation begins so you can test it.
|
||||
|
||||
Thank you for helping improve OmniRoute! 🚀
|
||||
```
|
||||
|
||||
**⚠️ Do NOT close viable issues — they remain OPEN for tracking.**
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Report: Present Findings to User
|
||||
|
||||
### 3.1 🛑 MANDATORY STOP — Present Consolidated Report
|
||||
|
||||
After completing Phase 1, Phase 2, and Phase 2.5, **STOP and present the following report** in the chat. Do NOT proceed to implementation.
|
||||
|
||||
Present a structured report containing:
|
||||
|
||||
#### 3.1a — Feature Summary Table
|
||||
|
||||
| # | Issue | Title | Verdict | Location | Action |
|
||||
| --- | ----- | ----- | --------------- | ----------------------------- | ----------------------------- |
|
||||
| 1 | #N | Title | ✅ VIABLE | `_ideia/viable/` | Issue OPEN, comment posted |
|
||||
| 2 | #N | Title | ⏭️ DEFER | `_ideia/defer/` | Issue CLOSED with explanation |
|
||||
| 3 | #N | Title | ❌ NOT FIT | `_ideia/notfit/` | Issue CLOSED with explanation |
|
||||
| 4 | #N | Title | 🔁 EXISTS | `_ideia/notfit/` | Issue CLOSED with guidance |
|
||||
| 5 | #N | Title | ❓ NEEDS DETAIL | `_ideia/viable/need_details/` | Issue OPEN, questions posted |
|
||||
|
||||
#### 3.1b — Viable Features Detail
|
||||
|
||||
For each VIABLE feature, provide a brief paragraph:
|
||||
|
||||
- What was found during research
|
||||
- The proposed approach
|
||||
- Key risks or unknowns
|
||||
- Which reference repositories were most useful
|
||||
|
||||
#### 3.1c — Issues Requiring Author Feedback
|
||||
|
||||
For features marked ❓ NEEDS DETAIL, list:
|
||||
|
||||
- What specific information is missing
|
||||
- What examples or repository references would help
|
||||
|
||||
#### 3.1d — Ask for User Confirmation
|
||||
|
||||
End the report with:
|
||||
|
||||
> **Ready to proceed with implementation?**
|
||||
>
|
||||
> - Reply **"sim"** or **"yes"** to generate full implementation plans for all VIABLE features.
|
||||
> - Reply with specific issue numbers to select only certain features.
|
||||
> - Reply **"não"** or **"no"** to stop here.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — Plan: Generate Implementation Plans (after user says "yes")
|
||||
|
||||
> **⚠️ Do NOT enter this phase without explicit user approval from Phase 3.**
|
||||
|
||||
### 4.1 Create Task Directory
|
||||
|
||||
```bash
|
||||
mkdir -p <project_root>/_tasks/features-vX.Y.Z/
|
||||
```
|
||||
|
||||
### 4.2 Generate One Implementation Plan Per Feature
|
||||
|
||||
For each VIABLE feature approved by the user, create:
|
||||
|
||||
**Filename**: `_tasks/features-vX.Y.Z/<NUMBER>-<kebab-case-title>.plan.md`
|
||||
|
||||
```markdown
|
||||
# Implementation Plan: <Feature Title>
|
||||
|
||||
> Issue: #<NUMBER>
|
||||
> Idea: [\_ideia/viable/<NUMBER>-title.md](../../_ideia/viable/<NUMBER>-title.md)
|
||||
> Requirements: [\_ideia/viable/<NUMBER>-title.requirements.md](../../_ideia/viable/<NUMBER>-title.requirements.md)
|
||||
> Branch: `release/vX.Y.Z`
|
||||
|
||||
## Overview
|
||||
|
||||
<Brief description of what will be built>
|
||||
|
||||
## Pre-Implementation Checklist
|
||||
|
||||
- [ ] Read all related source files listed below
|
||||
- [ ] Confirm no conflicts with in-flight PRs
|
||||
- [ ] Verify database migration numbering
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: <Title>
|
||||
|
||||
**Files:**
|
||||
|
||||
- `path/to/file.ts` — <what to change>
|
||||
|
||||
**Details:**
|
||||
<Detailed description of the change, including code patterns to follow, function signatures, etc.>
|
||||
|
||||
### Step 2: <Title>
|
||||
|
||||
...
|
||||
|
||||
### Step N: Tests
|
||||
|
||||
**New test files:**
|
||||
|
||||
- `tests/unit/<test-file>.test.mjs` — <what to test>
|
||||
|
||||
**Test cases:**
|
||||
|
||||
- [ ] <test case 1>
|
||||
- [ ] <test case 2>
|
||||
|
||||
### Step N+1: i18n
|
||||
|
||||
**Translation keys to add:**
|
||||
|
||||
- `<namespace>.<key>` — "<English value>"
|
||||
|
||||
### Step N+2: Documentation
|
||||
|
||||
- [ ] Update CHANGELOG.md
|
||||
- [ ] Update relevant docs/ files
|
||||
|
||||
## Verification Plan
|
||||
|
||||
1. Run `npm run build` — must pass
|
||||
2. Run `npm test` — all tests must pass
|
||||
3. Run `npm run lint` — no new errors
|
||||
4. <Manual verification steps>
|
||||
|
||||
## Commit Plan
|
||||
```
|
||||
|
||||
feat: <description> (#<NUMBER>)
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
### 4.3 Present Plans for Final Approval
|
||||
|
||||
Present a summary of all generated plans:
|
||||
|
||||
> **Implementation plans generated:**
|
||||
>
|
||||
> | # | Feature | Plan File | Steps | Effort |
|
||||
> | --- | ------- | ---------------------------------------- | ------- | ------ |
|
||||
> | 1 | <title> | `_tasks/features-vX.Y.Z/N-title.plan.md` | N steps | Medium |
|
||||
>
|
||||
> Reply **"sim"** or **"yes"** to begin implementation of all features.
|
||||
> Reply with specific issue numbers to implement only certain ones.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — Execute: Implement the Plans (after user says "yes")
|
||||
|
||||
> **⚠️ Do NOT enter this phase without explicit user approval from Phase 4.**
|
||||
|
||||
### 5.1 Implement Each Feature
|
||||
|
||||
For each approved plan, execute it step by step:
|
||||
|
||||
1. **Follow the plan** — implement exactly as specified in the `.plan.md` file
|
||||
2. **Build** — Run `npm run build` after each feature to verify compilation
|
||||
3. **Test** — Run `npm test` to ensure no regressions
|
||||
4. **Commit** — Commit with: `feat: <description> (#<NUMBER>)`
|
||||
5. **Update the plan** — Mark completed steps with `[x]` in the plan file
|
||||
6. **Continue** — Move to the next feature (do NOT switch branches)
|
||||
|
||||
### 5.2 Respond to Authors (Update Viable Issues)
|
||||
|
||||
For each implemented feature, **close the issue with a final comment**:
|
||||
|
||||
````markdown
|
||||
✅ **Implemented in `release/vX.Y.Z`!**
|
||||
|
||||
Hi @<author>! Great news — your feature request has been implemented! 🎉
|
||||
|
||||
**What was done:**
|
||||
|
||||
- <bullet list of what was built>
|
||||
|
||||
**How to try it:**
|
||||
|
||||
```bash
|
||||
git fetch origin && git checkout release/vX.Y.Z
|
||||
npm install && npm run dev
|
||||
```
|
||||
````
|
||||
|
||||
This will be included in the upcoming **vX.Y.Z** release. Feel free to reopen if you spot any issues! 🚀
|
||||
|
||||
````
|
||||
|
||||
```bash
|
||||
gh issue close <NUMBER> --repo <owner>/<repo> --comment "<comment above>"
|
||||
````
|
||||
|
||||
Then **DELETE the idea file** — it has served its purpose:
|
||||
|
||||
```bash
|
||||
# ✅ Implemented files are DELETED (not moved)
|
||||
rm _ideia/viable/<NUMBER>-<title>.md
|
||||
rm _ideia/viable/<NUMBER>-<title>.requirements.md # if exists
|
||||
```
|
||||
|
||||
> **Why delete?** `viable/` only holds features that still NEED to be done. Once implemented, the commit history and CHANGELOG are the source of truth. Keeping the file would be confusing.
|
||||
|
||||
### 5.3 Finalize & Push
|
||||
|
||||
After implementing all approved features:
|
||||
|
||||
1. **Update CHANGELOG.md** on the release branch with all new feature entries
|
||||
2. Push the release branch: `git push origin release/vX.Y.Z`
|
||||
3. Run `/generate-release` workflow Phase 1 steps 7–10 (tests → commit → push → open PR to main → wait for user)
|
||||
|
||||
### 5.4 Final Summary Report
|
||||
|
||||
Present a final summary report to the user:
|
||||
|
||||
| Issue | Title | Verdict | Action | Commit |
|
||||
| ----- | ----- | --------------- | -------------------------------------------------- | --------- |
|
||||
| #N | Title | ✅ Implemented | Issue closed, idea file deleted | `abc1234` |
|
||||
| #N | Title | ⏭️ Deferred | Issue closed + saved in `_ideia/defer/` | — |
|
||||
| #N | Title | ❌ Not Fit | Issue closed + saved in `_ideia/notfit/` | — |
|
||||
| #N | Title | 🔁 Exists | Issue closed + saved in `_ideia/notfit/` | — |
|
||||
| #N | Title | ❓ Needs Detail | Issue OPEN, moved to `_ideia/viable/need_details/` | — |
|
||||
|
||||
Include:
|
||||
|
||||
- Total features harvested
|
||||
- Total ideas cataloged (`viable/need_details/` + `defer/` + `notfit/`)
|
||||
- Total features implemented (idea files deleted, issues closed)
|
||||
- Total features deferred
|
||||
- Total issues closed
|
||||
- Total issues left open (needs detail only — viable are closed after implementation)
|
||||
- Test results (pass/fail count)
|
||||
50
.claude/commands/issue-triage-cc.md
Normal file
50
.claude/commands/issue-triage-cc.md
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
description: How to respond to GitHub issues with insufficient information
|
||||
---
|
||||
|
||||
# Issue Triage Workflow
|
||||
|
||||
Respond to GitHub issues that need more information before they can be investigated.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Identify issues needing triage
|
||||
|
||||
```bash
|
||||
gh issue list --state open --limit 20
|
||||
```
|
||||
|
||||
### 2. Evaluate each issue
|
||||
|
||||
Check if the issue has:
|
||||
|
||||
- Clear reproduction steps
|
||||
- Environment details (OS, Node.js version, OmniRoute version)
|
||||
- Error logs/screenshots
|
||||
- Expected vs actual behavior
|
||||
|
||||
### 3. Respond with triage template
|
||||
|
||||
For issues missing information:
|
||||
|
||||
```markdown
|
||||
Thank you for reporting this issue! To help us investigate, please provide:
|
||||
|
||||
1. **OmniRoute version**: (`omniroute --version`)
|
||||
2. **Node.js version**: (`node --version`)
|
||||
3. **Operating system**: (e.g., Ubuntu 24.04, macOS 15, Windows 11)
|
||||
4. **Installation method**: (npm, Docker, source)
|
||||
5. **Steps to reproduce**: (exact commands/actions that trigger the issue)
|
||||
6. **Error logs**: (paste relevant logs from the console)
|
||||
7. **Expected behavior**: (what should happen)
|
||||
|
||||
This will help us debug and resolve your issue faster. 🙏
|
||||
```
|
||||
|
||||
### 4. Label the issue
|
||||
|
||||
Add appropriate labels: `needs-info`, `bug`, `enhancement`, `question`, etc.
|
||||
|
||||
```bash
|
||||
gh issue edit <NUMBER> --add-label "needs-info"
|
||||
```
|
||||
166
.claude/commands/resolve-issues-cc.md
Normal file
166
.claude/commands/resolve-issues-cc.md
Normal file
@@ -0,0 +1,166 @@
|
||||
---
|
||||
description: Fetch all open GitHub issues, analyze bugs, resolve what's possible, triage the rest, wait for user validation, then commit and release
|
||||
---
|
||||
|
||||
# /resolve-issues — Automated Issue Resolution Workflow
|
||||
|
||||
## Overview
|
||||
|
||||
This workflow fetches all open issues from the project's GitHub repository, classifies them, analyzes bugs, proposes a resolution plan, waits for user validation, and ONLY THEN implements the fixes, commits, and closes the issues on the current release branch (`release/vX.Y.Z`). It does NOT merge or release automatically — the release branch is later merged via PR to main.
|
||||
|
||||
> **BRANCH RULE**: All work MUST happen on the current `release/vX.Y.Z` branch. Never create separate `fix/` branches. If no release branch exists yet, create one first using `/generate-release` Phase 1 steps 1–5.
|
||||
|
||||
> **⛔ PR PROHIBITION**: If a fix is associated with a contributor's PR, you MUST merge their PR — NEVER close it and re-implement the fix yourself. See `/review-prs` workflow for the full policy. The `gh pr close` command is FORBIDDEN unless the repository owner explicitly requests it.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Identify the GitHub Repository
|
||||
|
||||
// turbo
|
||||
|
||||
- Run: `git -C <project_root> remote get-url origin` to extract the owner/repo
|
||||
- Parse the owner and repo name from the URL
|
||||
|
||||
### 2. Ensure Release Branch Exists
|
||||
|
||||
// turbo
|
||||
|
||||
Before doing any work, ensure you are on the current release branch:
|
||||
|
||||
```bash
|
||||
# Check current branch
|
||||
git branch --show-current
|
||||
|
||||
# If on main, determine next version and create the release branch
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
NEXT=$(node -p "const [a,b,c]=('$VERSION').split('.').map(Number); c>=9?a+'.'+(b+1)+'.0':a+'.'+b+'.'+(c+1)")
|
||||
git checkout -b release/v$NEXT
|
||||
npm version patch --no-git-tag-version
|
||||
npm install
|
||||
```
|
||||
|
||||
If already on a `release/vX.Y.Z` branch, continue working there.
|
||||
|
||||
### 3. Fetch All Open Issues
|
||||
|
||||
// turbo-all
|
||||
|
||||
**⚠️ CRITICAL**: The JSON output of `gh issue list` can be truncated by the tool, silently hiding issues. You MUST use the two-step approach below to guarantee **all** issues are fetched.
|
||||
|
||||
**Step 3a — Get Issue numbers only** (small output, never truncated):
|
||||
|
||||
- Run: `gh issue list --repo <owner>/<repo> --state open --limit 500 --json number --jq '.[].number'`
|
||||
- This outputs one issue number per line. Count them and confirm total.
|
||||
|
||||
**Step 3b — Fetch full metadata for each Issue** (one call per issue):
|
||||
|
||||
- For each issue number from step 3a, run:
|
||||
`gh issue view <NUMBER> --repo <owner>/<repo> --json number,title,labels,body,comments,createdAt,author`
|
||||
- You may batch these into parallel calls (up to 4 at a time).
|
||||
- Sort by oldest first (FIFO).
|
||||
|
||||
### 4. Classify Each Issue
|
||||
|
||||
For each issue, determine its type:
|
||||
|
||||
- **Bug** — Has `bug` label, or body contains error messages, stack traces, "doesn't work", "broken", "crash", "error"
|
||||
- **Feature Request** — Has `enhancement`/`feature` label, or body describes new functionality
|
||||
- **Question** — Has `question` label, or is asking "how to" something
|
||||
- **Other** — Anything else
|
||||
|
||||
Focus ONLY on **Bugs** for resolution. Feature requests and questions should be skipped with a note in the final report.
|
||||
|
||||
### 5. Deep-Read Each Bug Issue (One-by-One Analysis)
|
||||
|
||||
**IMPORTANT**: Read each bug issue thoroughly, one at a time, before moving to the next. This is NOT a batch process — each issue needs focused attention.
|
||||
|
||||
#### 5a. Understand the Problem
|
||||
|
||||
For each bug issue, perform the full analysis:
|
||||
|
||||
1. **Read the entire body** — including Description, Steps to Reproduce, Expected/Actual Behavior, Error Logs, and Screenshots
|
||||
2. **Read ALL comments** — including bot triage comments (Kilo, etc.) and owner/community responses. Pay attention to:
|
||||
- Whether someone already responded with a fix
|
||||
- Whether a community member confirmed the issue is resolved
|
||||
- Whether the issue was marked as duplicate by a bot. **WARNING: DO NOT blindly trust bot duplicate labels (e.g., kilo-duplicate). Bots make mistakes. You MUST read the full conversation and do your own independent analysis to determine if it is truly a duplicate or a distinct bug.**
|
||||
3. **Identify the claimed error** — extract the exact error message, status code, and provider/model involved
|
||||
|
||||
#### 5b. Check Information Sufficiency
|
||||
|
||||
Verify the issue contains enough to act on:
|
||||
|
||||
- [ ] Clear description of the problem
|
||||
- [ ] Steps to reproduce OR error logs
|
||||
- [ ] Provider/model/version information
|
||||
- [ ] Expected vs actual behavior
|
||||
|
||||
#### 5c. Determine Issue Disposition
|
||||
|
||||
For each bug, classify into one of 5 actions:
|
||||
|
||||
| Disposition | When to Apply | Action |
|
||||
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
|
||||
| **✅ CLOSE — Already Fixed** | Owner responded with fix + no user follow-up, OR community confirmed fix | Close with comment citing which version fixed it |
|
||||
| **✅ CLOSE — Duplicate** | You have independently verified the issue is a duplicate (do NOT rely solely on bot flags) + user provides no new info | Close referencing the original issue |
|
||||
| **✅ CLOSE — Stale** | We requested logs/info > 7 days ago with no reply | Close thanking the user, invite to reopen if needed |
|
||||
| **📝 RESPOND — Needs Info** | Issue is real but missing critical reproduction details | Comment asking for specifics per `/issue-triage` |
|
||||
| **📝 RESPOND — User Config** | Error is caused by unsupported env (Node version, wrong model path, missing API enablement) | Comment explaining the user-side fix |
|
||||
| **🔧 FIX — Code Change** | Root cause is confirmed in the codebase | Research, propose solution in report, wait for approval |
|
||||
|
||||
#### 5d. For "FIX — Code Change" Issues
|
||||
|
||||
Before coding, perform deep source analysis to formulate a plan:
|
||||
|
||||
1. **Search the codebase** — `grep_search` for error strings, relevant function names, affected files
|
||||
2. **Search the web** — for upstream API changes, SDK updates, or breaking changes that explain the bug
|
||||
3. **Read the full source file** — don't rely on grep snippets; understand the surrounding logic
|
||||
4. **Verify the root cause** — confirm the bug is reproducible based on the code, not just a user misconfiguration
|
||||
5. **Formulate a proposed solution** — detail the exact files and lines you will change and how you will solve it.
|
||||
6. **Create an Implementation Plan file** — write your proposed solution to `_tasks/features-vX.Y.Z/<ISSUE_NUMBER>-<short-description>.plan.md` (e.g. `_tasks/features-v3.7.6/1810-auto-restore-probe-failed-db.plan.md`) where `vX.Y.Z` is the current branch version. The plan should contain an Overview, Pre-Implementation Checklist, and detailed Implementation Steps (Files, Changes).
|
||||
7. **DO NOT modify the codebase yet** — wait for user approval on your report and plan first.
|
||||
|
||||
#### 5e. For "RESPOND" Issues
|
||||
|
||||
Post a substantive comment that:
|
||||
|
||||
- Acknowledges the specific error they reported
|
||||
- Explains the likely root cause
|
||||
- Provides concrete steps to resolve (version upgrade, env var fix, model path correction)
|
||||
- Asks for follow-up info if needed
|
||||
|
||||
**Do NOT post generic template responses.** Every comment should reference the user's specific error messages and environment.
|
||||
|
||||
### 6. Generate Report & Wait for Validation
|
||||
|
||||
Present a summary report to the user detailing your proposed actions. For any bugs that need fixing, explicitly explain your proposed solution (files to change and logic) and point out that it will be implemented on the release branch (`release/vX.Y.Z`) after approval.
|
||||
|
||||
| Issue | Title | Status | Proposed Action / Version |
|
||||
| ----- | ----- | ------------- | ----------------------------------------- |
|
||||
| #N | Title | ✅ Close | Already fixed / duplicate (explain why) |
|
||||
| #N | Title | 🔧 Propose | Explanation of the code fix to be applied |
|
||||
| #N | Title | 📝 Respond | Guidance comment to be posted |
|
||||
| #N | Title | ❓ Needs Info | Triage comment to be posted |
|
||||
| #N | Title | ⏭️ Skip | Feature request / not a bug |
|
||||
|
||||
> **⚠️ IMPORTANT**: Do NOT implement code changes, commit, push, or close issues at this step.
|
||||
> Wait for the user to review the proposed fixes and respond with **OK** before proceeding.
|
||||
|
||||
- If the user says **OK** or approves → Proceed to step 7
|
||||
- If the user requests changes → Adjust the proposed solution and present the report again
|
||||
- If the user rejects → Revert any accidental changes and stop
|
||||
|
||||
### 7. Implement Fixes, Run Tests & Commit (only after user approval)
|
||||
|
||||
After the user validates and gives the OK:
|
||||
|
||||
1. **Implement the fixes** — modify the codebase according to the approved plan.
|
||||
2. **Run tests** — `npm run test:all` (or the specific test file) to ensure 100% pass.
|
||||
3. **Update CHANGELOG.md** with all new bug fix entries.
|
||||
4. **Commit** each fix individually on the release branch with message format: `fix: <description> (#<issue_number>)`.
|
||||
5. **Push** the release branch: `git push origin release/vX.Y.Z`.
|
||||
6. **Close resolved issues immediately**. For each issue that was marked as Fixed, run:
|
||||
`gh issue close <NUMBER> --repo <owner>/<repo> --comment "Thank you for reporting! This issue has been fixed and will be included in the next release (vX.Y.Z)."`
|
||||
7. Likewise, close `Duplicate` issues referencing the original, close `Needs Info` if stale, and post the required comments.
|
||||
8. If the project runs automatic releases or needs a PR, proceed to run `/generate-release` workflow Phase 1 steps 7–10 (tests → commit → push → open PR to main → wait for user).
|
||||
|
||||
If NO fixes were committed, skip closing and source control steps and just conclude the workflow.
|
||||
120
.claude/commands/review-discussions-cc.md
Normal file
120
.claude/commands/review-discussions-cc.md
Normal file
@@ -0,0 +1,120 @@
|
||||
---
|
||||
description: Read all open GitHub Discussions, summarize them, respond to pending ones, and create issues from actionable feature requests
|
||||
---
|
||||
|
||||
# /review-discussions — GitHub Discussions Review & Response Workflow
|
||||
|
||||
## Overview
|
||||
|
||||
This workflow reads all open GitHub Discussions, generates a categorized summary, identifies which ones need a response, drafts and posts replies, and optionally creates issues from actionable feature requests. It follows the same flow used for Issues but adapted for the Discussions forum.
|
||||
|
||||
> **Tool mapping note (v3.8):** Where steps below say `browser_subagent` (an earlier-runtime tool), in Claude Code use the `gh` CLI via the `Bash` tool — `gh api graphql` for reading discussions and `gh api graphql -F query=...` mutations for posting comments. `WebFetch` is acceptable for read-only HTML scraping when GraphQL is overkill, but prefer `gh` for any write actions.
|
||||
|
||||
// turbo-all
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Identify the GitHub Repository
|
||||
|
||||
- Run: `git -C <project_root> remote get-url origin` to extract the owner/repo
|
||||
- Parse the owner and repo name from the URL
|
||||
|
||||
### 2. Fetch All Open Discussions
|
||||
|
||||
- Use `WebFetch` to fetch `https://github.com/<owner>/<repo>/discussions`
|
||||
- Parse the discussion list to get all discussion titles, IDs, authors, categories, and dates
|
||||
- For each discussion, fetch the individual page to read the full content and all comments/replies
|
||||
|
||||
### 3. Summarize All Discussions
|
||||
|
||||
For each discussion, extract:
|
||||
|
||||
- **Title** and **#Number**
|
||||
- **Author** (GitHub username)
|
||||
- **Category** (Announcements, General, Ideas, Q&A, Show and tell)
|
||||
- **Date** created
|
||||
- **Summary** of the original post (1-2 sentences)
|
||||
- **Comments count** and key participants
|
||||
- **Your previous response** (if any)
|
||||
- **Pending action** — whether a response or follow-up is needed
|
||||
|
||||
### 4. Present Summary Report to User
|
||||
|
||||
Present the full summary to the user organized by category, using a table:
|
||||
|
||||
| # | Category | Title | Author | Date | Status |
|
||||
| --- | -------- | ----- | ------ | ------ | ----------------- |
|
||||
| #N | Ideas | Title | @user | Mar 23 | ⚠️ Needs response |
|
||||
| #N | Q&A | Title | @user | Mar 9 | ✅ Answered |
|
||||
| #N | General | Title | @user | Mar 19 | ⚠️ Needs response |
|
||||
|
||||
Highlight:
|
||||
|
||||
- **⚠️ Needs response** — No reply from maintainer, or a follow-up comment was left unanswered
|
||||
- **✅ Answered** — Maintainer already responded
|
||||
- **🐛 Bug reported** — A bug was mentioned that needs tracking
|
||||
- **💡 Actionable** — Contains a concrete feature request that could become an issue
|
||||
|
||||
### 5. Draft & Post Responses
|
||||
|
||||
For each discussion that needs a response, draft a reply following these guidelines:
|
||||
|
||||
#### Response Style
|
||||
|
||||
- **Friendly and professional** — Start with "Hey @username!"
|
||||
- **Acknowledge the contribution** — Thank the user for their input
|
||||
- **Be specific** — Reference existing features, settings, or dashboard pages if the feature already exists
|
||||
- **Provide workarounds** — If the request isn't implemented yet, suggest current alternatives
|
||||
- **Commit to action** — If the request is valid, state that you'll open an issue or add it to the roadmap
|
||||
- **Keep it concise** — 3-5 paragraphs max
|
||||
|
||||
#### Posting via Browser
|
||||
|
||||
- Use `browser_subagent` to navigate to each discussion and post the comment
|
||||
- **IMPORTANT**: When typing text in GitHub comment boxes via the browser, use only plain ASCII characters:
|
||||
- Use regular hyphens `-` instead of em-dashes
|
||||
- Use `->` instead of arrow symbols
|
||||
- Do NOT use emoji Unicode characters (the browser keyboard may fail on them)
|
||||
- Use `**bold**` and `\`code\`` markdown formatting
|
||||
- Click the green "Comment" button (or "Reply" for threaded replies) after typing
|
||||
- Verify the comment was posted by checking the page shows the new comment
|
||||
|
||||
### 6. Create Issues from Actionable Feature Requests
|
||||
|
||||
For discussions that contain concrete, actionable feature requests:
|
||||
|
||||
1. Ask the user which ones should become issues
|
||||
2. For each approved request, create a GitHub issue via `browser_subagent`:
|
||||
- Navigate to `https://github.com/<owner>/<repo>/issues/new`
|
||||
- **Title**: `<Feature Name> - <Short description>`
|
||||
- **Body** should include:
|
||||
- `## Feature Request` header
|
||||
- `**Source:** Discussion #N by @author`
|
||||
- `## Problem` — What limitation the user hit
|
||||
- `## Proposed Solution` — How it could work
|
||||
- `### Implementation Ideas` — Technical approach
|
||||
- `### Current Workarounds` — What users can do today
|
||||
- `## Additional Context` — Links to related issues/discussions
|
||||
- Add `enhancement` label
|
||||
- Click "Submit new issue" / "Create"
|
||||
3. After creation, go back to the original discussion and post a comment linking to the new issue:
|
||||
- "I've opened Issue #N to track this feature request. Follow along there for updates!"
|
||||
|
||||
### 7. Final Report
|
||||
|
||||
Present a final summary to the user:
|
||||
|
||||
| Discussion | Action Taken |
|
||||
| ---------- | ---------------------------------- |
|
||||
| #N — Title | Responded with workarounds |
|
||||
| #N — Title | Responded + created Issue #N |
|
||||
| #N — Title | Already answered, no action needed |
|
||||
| #N — Title | Responded to follow-up comment |
|
||||
|
||||
## Notes
|
||||
|
||||
- This workflow is **interactive** — always present the summary and wait for user approval before posting responses or creating issues
|
||||
- If the user says "pode responder" (or similar approval), proceed with posting all drafted responses
|
||||
- For discussions in non-English languages, respond in the same language as the original post
|
||||
- Always reference specific dashboard paths, config options, or code files when explaining existing features
|
||||
- When a discussion reveals a bug, note it separately from feature requests
|
||||
256
.claude/commands/review-prs-cc.md
Normal file
256
.claude/commands/review-prs-cc.md
Normal file
@@ -0,0 +1,256 @@
|
||||
---
|
||||
description: Analyze open Pull Requests from the project's GitHub repository, generate a critical report, and optionally implement approved changes
|
||||
---
|
||||
|
||||
# /review-prs — PR Review & Analysis Workflow
|
||||
|
||||
## ⛔ ABSOLUTE PROHIBITION — Read Before Anything Else
|
||||
|
||||
> **NEVER close a contributor's PR if you intend to use ANY of their code, ideas, or fixes.**
|
||||
>
|
||||
> **NEVER manually integrate contributor code into a release branch and then close their PR.**
|
||||
>
|
||||
> These actions are **STRICTLY FORBIDDEN** under all circumstances:
|
||||
>
|
||||
> 1. ❌ Closing a PR and cherry-picking/copying its code into a release branch
|
||||
> 2. ❌ Closing a PR "because of conflicts" and re-implementing the same fix yourself
|
||||
> 3. ❌ Closing a PR and committing a "similar" solution inspired by it
|
||||
> 4. ❌ Using `gh pr close` on any PR whose content was or will be used
|
||||
>
|
||||
> **Why**: Closing a PR after taking the contributor's work means they get ZERO credit on GitHub — no "Merged" badge, no contribution graph entry, no public record. This is effectively stealing their contribution. An audit found this happened to **37 PRs** in the past.
|
||||
>
|
||||
> **The ONLY acceptable flow**: Resolve conflicts IN the contributor's branch, push fixes TO their branch, then merge THEIR PR via `gh pr merge`. See Step 7 and Step 8 for the exact procedure.
|
||||
>
|
||||
> **When to close a PR**: ONLY when the user (repository owner) explicitly requests it, OR when the PR is clearly spam/malicious, OR when the author themselves asks to close it. In ALL other cases, leave it open.
|
||||
|
||||
## Overview
|
||||
|
||||
This workflow fetches all open PRs from the project's GitHub repository, performs a critical analysis of each one, generates a detailed report, and waits for user approval before proceeding with implementation. **All improvements are committed on the current release branch** (`release/vX.Y.Z`).
|
||||
|
||||
> **BRANCH RULE**: PRs are ALWAYS merged into the current `release/vX.Y.Z` branch, NEVER directly into `main`. The release branch acts as a staging area — only after all PRs are integrated and tests pass does the release branch get merged into `main` via the `/generate-release` workflow.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Identify the GitHub Repository
|
||||
|
||||
- Read `package.json` to get the repository URL, or use the git remote origin URL
|
||||
// turbo
|
||||
- Run: `git -C <project_root> remote get-url origin` to extract the owner/repo
|
||||
|
||||
### 2. Ensure Release Branch Exists
|
||||
|
||||
// turbo
|
||||
|
||||
Before doing any work, ensure you are on the current release branch:
|
||||
|
||||
```bash
|
||||
# Check current branch
|
||||
git branch --show-current
|
||||
|
||||
# If on main, determine next version and create the release branch
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
# Bump patch: e.g. 3.3.11 → 3.3.12
|
||||
NEXT=$(node -p "const [a,b,c]=('$VERSION').split('.').map(Number); c>=9?a+'.'+(b+1)+'.0':a+'.'+b+'.'+(c+1)")
|
||||
git checkout -b release/v$NEXT
|
||||
npm version patch --no-git-tag-version
|
||||
npm install
|
||||
```
|
||||
|
||||
If already on a `release/vX.Y.Z` branch, continue working there.
|
||||
|
||||
### 3. Fetch Open Pull Requests
|
||||
|
||||
// turbo-all
|
||||
|
||||
**⚠️ CRITICAL**: The JSON output of `gh pr list` can be truncated by the tool, silently hiding PRs. You MUST use the two-step approach below to guarantee **all** PRs are fetched.
|
||||
|
||||
**Step 3a — Get PR numbers only** (small output, never truncated):
|
||||
|
||||
- Run: `gh pr list --repo <owner>/<repo> --state open --limit 500 --json number --jq '.[].number'`
|
||||
- This outputs one PR number per line. Count them and confirm total.
|
||||
|
||||
**Step 3b — Fetch full metadata for each PR** (one call per PR):
|
||||
|
||||
- For each PR number from step 3a, run:
|
||||
`gh pr view <NUMBER> --repo <owner>/<repo> --json number,title,author,headRefName,baseRefName,body,createdAt,additions,deletions,files`
|
||||
- You may batch these into parallel calls (up to 4 at a time).
|
||||
|
||||
**Step 3c — Fetch diffs for each PR** (one call per PR, saved to /tmp):
|
||||
|
||||
- For each PR number, run:
|
||||
`gh pr diff <NUMBER> --repo <owner>/<repo> > /tmp/pr<NUMBER>.diff`
|
||||
- Then read each diff file with the `Read` tool.
|
||||
|
||||
- For each open PR, collect:
|
||||
- PR number, title, author, branch, number of commits, date
|
||||
- PR description/body
|
||||
- Files changed (diff)
|
||||
- Existing review comments (from bots or humans)
|
||||
|
||||
**Verification**: Confirm the count of PRs analyzed matches the count from step 3a before proceeding.
|
||||
|
||||
### 3.5 Redirect PR Base Branches to Release Branch
|
||||
|
||||
// turbo-all
|
||||
|
||||
**⚠️ CRITICAL**: Contributors typically open PRs targeting `main`. Before analyzing or merging, redirect ALL open PRs to target the current release branch instead.
|
||||
|
||||
```bash
|
||||
# Get the current release branch name
|
||||
RELEASE_BRANCH=$(git branch --show-current) # e.g. release/v3.5.4
|
||||
|
||||
# For each open PR that targets main, change its base to the release branch
|
||||
for PR_NUM in $(gh pr list --repo <owner>/<repo> --state open --json number,baseRefName --jq '.[] | select(.baseRefName == "main") | .number'); do
|
||||
echo "Redirecting PR #$PR_NUM → $RELEASE_BRANCH"
|
||||
gh pr edit "$PR_NUM" --repo <owner>/<repo> --base "$RELEASE_BRANCH"
|
||||
done
|
||||
```
|
||||
|
||||
This ensures:
|
||||
|
||||
1. PRs merge into the release branch, not directly into `main`
|
||||
2. Merge conflict detection is accurate against the release branch
|
||||
3. The release branch accumulates all changes before the final merge to `main`
|
||||
4. If the release branch doesn't exist on remote yet, push it first: `git push origin $RELEASE_BRANCH`
|
||||
|
||||
### 4. Analyze Each PR — For each open PR, perform the following analysis:
|
||||
|
||||
#### 4a. Feature Assessment
|
||||
|
||||
- **Does it make sense?** Evaluate if the feature fills a real gap or solves a valid problem
|
||||
- **Alignment** — Check if it aligns with the project's architecture and roadmap
|
||||
- **Complexity** — Assess if the scope is reasonable or if it should be split
|
||||
|
||||
#### 4b. Code Quality Review
|
||||
|
||||
- Check for code duplication
|
||||
- Evaluate error handling patterns (consistent with existing codebase?)
|
||||
- Check naming conventions and code style
|
||||
- Verify TypeScript types (any `any` usage, missing types?)
|
||||
|
||||
#### 4c. Security Review
|
||||
|
||||
- Check for missing authentication/authorization on new endpoints
|
||||
- Check for injection vulnerabilities (URL params, SQL, XSS)
|
||||
- Verify input validation on all user-controlled data
|
||||
- Check for hardcoded secrets or credentials
|
||||
|
||||
#### 4d. Architecture Review
|
||||
|
||||
- Does the change follow existing patterns?
|
||||
- Are there any breaking changes to public APIs?
|
||||
- Is the database schema affected? Migration needed?
|
||||
- Impact on performance (N+1 queries, missing indexes?)
|
||||
|
||||
#### 4e. Test Coverage
|
||||
|
||||
- Does the PR include tests?
|
||||
- Are edge cases covered?
|
||||
- Would existing tests break?
|
||||
|
||||
#### 4f. Cross-Layer (Global) Analysis
|
||||
|
||||
Perform a **global impact assessment** to verify whether the PR changes are complete across all layers of the application:
|
||||
|
||||
- **Backend → Frontend check**: If the PR adds or modifies backend-only resources (new endpoints, services, data models), evaluate whether corresponding frontend changes are missing:
|
||||
- Does a new endpoint require a new screen/page in the dashboard?
|
||||
- Should there be a new action button, menu item, or navigation link?
|
||||
- Are there new data fields that should be displayed or editable in the UI?
|
||||
- Does a new feature need a toggle, configuration panel, or status indicator?
|
||||
- **Frontend → Backend check**: If the PR adds frontend elements, verify the backend support exists:
|
||||
- Are the required API endpoints implemented?
|
||||
- Is the data model sufficient for the new UI components?
|
||||
- **Cross-cutting concerns**: Check shared layers (types, DTOs, validation schemas, routes, middleware) for completeness
|
||||
- **Document gaps** — If missing layers are detected, list them as **IMPORTANT** issues in the report with concrete suggestions for what should be added
|
||||
|
||||
### 5. Generate Report — Create a markdown report for each PR including:
|
||||
|
||||
- **PR Summary** — What it does, files affected, commit count
|
||||
- **Improvements/Benefits** — Numbered list with impact level (HIGH/MEDIUM/LOW)
|
||||
- **Risks & Issues** — Categorized as CRITICAL / IMPORTANT / MINOR
|
||||
- **Scoring Table** — Rate across: Feature Relevance, Code Quality, Security, Robustness, Tests
|
||||
- **Verdict** — Ready to merge? With mandatory vs optional fixes
|
||||
- **Next Steps** — What will happen if approved
|
||||
|
||||
### 6. Present to User
|
||||
|
||||
- Show the report in the final response and stop. This is a mandatory checkpoint awaiting explicit user approval before continuing.
|
||||
- Wait for user decision:
|
||||
- **Approved** → Proceed to step 7
|
||||
- **Approved with changes** → Implement the fixes and corrections before merging
|
||||
- **Rejected** → Close the PR or leave a review comment
|
||||
|
||||
### 7. Pre-Merge Fixes & CI Green-Lighting (if approved)
|
||||
|
||||
> **⚠️ Fixes and Conflict Resolutions MUST be pushed back to the PR branch before merging.** We want the PR itself to be green and fully valid before it integrates.
|
||||
|
||||
- **Sync latest fixes & Resolve Conflicts:** Merge the current `release` branch into the PR branch. If there are merge conflicts, you MUST resolve them inside the author's PR branch. NEVER resolve conflicts by closing their PR and doing the work in a separate branch, as this steals credit from the original author.
|
||||
- **Implement improvements:** Apply the required fixes identified in the analysis directly on the PR branch (e.g., adding missing API routes, fixing SSRF, applying comments from other agents).
|
||||
- **Pushing changes to PR branches:**
|
||||
|
||||
```bash
|
||||
# Checkout the PR locally
|
||||
gh pr checkout <NUMBER>
|
||||
|
||||
# Apply fixes, commit your changes
|
||||
git commit -m "chore: apply review suggestions and missing layers"
|
||||
|
||||
# Attempt to push directly to the PR branch
|
||||
git push
|
||||
```
|
||||
|
||||
- **Fallback (ONLY for external forks without maintainer edit access):**
|
||||
Using `cherry-pick` instead of fixing the contributor's PR directly is a **LAST RESORT**. You MUST ALWAYS attempt to `git push` your fixes to their branch first.
|
||||
**ONLY if `git push` explicitly fails with a permission/access error** (meaning the contributor unchecked "Allow edits from maintainers" or it's a locked fork), you may use `git cherry-pick` to bring their changes into the release branch and fix the issues locally.
|
||||
Even then, ensure you preserve the contributor's authorship (`git commit --author="Contributor Name <email>"` if creating new commits).
|
||||
Once you have integrated their work into the release branch, **DO NOT close their PR**. Leave it open so the contributor retains credit. Under NO CIRCUMSTANCES should you use `gh pr close`.
|
||||
|
||||
- Run the project's test suite locally to verify nothing breaks:
|
||||
// turbo
|
||||
- Run: `npm test` or equivalent test command
|
||||
|
||||
### 8. Merge into Release Branch (NEVER CLOSE!)
|
||||
|
||||
> **⚠️ CRITICAL**: NEVER use `gh pr close` for a PR whose idea or code was accepted. Closing a PR in a contributor's face after taking their idea—or closing it just because it had conflicts—is unacceptable.
|
||||
> You MUST ALWAYS resolve conflicts and apply fixes ON THE AUTHOR'S PR BRANCH (unless explicitly locked from edits), and then merge the PR using GitHub so the contributor gets the official "Merged" badge and proper credit on their profile. **Do not use cherry-pick just because it is "easier" than resolving conflicts on their branch.**
|
||||
|
||||
Even if the PR had severe conflicts or required significant architectural adjustments, you MUST:
|
||||
|
||||
1. Resolve any conflicts and apply the fixes directly to their PR branch (as detailed in step 7) or use cherry-picking into the release branch.
|
||||
2. If you managed to fix their branch, merge it into the release branch using the GitHub CLI:
|
||||
`gh pr merge <NUMBER> --repo <owner>/<repo> --squash --body "Integrated into release/vX.Y.Z"`
|
||||
3. If you had to use cherry-picking because you couldn't push to their branch, DO NOT close the PR. GitHub will sometimes auto-detect the cherry-picked commits and mark it as Merged. If it doesn't, leave it open. The repository owner will handle it. NEVER run `gh pr close`.
|
||||
|
||||
In ALL cases:
|
||||
|
||||
- Post a **thank-you comment** on the PR via the GitHub API before or immediately after merging.
|
||||
- The message should:
|
||||
- Thank the author by name/username for their contribution.
|
||||
- Explain what was adjusted or improved (if we pushed fixes to their branch or cherry-picked).
|
||||
- Note it will be included in the upcoming release.
|
||||
- Be friendly, professional, and encouraging.
|
||||
|
||||
> **⚠️ MANDATORY CHANGELOG CREDIT**: When cherry-picking is used (because the PR branch couldn't be pushed to or `gh pr merge` failed), the contributor does NOT get the automatic GitHub "Merged" badge. In this case, you MUST compensate by adding an explicit entry to `CHANGELOG.md` in the `[Unreleased]` section with `(#PR_NUMBER — thanks @username)` format. This ensures the contributor gets public credit in the release notes even if GitHub doesn't auto-detect the cherry-pick. This is NOT optional — skipping it effectively erases the contributor's work from the release record.
|
||||
|
||||
### 9. Sync Local Release Branch
|
||||
|
||||
After merging PRs, sync the local release branch to include the new changes:
|
||||
|
||||
```bash
|
||||
git fetch origin
|
||||
git pull origin release/vX.Y.Z
|
||||
```
|
||||
|
||||
### 10. Continue or Finalize
|
||||
|
||||
After processing all approved PRs:
|
||||
|
||||
- If more PRs remain, go back to step 7
|
||||
- When all PRs are processed, **update CHANGELOG.md** on the release branch with all new entries
|
||||
- Run **test coverage** to verify the gate (≥75% statements/lines/functions, ≥70% branches — measured ~82%):
|
||||
```bash
|
||||
npm run test:coverage
|
||||
```
|
||||
- Fix any test regressions introduced by merged PRs
|
||||
- Run `/generate-release` workflow Phase 1 steps 7–10 (tests → commit → push → open PR to main → wait for user)
|
||||
- The `/generate-release` workflow handles the final merge from `release/vX.Y.Z` → `main`
|
||||
341
.claude/commands/version-bump-cc.md
Normal file
341
.claude/commands/version-bump-cc.md
Normal file
@@ -0,0 +1,341 @@
|
||||
---
|
||||
description: Bump version, auto-generate CHANGELOG from git commits, update all versioned files, and refresh root + docs/ documentation to reflect the current project state
|
||||
---
|
||||
|
||||
# Version Bump Workflow
|
||||
|
||||
Automatically bump the project version, generate CHANGELOG entries from git history since the last tag, update every file that references the version, and refresh project documentation to reflect the current state.
|
||||
|
||||
> **VERSION RULE: Always use PATCH bumps (3.x.y → 3.x.y+1)**
|
||||
> NEVER use `npm version minor` or `npm version major`.
|
||||
> Always use: `npm version patch --no-git-tag-version`
|
||||
> The threshold rule: when `y` reaches 10, bump to `3.(x+1).0` — e.g. `3.4.10` → `3.5.0`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Determine Version
|
||||
|
||||
### 1. Read current version and last tag
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute
|
||||
CURRENT_VERSION=$(node -p "require('./package.json').version")
|
||||
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
|
||||
CURRENT_BRANCH=$(git branch --show-current)
|
||||
echo "Current version: $CURRENT_VERSION"
|
||||
echo "Last tag: $LAST_TAG"
|
||||
echo "Current branch: $CURRENT_BRANCH"
|
||||
```
|
||||
|
||||
### 2. Calculate new version
|
||||
|
||||
Apply the patch bump rule:
|
||||
|
||||
- If the current patch number is `9`, the new version is `3.(minor+1).0`
|
||||
- Otherwise, increment patch: `3.x.y` → `3.x.(y+1)`
|
||||
|
||||
If the version was ALREADY bumped (e.g. you are on a release branch and package.json already has the new version), **skip the npm version bump** and use the existing version.
|
||||
|
||||
### 3. Bump package.json (if needed)
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
# Only if version hasn't been bumped yet
|
||||
npm version patch --no-git-tag-version
|
||||
```
|
||||
|
||||
Or for threshold (y=10):
|
||||
|
||||
```bash
|
||||
# Manual threshold bump
|
||||
VERSION="3.X.0" # compute manually
|
||||
npm version "$VERSION" --no-git-tag-version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Generate CHANGELOG from Git History
|
||||
|
||||
### 4. Collect commits since last tag
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute
|
||||
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null)
|
||||
echo "=== Commits since $LAST_TAG ==="
|
||||
git log "$LAST_TAG"..HEAD --pretty=format:"%h %s" --no-merges | head -100
|
||||
echo ""
|
||||
echo "=== Merge commits ==="
|
||||
git log "$LAST_TAG"..HEAD --merges --pretty=format:"%h %s" | head -50
|
||||
```
|
||||
|
||||
### 5. Classify commits and generate CHANGELOG section
|
||||
|
||||
Analyze each commit message and classify into categories based on the conventional-commit prefix and content:
|
||||
|
||||
| Category | Patterns |
|
||||
| ------------------- | ------------------------------------------------ |
|
||||
| ✨ New Features | `feat:`, `feat(*):` |
|
||||
| 🐛 Bug Fixes | `fix:`, `fix(*):` |
|
||||
| ⚠️ Breaking Changes | `BREAKING CHANGE`, `!:` suffix |
|
||||
| 🛠️ Maintenance | `chore:`, `refactor:`, `perf:`, `build:` |
|
||||
| 🧪 Tests | `test:`, `tests:` |
|
||||
| 📝 Documentation | `docs:` |
|
||||
| 🔒 Security | `security:`, CVE references, vulnerability fixes |
|
||||
| 🌍 i18n | translation updates, locale changes |
|
||||
|
||||
For each category with entries, create a markdown section with descriptive bullet points. Use the commit messages but rewrite them to be human-readable and descriptive (not raw commit messages).
|
||||
|
||||
**If a commit references a PR number** (e.g. `#880`, `PR #885`), include it in the description.
|
||||
|
||||
### 6. Update CHANGELOG.md
|
||||
|
||||
Replace the `## [Unreleased]` section content with the generated entries, then add the new versioned section:
|
||||
|
||||
```markdown
|
||||
## [Unreleased]
|
||||
|
||||
---
|
||||
|
||||
## [NEW_VERSION] — YYYY-MM-DD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **Feature name:** Description (#PR)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **Fix name:** Description (#PR)
|
||||
|
||||
### 🛠️ Maintenance
|
||||
|
||||
- **Item:** Description
|
||||
|
||||
---
|
||||
|
||||
## [PREVIOUS_VERSION] — YYYY-MM-DD
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
The date must be today's date in `YYYY-MM-DD` format.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Sync Version Across All Files
|
||||
|
||||
### 7. Update workspace package.json files and openapi.yaml
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
|
||||
# Update docs/reference/openapi.yaml version
|
||||
sed -i "s/ version: .*/ version: $VERSION/" docs/reference/openapi.yaml
|
||||
echo "✓ docs/reference/openapi.yaml → $VERSION"
|
||||
|
||||
# Update workspace packages (open-sse, electron)
|
||||
for dir in electron open-sse; do
|
||||
if [ -d "$dir" ] && [ -f "$dir/package.json" ]; then
|
||||
(cd "$dir" && npm version "$VERSION" --no-git-tag-version --allow-same-version > /dev/null)
|
||||
echo "✓ $dir/package.json → $VERSION"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "✓ All workspace packages synced to $VERSION"
|
||||
```
|
||||
|
||||
### 8. Update llm.txt version references
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
OLD_VERSION_PATTERN='[0-9]\+\.[0-9]\+\.[0-9]\+'
|
||||
|
||||
# Update "Current version:" line
|
||||
sed -i "s/\*\*Current version:\*\* $OLD_VERSION_PATTERN/**Current version:** $VERSION/" llm.txt
|
||||
|
||||
# Update "Key Features (vX.Y.Z)" header
|
||||
sed -i "s/## Key Features (v$OLD_VERSION_PATTERN)/## Key Features (v$VERSION)/" llm.txt
|
||||
|
||||
echo "✓ llm.txt → $VERSION"
|
||||
```
|
||||
|
||||
### 9. Regenerate lock file
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute
|
||||
npm install
|
||||
echo "✓ Lock file regenerated"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Update Root Documentation
|
||||
|
||||
Based on the CHANGELOG entries generated in Phase 2, review and update these root-level files if relevant changes warrant updates:
|
||||
|
||||
### 10. Review and update root documentation files
|
||||
|
||||
For each file below, read the current content and determine if the CHANGELOG entries require any updates. Only modify files where substantive changes have occurred:
|
||||
|
||||
| File | When to update |
|
||||
| ----------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `README.md` | New providers, major features, stats changes (test count, provider count), badges, installation instructions, feature table |
|
||||
| `AGENTS.md` | Architecture changes, new modules, new commands, new providers, new services/handlers/executors |
|
||||
| `CONTRIBUTING.md` | Dev workflow changes, new tooling, test infrastructure changes |
|
||||
| `SECURITY.md` | Security fixes, new auth mechanisms, vulnerability disclosures |
|
||||
| `llm.txt` | Provider count changes, new features, architecture changes |
|
||||
|
||||
**Update rules:**
|
||||
|
||||
- **README.md**: Update provider count, test count, feature highlights table, badges if any numbers changed. If a new provider was added, add it to the provider table. If a major feature was added, add it to the features section.
|
||||
- **AGENTS.md**: If new architecture components (handlers, executors, services, DB modules) were added, update the Architecture section. If new commands were added, update the Build/Test table.
|
||||
- **SECURITY.md**: Add new vulnerability fixes or security improvements to the relevant section.
|
||||
- **llm.txt**: Update provider count, feature list, version references.
|
||||
|
||||
### 11. Review and update docs/ files (excluding i18n/)
|
||||
|
||||
For each file in `docs/` (excluding `docs/i18n/`), review if CHANGELOG changes affect it:
|
||||
|
||||
| File | When to update |
|
||||
| --------------------------------------------- | ------------------------------------------------------------------ |
|
||||
| `docs/reference/API_REFERENCE.md` | New API endpoints, changed request/response formats |
|
||||
| `docs/architecture/ARCHITECTURE.md` | New modules, new services, changed data flow |
|
||||
| `docs/architecture/CODEBASE_DOCUMENTATION.md` | New files, architectural changes, module reorganization |
|
||||
| `docs/architecture/REPOSITORY_MAP.md` | New folders / files / one-line descriptions |
|
||||
| `docs/reference/CLI-TOOLS.md` | New CLI tool integrations, config format changes |
|
||||
| `docs/guides/USER_GUIDE.md` | UX changes, new dashboard pages, settings changes |
|
||||
| `docs/reference/PROVIDER_REFERENCE.md` | New providers (regenerate via `scripts/gen-provider-reference.ts`) |
|
||||
| `docs/frameworks/MCP-SERVER.md` | New MCP tools, changed tool signatures, scope changes |
|
||||
| `docs/frameworks/A2A-SERVER.md` | New A2A skills, protocol changes |
|
||||
| `docs/frameworks/AGENT_PROTOCOLS_GUIDE.md` | New external agent protocols supported |
|
||||
| `docs/frameworks/CLOUD_AGENT.md` | Cloud agent additions (codex-cloud, devin, jules) or API changes |
|
||||
| `docs/architecture/AUTHZ_GUIDE.md` | New route classifications, policy changes |
|
||||
| `docs/security/GUARDRAILS.md` | New guardrails registered, priority/order changes |
|
||||
| `docs/security/COMPLIANCE.md` | Audit log / retention / no-log policy changes |
|
||||
| `docs/frameworks/SKILLS.md` | Skill framework / registry / built-in skill changes |
|
||||
| `docs/frameworks/MEMORY.md` | Memory pipeline / extraction / injection / Qdrant changes |
|
||||
| `docs/frameworks/EVALS.md` | Evaluation framework changes, new evaluators |
|
||||
| `docs/frameworks/WEBHOOKS.md` | New webhook events, payload schema changes |
|
||||
| `docs/routing/REASONING_REPLAY.md` | Reasoning capture/replay pipeline changes |
|
||||
| `docs/routing/AUTO-COMBO.md` | Routing changes, new strategies, scoring weight changes |
|
||||
| `docs/architecture/RESILIENCE_GUIDE.md` | Circuit breaker / cooldown / lockout behavior changes |
|
||||
| `docs/security/STEALTH_GUIDE.md` | TLS / CLI fingerprint changes |
|
||||
| `docs/ops/TUNNELS_GUIDE.md` | Cloudflare tunnel feature changes |
|
||||
| `docs/guides/ELECTRON_GUIDE.md` | Electron build / signing / packaging changes |
|
||||
| `docs/guides/TROUBLESHOOTING.md` | New known issues, resolved problems |
|
||||
| `docs/ops/RELEASE_CHECKLIST.md` | Process changes |
|
||||
| `docs/ops/COVERAGE_PLAN.md` | Coverage gate adjustments, target metrics |
|
||||
| `docs/reference/openapi.yaml` | Already updated in step 7 |
|
||||
|
||||
**Only update files where the CHANGELOG entries directly affect the documented content.** Do NOT update files just to bump a version number — only when the documented behavior, features, or architecture has actually changed.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Verify
|
||||
|
||||
### 12. Run lint check
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### 13. Run tests
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute
|
||||
npm test
|
||||
```
|
||||
|
||||
### 14. Verify version sync across all files
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
echo "Expected version: $VERSION"
|
||||
echo ""
|
||||
|
||||
echo "--- package.json ---"
|
||||
grep '"version"' package.json | head -1
|
||||
|
||||
echo "--- open-sse/package.json ---"
|
||||
grep '"version"' open-sse/package.json | head -1
|
||||
|
||||
echo "--- electron/package.json ---"
|
||||
[ -f electron/package.json ] && grep '"version"' electron/package.json | head -1
|
||||
|
||||
echo "--- docs/reference/openapi.yaml ---"
|
||||
grep " version:" docs/reference/openapi.yaml | head -1
|
||||
|
||||
echo "--- llm.txt ---"
|
||||
grep "Current version:" llm.txt
|
||||
|
||||
echo "--- CHANGELOG.md (first versioned entry) ---"
|
||||
grep "^## \[" CHANGELOG.md | head -2
|
||||
```
|
||||
|
||||
### 15. 🛑 STOP — Present Summary to User
|
||||
|
||||
**STOP** and present a summary to the user including:
|
||||
|
||||
- Old version → New version
|
||||
- CHANGELOG entries generated
|
||||
- Files modified
|
||||
- Test results
|
||||
- Any documentation updates made
|
||||
|
||||
**Wait for the user to confirm before committing.**
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Commit (only after user approval)
|
||||
|
||||
### 16. Stage and commit
|
||||
|
||||
// turbo-all
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute
|
||||
git add -A
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
git commit -m "chore(release): bump to v$VERSION — changelog, docs, version sync"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- This workflow does **NOT** create tags, releases, or deploy. Use `/generate-release` for the full release cycle after this.
|
||||
- This workflow does **NOT** update `docs/i18n/` translations. Translation updates are handled manually or via release tooling — the `/update-i18n` command does not currently exist as a Claude Code slash command.
|
||||
- The CHANGELOG generation is based on git commits since the last tag. If there are no new commits, the workflow should inform the user and stop.
|
||||
- Always verify the generated CHANGELOG entries make sense — raw commit messages may need rewriting for clarity.
|
||||
- If the version was already bumped (e.g. you're on a `release/vX.Y.Z` branch), skip the `npm version` step and use the existing version.
|
||||
|
||||
## Version Touchpoints Checklist
|
||||
|
||||
| File | Field/Pattern |
|
||||
| ----------------------------- | ----------------------------------------------------------- |
|
||||
| `package.json` | `"version": "X.Y.Z"` |
|
||||
| `open-sse/package.json` | `"version": "X.Y.Z"` |
|
||||
| `electron/package.json` | `"version": "X.Y.Z"` |
|
||||
| `docs/reference/openapi.yaml` | `version: X.Y.Z` |
|
||||
| `llm.txt` | `**Current version:** X.Y.Z` and `## Key Features (vX.Y.Z)` |
|
||||
| `CHANGELOG.md` | `## [X.Y.Z] — YYYY-MM-DD` |
|
||||
BIN
.codegraph/codegraph.db
Normal file
BIN
.codegraph/codegraph.db
Normal file
Binary file not shown.
@@ -30,3 +30,97 @@ npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Test suites
|
||||
tests
|
||||
test-results
|
||||
playwright-report
|
||||
blob-report
|
||||
|
||||
# Documentation
|
||||
# Issue #2348: The Dashboard Docs viewer reads markdown from `/app/docs` at
|
||||
# runtime. The previous `docs/*` block hid every file except openapi.yaml,
|
||||
# so the in-product help screen failed with ENOENT for every page.
|
||||
# We now keep the English markdown tree but drop the bulky assets
|
||||
# (translations, screenshots, raster diagrams) that account for ~45 MB
|
||||
# of the ~50 MB docs directory. The Docs viewer reads the default-locale
|
||||
# (English) sources at runtime, so translations are not required in the
|
||||
# container image.
|
||||
docs/i18n/**
|
||||
docs/screenshots/**
|
||||
docs/diagrams/exported/**
|
||||
docs/diagrams/**/*.png
|
||||
docs/diagrams/**/*.jpg
|
||||
docs/diagrams/**/*.jpeg
|
||||
docs/diagrams/**/*.gif
|
||||
docs/diagrams/**/*.webp
|
||||
docs/diagrams/**/*.svg
|
||||
# Note: `*.md` matches the root only (Go filepath.Match does not cross /),
|
||||
# so nested docs/**/*.md is implicitly kept without a re-include rule.
|
||||
*.md
|
||||
!README.md
|
||||
|
||||
# Electron (separate build)
|
||||
electron
|
||||
|
||||
# VS Code extension (separate project)
|
||||
vscode-extension
|
||||
|
||||
# Build artifacts
|
||||
*.tgz
|
||||
*.AppImage
|
||||
*.deb
|
||||
*.rpm
|
||||
|
||||
# Package manager lock (bun)
|
||||
bun.lock
|
||||
|
||||
# Agent config
|
||||
.agents
|
||||
.gemini
|
||||
|
||||
# Misc
|
||||
llm.txt
|
||||
images
|
||||
clipr
|
||||
omnirouteCloud
|
||||
omnirouteSite
|
||||
|
||||
# Temporary/Scratch Folders
|
||||
_*
|
||||
|
||||
# CI/CD and Version Control (that are not actual code)
|
||||
.github
|
||||
.husky
|
||||
.omc
|
||||
|
||||
# Test Configs and Reports
|
||||
playwright.config.ts
|
||||
vitest*.ts
|
||||
audit-report.json
|
||||
sonar-project.properties
|
||||
|
||||
# Deployment Configs
|
||||
docker-compose*.yml
|
||||
fly.toml
|
||||
|
||||
# Consistent with .gitignore
|
||||
.DS_Store
|
||||
.idea/
|
||||
.config/
|
||||
.data/
|
||||
.omnivscodeagent/
|
||||
*.sqlite-*
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
security-analysis/
|
||||
.analysis/
|
||||
antigravity-manager-analysis/
|
||||
.sisyphus/
|
||||
.plans/
|
||||
app.__qa_backup/
|
||||
.app-build-backup-*/
|
||||
.gitnexus
|
||||
.worktrees
|
||||
.next-playwright/
|
||||
cloud/
|
||||
|
||||
1283
.env.example
1283
.env.example
File diff suppressed because it is too large
Load Diff
2
.github/CODEOWNERS
vendored
Normal file
2
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
* @diegosouzapw
|
||||
|
||||
171
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
171
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,171 @@
|
||||
name: Bug Report
|
||||
description: Report a bug or unexpected behavior in OmniRoute
|
||||
title: "[BUG] "
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to report a bug. Please fill out the sections below so we can reproduce and fix the issue.
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: OmniRoute Version
|
||||
description: "Run `omniroute --version` or check the left sidebar in the dashboard."
|
||||
placeholder: "e.g. 3.7.9"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: install-method
|
||||
attributes:
|
||||
label: Installation Method
|
||||
options:
|
||||
- npm (global)
|
||||
- Docker / Docker Compose
|
||||
- Electron desktop app
|
||||
- Built from source
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: Operating System
|
||||
options:
|
||||
- Windows
|
||||
- macOS
|
||||
- Linux
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: os-version
|
||||
attributes:
|
||||
label: OS Version
|
||||
placeholder: "e.g. Windows 11 25H2, macOS 26.5, Ubuntu 26.04"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: node-version
|
||||
attributes:
|
||||
label: Node.js Version
|
||||
description: "Run `node --version`. Skip if using Docker."
|
||||
placeholder: "e.g. 24.15.0"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: provider
|
||||
attributes:
|
||||
label: Provider(s) Involved
|
||||
description: "Which AI provider(s) does this affect?"
|
||||
placeholder: "e.g. Antigravity, OpenRouter, Ollama, Qwen"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: model
|
||||
attributes:
|
||||
label: Model(s) Involved
|
||||
placeholder: "e.g. claude-opus-4-7, gpt-5.5, gemini-3.1-pro"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: client-tool
|
||||
attributes:
|
||||
label: Client Tool
|
||||
description: "Which tool are you using OmniRoute with?"
|
||||
placeholder: "e.g. Claude Code, Cursor, Roo Code, OpenClaw, Gemini CLI, cURL"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: "A clear description of what the bug is."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: "Step-by-step instructions to reproduce the behavior."
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '...'
|
||||
3. See error
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: "What did you expect to happen?"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: Actual Behavior
|
||||
description: "What actually happened?"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: test-impact
|
||||
attributes:
|
||||
label: Test Impact
|
||||
description: "What automated test coverage should exist for this bug?"
|
||||
options:
|
||||
- Needs a new unit test
|
||||
- Needs a new integration test
|
||||
- Needs a new e2e test
|
||||
- Existing automated test already fails
|
||||
- Unsure
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Error Logs / Output
|
||||
description: "Paste any relevant error messages, logs, or terminal output. This will be automatically formatted as code."
|
||||
render: shell
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: "If applicable, add screenshots to help explain the problem. Please also include the text of any error messages above — screenshots alone are not searchable."
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: "Any other context about the problem (e.g. proxy config, number of accounts, network setup)."
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: validation-plan
|
||||
attributes:
|
||||
label: Validation Plan
|
||||
description: "Which commands or tests should prove this bug is fixed?"
|
||||
placeholder: |
|
||||
Example:
|
||||
- node --import tsx --test tests/unit/my-file.test.ts
|
||||
- npm run test:coverage
|
||||
validations:
|
||||
required: false
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Question / Help
|
||||
url: https://github.com/diegosouzapw/OmniRoute/discussions
|
||||
about: For questions or help with setup, please use GitHub Discussions instead of opening an issue.
|
||||
96
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
96
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
name: Feature Request
|
||||
description: Suggest a new feature or improvement for OmniRoute
|
||||
title: "[Feature] "
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for suggesting a feature! Please describe the problem you're trying to solve and how you'd like it to work.
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem / Use Case
|
||||
description: "What problem does this feature solve? Why do you need it?"
|
||||
placeholder: "I'm trying to ... but currently ..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Proposed Solution
|
||||
description: "How would you like this to work?"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Alternatives Considered
|
||||
description: "Have you considered any workarounds or alternative approaches?"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: acceptance
|
||||
attributes:
|
||||
label: Acceptance Criteria
|
||||
description: "Describe the concrete behaviors or outcomes that should be validated before this is considered done."
|
||||
placeholder: |
|
||||
Example:
|
||||
- API route returns 200 with valid payload
|
||||
- Unit coverage added for the new branch
|
||||
- Existing integrations remain green
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: area
|
||||
attributes:
|
||||
label: Area
|
||||
description: "Which part of OmniRoute does this relate to?"
|
||||
multiple: true
|
||||
options:
|
||||
- Dashboard / UI
|
||||
- Proxy / Routing
|
||||
- Provider Support
|
||||
- CLI Tools Integration
|
||||
- OAuth / Authentication
|
||||
- Analytics / Usage Tracking
|
||||
- Docker / Deployment
|
||||
- Documentation
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: provider
|
||||
attributes:
|
||||
label: Related Provider(s)
|
||||
description: "If this relates to specific providers, list them."
|
||||
placeholder: "e.g. Antigravity, OpenRouter, Ollama"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: "Any other context, mockups, or references."
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: test-plan
|
||||
attributes:
|
||||
label: Expected Test Plan
|
||||
description: "Which automated tests or coverage changes should accompany this work?"
|
||||
placeholder: |
|
||||
Example:
|
||||
- Add unit tests for open-sse/services/combo.ts
|
||||
- Extend integration coverage for /api/v1/models
|
||||
- Keep npm run test:coverage at 60%+
|
||||
validations:
|
||||
required: false
|
||||
73
.github/ISSUE_TEMPLATE/test_coverage_task.yml
vendored
Normal file
73
.github/ISSUE_TEMPLATE/test_coverage_task.yml
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
name: Test Coverage Task
|
||||
description: Create a focused coverage-improvement issue tied to concrete files and targets
|
||||
title: "[Coverage] "
|
||||
labels: ["test", "coverage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Use this template for scoped coverage work. Keep it tied to specific files, measurable targets, and the gate that must stay green.
|
||||
|
||||
- type: input
|
||||
id: baseline
|
||||
attributes:
|
||||
label: Current Coverage Baseline
|
||||
description: "Paste the current overall or file-level coverage number that justifies this task."
|
||||
placeholder: "e.g. Lines 79.00%, Branches 72.85%, open-sse/handlers/chatCore.ts = 67.22%"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: scope
|
||||
attributes:
|
||||
label: Target Files Or Modules
|
||||
description: "List the concrete source files or directories that this task will cover."
|
||||
placeholder: |
|
||||
Example:
|
||||
- open-sse/handlers/chatCore.ts
|
||||
- open-sse/services/combo.ts
|
||||
- tests/integration/chat-pipeline.test.ts
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: scenarios
|
||||
attributes:
|
||||
label: Missing Scenarios
|
||||
description: "Describe the specific behaviors, branches, or failure paths that are currently uncovered."
|
||||
placeholder: |
|
||||
Example:
|
||||
- Upstream timeout path
|
||||
- Empty tool_calls normalization
|
||||
- Fallback route after first provider failure
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: target
|
||||
attributes:
|
||||
label: Coverage Target
|
||||
description: "Set the expected target for this task."
|
||||
placeholder: "e.g. Raise open-sse/handlers to 80%+ lines and keep global gate >= 60%"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: validation
|
||||
attributes:
|
||||
label: Validation Commands
|
||||
description: "List the commands that must pass before this issue can be closed."
|
||||
placeholder: |
|
||||
Example:
|
||||
- node --import tsx --test tests/unit/my-suite.test.ts
|
||||
- npm run test:coverage
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: notes
|
||||
attributes:
|
||||
label: Notes
|
||||
description: "Optional context, blockers, or dependencies."
|
||||
validations:
|
||||
required: false
|
||||
15
.github/copilot-instructions.md
vendored
Normal file
15
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# OmniRoute PR and Coverage Instructions
|
||||
|
||||
- Treat `npm run test:coverage` as a required gate for PR work.
|
||||
- The repository minimum is `60%` for statements, lines, functions, and branches.
|
||||
- If a PR changes production code in `src/`, `open-sse/`, `electron/`, or `bin/`, it must include automated tests in the same PR.
|
||||
- When reviewing or updating a PR, if the report shows missing tests or coverage below `60%`, do not stop after reporting the problem. Add or update tests in the PR first, rerun the coverage gate, and only then ask for confirmation.
|
||||
- Prefer the smallest test layer that proves the behavior:
|
||||
- unit tests first
|
||||
- integration tests when multiple modules or DB state are involved
|
||||
- e2e only when the behavior is truly UI or workflow-dependent
|
||||
- For bug issues, try to encode the reproduction as an automated test before or alongside the fix.
|
||||
- In the final PR report, include:
|
||||
- the commands you ran
|
||||
- the changed test files
|
||||
- the final coverage result
|
||||
16
.github/dependabot.yml
vendored
16
.github/dependabot.yml
vendored
@@ -29,3 +29,19 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/electron"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
commit-message:
|
||||
prefix: "deps"
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
commit-message:
|
||||
prefix: "deps"
|
||||
|
||||
30
.github/pull_request_template.md
vendored
Normal file
30
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
## Summary
|
||||
|
||||
- Describe the user-facing or operational change.
|
||||
|
||||
## Related Issues
|
||||
|
||||
- Closes #
|
||||
- Related to #
|
||||
|
||||
## Validation
|
||||
|
||||
- [ ] `npm run lint`
|
||||
- [ ] `npm run test:unit`
|
||||
- [ ] `npm run test:coverage`
|
||||
- [ ] Coverage is still `>= 60%` for statements, lines, functions, and branches
|
||||
- [ ] SonarQube PR analysis is green or any remaining issues are explicitly documented below
|
||||
|
||||
## Tests Added Or Updated
|
||||
|
||||
- List every changed or added automated test file.
|
||||
- If no production code changed, state that here.
|
||||
|
||||
## Coverage Notes
|
||||
|
||||
- If this PR changes `src/`, `open-sse/`, `electron/`, or `bin/`, explain which tests cover the change.
|
||||
- If coverage moved down in any touched file, explain why and what follow-up task will recover it.
|
||||
|
||||
## Reviewer Notes
|
||||
|
||||
- Call out any risky areas, migrations, feature flags, or manual validation that reviewers should know about.
|
||||
65
.github/workflows/build-fork.yml
vendored
Normal file
65
.github/workflows/build-fork.yml
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
name: Publish Fork Image to GHCR
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
IMAGE_NAME: ghcr.io/kang-heewon/omniroute
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and Push Fork Image
|
||||
if: github.repository == 'kang-heewon/OmniRoute'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
type=sha,prefix=sha-
|
||||
type=ref,event=tag
|
||||
labels: |
|
||||
org.opencontainers.image.title=omniroute
|
||||
org.opencontainers.image.description=Unified AI proxy/router — fork image
|
||||
org.opencontainers.image.url=https://github.com/kang-heewon/OmniRoute
|
||||
org.opencontainers.image.source=https://github.com/kang-heewon/OmniRoute
|
||||
org.opencontainers.image.licenses=MIT
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
target: runner-base
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
560
.github/workflows/ci.yml
vendored
560
.github/workflows/ci.yml
vendored
@@ -5,6 +5,8 @@ on:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -13,6 +15,11 @@ concurrency:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
CI_NODE_VERSION: "24"
|
||||
CI_NODE_24_VERSION: "24"
|
||||
CI_NODE_26_VERSION: "26"
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
@@ -21,71 +28,283 @@ jobs:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run check:node-runtime
|
||||
- run: npm run audit:deps
|
||||
- run: npm run lint
|
||||
- run: npm run check:cycles
|
||||
- run: npm run check:route-validation:t06
|
||||
- run: npm run check:any-budget:t11
|
||||
- run: npm run check:docs-sync
|
||||
- run: npm run typecheck:core
|
||||
# typecheck:noimplicit:core is a forward-looking gate (noImplicitAny).
|
||||
# Run informationally for now — many pre-existing call sites still need
|
||||
# explicit annotations; track in a dedicated follow-up.
|
||||
- run: npm run typecheck:noimplicit:core
|
||||
continue-on-error: true
|
||||
|
||||
security:
|
||||
name: Security Audit
|
||||
docs-sync-strict:
|
||||
name: Docs Sync (Strict)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- name: Dependency audit
|
||||
run: npm audit --audit-level=high --omit=dev
|
||||
- name: Check for known vulnerabilities
|
||||
run: npx is-my-node-vulnerable
|
||||
continue-on-error: true
|
||||
- run: npm run check:docs-all
|
||||
- name: i18n translation drift (warn)
|
||||
run: node scripts/i18n/check-translation-drift.mjs --warn
|
||||
|
||||
i18n-ui-coverage:
|
||||
name: i18n UI Coverage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: node scripts/i18n/check-ui-keys-coverage.mjs --threshold=65
|
||||
|
||||
i18n-matrix:
|
||||
name: Build language matrix
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
langs: ${{ steps.langs.outputs.langs }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- id: langs
|
||||
run: |
|
||||
LANG_DIR="src/i18n/messages"
|
||||
LANGS=$(ls "$LANG_DIR"/*.json | xargs -n1 basename | sed 's/.json$//' | grep -v '^en$' | jq -R . | jq -s . | jq -c .)
|
||||
echo "langs=${LANGS}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
i18n:
|
||||
name: i18n Validation
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
lang: ${{ fromJson(needs.i18n-matrix.outputs.langs) }}
|
||||
needs: i18n-matrix
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Validate ${{ matrix.lang }}
|
||||
run: |
|
||||
python3 scripts/i18n/validate_translation.py quick -l '${{ matrix.lang }}' > result.txt
|
||||
|
||||
- name: Upload result
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: i18n-${{ matrix.lang }}
|
||||
path: result.txt
|
||||
|
||||
pr-test-policy:
|
||||
name: PR Test Policy
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
- name: Fetch base branch
|
||||
run: git fetch --no-tags origin "${GITHUB_BASE_REF}" --depth=1
|
||||
- name: Validate source changes include tests
|
||||
run: node scripts/check/check-pr-test-policy.mjs --summary-file .artifacts/pr-test-policy.md
|
||||
- name: Publish PR test policy summary
|
||||
if: always()
|
||||
run: |
|
||||
if [ -f .artifacts/pr-test-policy.md ]; then
|
||||
cat .artifacts/pr-test-policy.md >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20, 22]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run check:node-runtime
|
||||
- run: npm run build
|
||||
|
||||
test-unit:
|
||||
name: Unit Tests
|
||||
package-artifact:
|
||||
name: Package Artifact
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
env:
|
||||
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run check:node-runtime
|
||||
- run: npm run build:cli
|
||||
- run: npm run check:pack-artifact
|
||||
|
||||
electron-package-smoke:
|
||||
name: Electron Package Smoke
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
needs: build
|
||||
env:
|
||||
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: "false"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run check:node-runtime
|
||||
- run: npm run build
|
||||
- name: Install Electron dependencies
|
||||
working-directory: electron
|
||||
run: npm install --no-audit --no-fund
|
||||
- name: Pack Electron app
|
||||
working-directory: electron
|
||||
run: npm run pack
|
||||
- name: Smoke packaged Electron app
|
||||
env:
|
||||
ELECTRON_SMOKE_TIMEOUT_MS: 60000
|
||||
run: xvfb-run -a npm run electron:smoke:packaged
|
||||
|
||||
test-unit:
|
||||
name: Unit Tests (${{ matrix.shard }}/4)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
needs: build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node-version: [20, 22]
|
||||
shard: [1, 2, 3, 4]
|
||||
env:
|
||||
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
|
||||
API_KEY_SECRET: ci-test-api-key-secret-long
|
||||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run test:unit
|
||||
- run: npm run check:node-runtime
|
||||
- run: node --import tsx --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/4 tests/unit/*.test.ts
|
||||
|
||||
node-24-compat:
|
||||
name: Node 24 Compatibility (${{ matrix.shard }}/2)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
needs: build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2]
|
||||
env:
|
||||
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
|
||||
API_KEY_SECRET: ci-test-api-key-secret-long
|
||||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_24_VERSION }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run check:node-runtime
|
||||
- run: npm run build
|
||||
- run: node --import tsx --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/2 tests/unit/*.test.ts
|
||||
|
||||
node-26-compat:
|
||||
name: Node 26 Compatibility (${{ matrix.shard }}/2)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
needs: build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2]
|
||||
env:
|
||||
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
|
||||
API_KEY_SECRET: ci-test-api-key-secret-long
|
||||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_26_VERSION }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run check:node-runtime
|
||||
- run: npm run build
|
||||
- run: node --import tsx --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/2 tests/unit/*.test.ts
|
||||
|
||||
test-coverage-shard:
|
||||
name: Coverage Shard (${{ matrix.shard }}/4)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
needs: build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4]
|
||||
env:
|
||||
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
|
||||
API_KEY_SECRET: ci-test-api-key-secret-long
|
||||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run check:node-runtime
|
||||
- name: Run c8 over shard ${{ matrix.shard }}/4
|
||||
run: |
|
||||
npx c8 \
|
||||
--reporter=json \
|
||||
--output-dir=coverage-shard \
|
||||
--exclude=tests/** \
|
||||
--exclude=**/*.test.* \
|
||||
node --import tsx --test --test-force-exit --test-concurrency=4 \
|
||||
--test-shard=${{ matrix.shard }}/4 tests/unit/*.test.ts
|
||||
- name: Upload raw shard coverage
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: coverage-shard-${{ matrix.shard }}
|
||||
path: coverage-shard/
|
||||
if-no-files-found: warn
|
||||
|
||||
test-coverage:
|
||||
name: Coverage
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
timeout-minutes: 10
|
||||
needs: test-coverage-shard
|
||||
if: ${{ always() && needs.test-coverage-shard.result == 'success' }}
|
||||
env:
|
||||
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
|
||||
API_KEY_SECRET: ci-test-api-key-secret-long
|
||||
@@ -93,49 +312,216 @@ jobs:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run test:coverage
|
||||
- name: Check coverage threshold
|
||||
- name: Download all shard coverage
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
pattern: coverage-shard-*
|
||||
path: coverage-shards/
|
||||
merge-multiple: true
|
||||
- name: Merge + report + gate
|
||||
run: |
|
||||
echo "Coverage report generated. Check output for threshold compliance."
|
||||
mkdir -p coverage
|
||||
npx c8 report \
|
||||
--temp-directory coverage-shards \
|
||||
--reports-dir coverage \
|
||||
--reporter=text-summary \
|
||||
--reporter=html \
|
||||
--reporter=json-summary \
|
||||
--reporter=lcov \
|
||||
--exclude=tests/** \
|
||||
--exclude=**/*.test.* \
|
||||
--check-coverage \
|
||||
--statements 75 --lines 75 --functions 75 --branches 70
|
||||
- name: Build coverage summary
|
||||
if: always()
|
||||
run: |
|
||||
mkdir -p coverage
|
||||
if [ -f coverage/coverage-summary.json ]; then
|
||||
node scripts/check/test-report-summary.mjs \
|
||||
--input coverage/coverage-summary.json \
|
||||
--output coverage/coverage-report.md \
|
||||
--threshold 60
|
||||
else
|
||||
printf '%s\n' \
|
||||
'# Coverage Report' \
|
||||
'' \
|
||||
'Coverage summary JSON was not generated. Inspect the Coverage job logs.' \
|
||||
> coverage/coverage-report.md
|
||||
fi
|
||||
cat coverage/coverage-report.md >> "$GITHUB_STEP_SUMMARY"
|
||||
- name: Upload coverage artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: coverage-report
|
||||
path: |
|
||||
coverage/coverage-summary.json
|
||||
coverage/lcov.info
|
||||
coverage/coverage-report.md
|
||||
if-no-files-found: warn
|
||||
|
||||
sonarqube:
|
||||
name: SonarQube
|
||||
runs-on: ubuntu-latest
|
||||
needs: test-coverage
|
||||
if: ${{ always() && needs.test-coverage.result == 'success' }}
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: coverage-report
|
||||
path: .
|
||||
- name: Explain SonarQube skip
|
||||
if: ${{ github.event_name != 'pull_request' || env.SONAR_TOKEN == '' || env.SONAR_HOST_URL == '' }}
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" != "pull_request" ]; then
|
||||
echo "SonarQube scan skipped on non-PR events to keep main pushes governed by repository CI gates." >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
echo "SonarQube scan skipped because SONAR_TOKEN or SONAR_HOST_URL is not configured." >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
- name: SonarQube Scan
|
||||
if: ${{ github.event_name == 'pull_request' && env.SONAR_TOKEN != '' && env.SONAR_HOST_URL != '' }}
|
||||
uses: SonarSource/sonarqube-scan-action@v8
|
||||
env:
|
||||
SONAR_TOKEN: ${{ env.SONAR_TOKEN }}
|
||||
SONAR_HOST_URL: ${{ env.SONAR_HOST_URL }}
|
||||
|
||||
coverage-pr-comment:
|
||||
name: PR Coverage Comment
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }}
|
||||
needs:
|
||||
- pr-test-policy
|
||||
- test-coverage
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Download coverage artifact
|
||||
if: ${{ needs.test-coverage.result != 'cancelled' }}
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: coverage-report
|
||||
path: .
|
||||
- name: Prepare PR coverage comment
|
||||
env:
|
||||
COVERAGE_RESULT: ${{ needs.test-coverage.result }}
|
||||
POLICY_RESULT: ${{ needs.pr-test-policy.result }}
|
||||
run: |
|
||||
mkdir -p .artifacts
|
||||
{
|
||||
echo "<!-- omniroute-coverage-report -->"
|
||||
echo "## CI Coverage Report"
|
||||
echo ""
|
||||
echo "- Coverage job: \`${COVERAGE_RESULT}\`"
|
||||
echo "- PR test policy: \`${POLICY_RESULT}\`"
|
||||
echo ""
|
||||
if [ -f coverage/coverage-report.md ]; then
|
||||
cat coverage/coverage-report.md
|
||||
else
|
||||
echo "Coverage artifact was not available for this run."
|
||||
fi
|
||||
if [ "${POLICY_RESULT}" = "failure" ]; then
|
||||
echo ""
|
||||
echo "## PR Test Policy"
|
||||
echo ""
|
||||
echo "This PR changes production code in \`src/\`, \`open-sse/\`, \`electron/\`, or \`bin/\` without accompanying automated tests."
|
||||
fi
|
||||
} > .artifacts/pr-coverage-comment.md
|
||||
- uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const fs = require("fs");
|
||||
const marker = "<!-- omniroute-coverage-report -->";
|
||||
const body = fs.readFileSync(".artifacts/pr-coverage-comment.md", "utf8");
|
||||
const { owner, repo } = context.repo;
|
||||
const issue_number = context.issue.number;
|
||||
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
const existing = comments.find((comment) => comment.body?.includes(marker));
|
||||
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: existing.id,
|
||||
body,
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
test-e2e:
|
||||
name: E2E Tests
|
||||
name: E2E Tests (${{ matrix.shard }}/6)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
needs: build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4, 5, 6]
|
||||
env:
|
||||
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
|
||||
API_KEY_SECRET: ci-test-api-key-secret-long
|
||||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||||
OMNIROUTE_PLAYWRIGHT_SKIP_BUILD: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run check:node-runtime
|
||||
- run: npx playwright install --with-deps chromium
|
||||
- run: npm run build
|
||||
- run: npm run test:e2e
|
||||
- run: npx playwright test tests/e2e/*.spec.ts --shard=${{ matrix.shard }}/6
|
||||
|
||||
test-integration:
|
||||
name: Integration Tests
|
||||
name: Integration Tests (${{ matrix.shard }}/2)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
needs: build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2]
|
||||
env:
|
||||
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
|
||||
API_KEY_SECRET: ci-test-api-key-secret-long
|
||||
INITIAL_PASSWORD: ci-test-password-for-integration
|
||||
DATA_DIR: /tmp/omniroute-ci
|
||||
DATA_DIR: /tmp/omniroute-ci-${{ matrix.shard }}
|
||||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run test:integration
|
||||
- run: npm run check:node-runtime
|
||||
- run: node --import tsx --test --test-force-exit --test-concurrency=1 --test-shard=${{ matrix.shard }}/2 tests/integration/*.test.ts
|
||||
|
||||
test-security:
|
||||
name: Security Tests
|
||||
@@ -144,11 +530,123 @@ jobs:
|
||||
env:
|
||||
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
|
||||
API_KEY_SECRET: ci-test-api-key-secret-long
|
||||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run check:node-runtime
|
||||
- run: npm run test:security
|
||||
|
||||
ci-summary:
|
||||
name: CI Dashboard
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
needs:
|
||||
- lint
|
||||
- docs-sync-strict
|
||||
- i18n-ui-coverage
|
||||
- i18n
|
||||
- pr-test-policy
|
||||
|
||||
- build
|
||||
- package-artifact
|
||||
- electron-package-smoke
|
||||
- test-unit
|
||||
- node-24-compat
|
||||
- node-26-compat
|
||||
- test-coverage
|
||||
- sonarqube
|
||||
- coverage-pr-comment
|
||||
- test-e2e
|
||||
- test-integration
|
||||
- test-security
|
||||
steps:
|
||||
- name: Download i18n results
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: i18n-*
|
||||
path: results
|
||||
merge-multiple: true
|
||||
|
||||
- name: Generate dashboard
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
run: |
|
||||
status() {
|
||||
case "$1" in
|
||||
success) echo "🟢 PASS" ;;
|
||||
failure) echo "🔴 FAIL" ;;
|
||||
cancelled) echo "⚫ CANCELLED" ;;
|
||||
skipped) echo "⚪ SKIPPED" ;;
|
||||
*) echo "🟡 UNKNOWN" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
echo "# 🚀 CI Dashboard" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
echo "## 🧱 Core Checks" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Job | Status |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "|-----|--------|" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Lint | $(status '${{ needs.lint.result }}') |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Docs Sync (Strict) | $(status '${{ needs.docs-sync-strict.result }}') |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| i18n UI Coverage | $(status '${{ needs.i18n-ui-coverage.result }}') |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| PR Test Policy | $(status '${{ needs.pr-test-policy.result }}') |" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
echo "| SonarQube | $(status '${{ needs.sonarqube.result }}') |" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "## 🏗️ Build" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Job | Status |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "|-----|--------|" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Build Matrix | $(status '${{ needs.build.result }}') |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Package Artifact | $(status '${{ needs.package-artifact.result }}') |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Electron Package Smoke | $(status '${{ needs.electron-package-smoke.result }}') |" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "## 🧪 Tests" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Suite | Status |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "|-------|--------|" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Unit | $(status '${{ needs.test-unit.result }}') |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Node 24 Compatibility | $(status '${{ needs.node-24-compat.result }}') |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Node 26 Compatibility | $(status '${{ needs.node-26-compat.result }}') |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Coverage | $(status '${{ needs.test-coverage.result }}') |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| PR Coverage Comment | $(status '${{ needs.coverage-pr-comment.result }}') |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| E2E | $(status '${{ needs.test-e2e.result }}') |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Integration | $(status '${{ needs.test-integration.result }}') |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Security Tests | $(status '${{ needs.test-security.result }}') |" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "## 🌍 Translations" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
total=0
|
||||
langs=0
|
||||
|
||||
if [ -d results ]; then
|
||||
for file in results/*.txt; do
|
||||
[ -f "$file" ] || continue
|
||||
val=$(sed -r 's/\x1B\[[0-9;]*[mK]//g' "$file" | grep "Untranslated:" | awk '{print $2}')
|
||||
val=${val:-0}
|
||||
total=$((total + val))
|
||||
langs=$((langs + 1))
|
||||
done
|
||||
fi
|
||||
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Metric | Value |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "|--------|------|" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Languages checked | $langs |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Total untranslated | $total |" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
if [ "$total" -gt 0 ]; then
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "⚠️ **Translations need attention**" >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "✅ **All translations complete**" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
49
.github/workflows/claude.yml
vendored
Normal file
49
.github/workflows/claude.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
name: Claude Code
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened, assigned]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
claude:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
actions: read # Required for Claude to read CI results on PRs
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
|
||||
# This is an optional setting that allows Claude to read CI results on PRs
|
||||
additional_permissions: |
|
||||
actions: read
|
||||
|
||||
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
|
||||
# prompt: 'Update the pull request description to include a summary of changes.'
|
||||
|
||||
# Optional: Add claude_args to customize behavior and configuration
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://code.claude.com/docs/en/cli-reference for available options
|
||||
# claude_args: '--allowed-tools Bash(gh pr *)'
|
||||
3
.github/workflows/deploy-vps.yml
vendored
3
.github/workflows/deploy-vps.yml
vendored
@@ -6,6 +6,9 @@ on:
|
||||
types: [completed]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
if: >-
|
||||
|
||||
35
.github/workflows/docker-publish.yml
vendored
35
.github/workflows/docker-publish.yml
vendored
@@ -1,11 +1,25 @@
|
||||
name: Publish to Docker Hub
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- "v*"
|
||||
paths-ignore:
|
||||
- ".github/workflows/**"
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version tag to build (e.g. 2.6.0)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
@@ -16,6 +30,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/v{0}', inputs.version) || '' }}
|
||||
|
||||
- name: Set up QEMU (for multi-arch builds)
|
||||
uses: docker/setup-qemu-action@v4
|
||||
@@ -29,11 +45,22 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Extract version from release tag
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract version from release tag or input
|
||||
id: version
|
||||
run: |
|
||||
VERSION="${GITHUB_REF_NAME}"
|
||||
VERSION="${VERSION#v}"
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
VERSION="${{ inputs.version }}"
|
||||
else
|
||||
VERSION="${GITHUB_REF_NAME}"
|
||||
VERSION="${VERSION#v}"
|
||||
fi
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "Publishing Docker image: $IMAGE_NAME:$VERSION"
|
||||
|
||||
@@ -47,6 +74,8 @@ jobs:
|
||||
tags: |
|
||||
${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}
|
||||
${{ env.IMAGE_NAME }}:latest
|
||||
ghcr.io/diegosouzapw/omniroute:${{ steps.version.outputs.version }}
|
||||
ghcr.io/diegosouzapw/omniroute:latest
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
no-cache: false
|
||||
|
||||
51
.github/workflows/electron-release.yml
vendored
51
.github/workflows/electron-release.yml
vendored
@@ -13,6 +13,8 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
@@ -69,13 +71,11 @@ jobs:
|
||||
deb_ext: .deb
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
cache: npm
|
||||
|
||||
- name: Cache node_modules
|
||||
@@ -88,6 +88,18 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
env:
|
||||
NPM_CONFIG_LEGACY_PEER_DEPS: true
|
||||
|
||||
- name: Sanitize Windows home directory
|
||||
if: runner.os == 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
# The default USERPROFILE contains junction points (Application Data)
|
||||
# that cause EPERM errors during Next.js standalone build glob scans.
|
||||
# Create a clean temp profile directory to avoid this.
|
||||
mkdir -p "$RUNNER_TEMP/home"
|
||||
echo "USERPROFILE=$RUNNER_TEMP/home" >> $GITHUB_ENV
|
||||
|
||||
- name: Build Next.js standalone
|
||||
env:
|
||||
@@ -121,6 +133,23 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: npm run build:${{ matrix.target }}
|
||||
|
||||
- name: Smoke packaged Electron app
|
||||
if: matrix.platform != 'linux'
|
||||
# Windows CI: requestSingleInstanceLock() fails due to USERPROFILE
|
||||
# sanitization needed for the build step. Smoke is best-effort there.
|
||||
continue-on-error: ${{ matrix.platform == 'windows' }}
|
||||
env:
|
||||
ELECTRON_SMOKE_TIMEOUT_MS: 60000
|
||||
ELECTRON_SMOKE_STREAM_LOGS: "1"
|
||||
run: npm run electron:smoke:packaged
|
||||
|
||||
- name: Smoke packaged Electron app (Linux)
|
||||
if: matrix.platform == 'linux'
|
||||
env:
|
||||
ELECTRON_SMOKE_TIMEOUT_MS: 60000
|
||||
ELECTRON_SMOKE_STREAM_LOGS: "1"
|
||||
run: xvfb-run -a npm run electron:smoke:packaged
|
||||
|
||||
- name: Collect installers
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -184,7 +213,7 @@ jobs:
|
||||
run: ls -la release-assets/
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
tag_name: ${{ needs.validate.outputs.version }}
|
||||
draft: false
|
||||
@@ -201,3 +230,13 @@ jobs:
|
||||
release-assets/*.source.zip
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
publish-npm:
|
||||
name: Publish to npm
|
||||
needs: [validate, release]
|
||||
uses: ./.github/workflows/npm-publish.yml
|
||||
with:
|
||||
version: ${{ needs.validate.outputs.version }}
|
||||
tag: latest
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
70
.github/workflows/lock-released-branch.yml
vendored
Normal file
70
.github/workflows/lock-released-branch.yml
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
name: Lock released branch
|
||||
|
||||
# When a GitHub Release is published (e.g. tag v3.8.2), make the matching
|
||||
# release/<tag> branch read-only so no further commits can land on a shipped
|
||||
# version. Uses branch protection's lock_branch + enforce_admins so the freeze
|
||||
# applies even to repository admins. To reopen a branch later:
|
||||
# gh api -X DELETE repos/<owner>/<repo>/branches/release/<tag>/protection
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Tag of the released version (e.g. v3.8.2)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
# Editing branch protection requires the administration scope.
|
||||
administration: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lock-branch:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Lock release/<tag> branch
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.BRANCH_LOCK_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
# release event -> github.event.release.tag_name; manual -> inputs.tag
|
||||
TAG: ${{ github.event.release.tag_name || inputs.tag }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [ -z "${TAG}" ]; then
|
||||
echo "::error::No tag provided; cannot determine release branch."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BRANCH="release/${TAG}"
|
||||
echo "Target branch: ${BRANCH} (repo: ${REPO})"
|
||||
|
||||
# Skip gracefully if the release branch does not exist.
|
||||
if ! gh api "repos/${REPO}/branches/${BRANCH}" >/dev/null 2>&1; then
|
||||
echo "::warning::Branch ${BRANCH} not found — nothing to lock."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Applying lock_branch protection to ${BRANCH}..."
|
||||
gh api -X PUT "repos/${REPO}/branches/${BRANCH}/protection" --input - <<'JSON'
|
||||
{
|
||||
"required_status_checks": null,
|
||||
"enforce_admins": true,
|
||||
"required_pull_request_reviews": null,
|
||||
"restrictions": null,
|
||||
"lock_branch": true,
|
||||
"allow_force_pushes": false,
|
||||
"allow_deletions": false
|
||||
}
|
||||
JSON
|
||||
|
||||
LOCKED=$(gh api "repos/${REPO}/branches/${BRANCH}/protection" \
|
||||
--jq '.lock_branch.enabled')
|
||||
if [ "${LOCKED}" != "true" ]; then
|
||||
echo "::error::Failed to confirm lock on ${BRANCH} (lock_branch=${LOCKED})."
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ ${BRANCH} is now locked (read-only)."
|
||||
141
.github/workflows/npm-publish.yml
vendored
141
.github/workflows/npm-publish.yml
vendored
@@ -3,10 +3,42 @@ name: Publish to npm
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to publish (e.g. 2.9.5 or 3.0.0-rc.15)"
|
||||
required: true
|
||||
type: string
|
||||
tag:
|
||||
description: "npm dist-tag (latest / next)"
|
||||
required: false
|
||||
default: "latest"
|
||||
type: choice
|
||||
options:
|
||||
- latest
|
||||
- next
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to publish (without v prefix)"
|
||||
required: true
|
||||
type: string
|
||||
tag:
|
||||
description: "npm dist-tag (latest / next)"
|
||||
required: false
|
||||
default: "latest"
|
||||
type: string
|
||||
secrets:
|
||||
NPM_TOKEN:
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
packages: write
|
||||
|
||||
env:
|
||||
NPM_PUBLISH_NODE_VERSION: "24"
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
@@ -19,33 +51,122 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: ${{ env.NPM_PUBLISH_NODE_VERSION }}
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
- name: Install dependencies (skip scripts to avoid heavy build)
|
||||
run: npm install --ignore-scripts --no-audit --no-fund
|
||||
|
||||
- name: Sync version from release tag
|
||||
- name: Resolve version and dist-tag
|
||||
id: resolve
|
||||
run: |
|
||||
VERSION="${GITHUB_REF_NAME}"
|
||||
# Remove 'v' prefix if present (v2.1.0 -> 2.1.0)
|
||||
VERSION="${{ inputs.version }}"
|
||||
TAG="${{ inputs.tag }}"
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
if [ "${{ github.event_name }}" = "release" ]; then
|
||||
VERSION="${GITHUB_REF_NAME}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Strip v prefix if present
|
||||
VERSION="${VERSION#v}"
|
||||
npm version "$VERSION" --no-git-tag-version --allow-same-version
|
||||
echo "Publishing version: $VERSION"
|
||||
|
||||
# Default dist-tag logic
|
||||
if [ -z "$TAG" ]; then
|
||||
if [[ "$VERSION" == *-* ]]; then
|
||||
TAG="next"
|
||||
else
|
||||
TAG="latest"
|
||||
fi
|
||||
fi
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||
echo "📦 Publishing omniroute@$VERSION with tag=$TAG"
|
||||
|
||||
- name: Sync package.json version
|
||||
run: |
|
||||
npm version "${{ steps.resolve.outputs.version }}" --no-git-tag-version --allow-same-version
|
||||
|
||||
- name: Build CLI bundle (standalone app)
|
||||
env:
|
||||
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
|
||||
run: node scripts/prepublish.mjs
|
||||
run: npm run build:cli
|
||||
|
||||
- name: Validate npm package artifact
|
||||
run: npm run check:pack-artifact
|
||||
|
||||
- name: Publish to npm
|
||||
run: |
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
VERSION="${{ steps.resolve.outputs.version }}"
|
||||
TAG="${{ steps.resolve.outputs.tag }}"
|
||||
# Check if this version is already published — skip instead of failing with E403
|
||||
if npm view "omniroute@${VERSION}" version --silent 2>/dev/null | grep -q "^${VERSION}$"; then
|
||||
echo "️⚠️ Version ${VERSION} is already published on npm — skipping."
|
||||
echo "⚠️ Version ${VERSION} is already published on npm — skipping."
|
||||
exit 0
|
||||
fi
|
||||
npm publish --access public
|
||||
if [ "$TAG" = "latest" ]; then
|
||||
npm publish --access public
|
||||
else
|
||||
npm publish --access public --tag "$TAG"
|
||||
fi
|
||||
echo "✅ Published omniroute@$VERSION (tag: $TAG)"
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Publish to GitHub Packages
|
||||
run: |
|
||||
VERSION="${{ steps.resolve.outputs.version }}"
|
||||
TAG="${{ steps.resolve.outputs.tag }}"
|
||||
|
||||
echo "Configuring for GitHub Packages..."
|
||||
echo "//npm.pkg.github.com/:_authToken=${{ secrets.GITHUB_TOKEN }}" > .npmrc
|
||||
npm pkg set name="@diegosouzapw/omniroute"
|
||||
|
||||
if [ "$TAG" = "latest" ]; then
|
||||
npm publish --registry=https://npm.pkg.github.com || echo "⚠️ Version ${VERSION} might already be published on GitHub."
|
||||
else
|
||||
npm publish --registry=https://npm.pkg.github.com --tag "$TAG" || echo "⚠️ Version ${VERSION} might already be published on GitHub."
|
||||
fi
|
||||
echo "✅ Action finished for GitHub Packages"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
publish-opencode-plugin:
|
||||
runs-on: ubuntu-latest
|
||||
environment: NPM_TOKEN
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.NPM_PUBLISH_NODE_VERSION }}
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
- name: Install plugin dependencies
|
||||
working-directory: "@omniroute/opencode-plugin"
|
||||
run: npm install --no-audit --no-fund
|
||||
|
||||
- name: Build plugin
|
||||
working-directory: "@omniroute/opencode-plugin"
|
||||
run: npm run clean && npm run build
|
||||
|
||||
- name: Test plugin
|
||||
working-directory: "@omniroute/opencode-plugin"
|
||||
run: npm test
|
||||
|
||||
- name: Publish @omniroute/opencode-plugin to npm
|
||||
working-directory: "@omniroute/opencode-plugin"
|
||||
run: |
|
||||
PKG_VERSION=$(node -p "require('./package.json').version")
|
||||
PKG_NAME=$(node -p "require('./package.json').name")
|
||||
if npm view "${PKG_NAME}@${PKG_VERSION}" version --silent 2>/dev/null | grep -q "^${PKG_VERSION}$"; then
|
||||
echo "⚠️ ${PKG_NAME}@${PKG_VERSION} is already published on npm — skipping."
|
||||
exit 0
|
||||
fi
|
||||
npm publish --access public --ignore-scripts
|
||||
echo "✅ Published ${PKG_NAME}@${PKG_VERSION}"
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
62
.github/workflows/opencode-plugin-ci.yml
vendored
Normal file
62
.github/workflows/opencode-plugin-ci.yml
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
name: opencode-plugin CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, release/v3.8.2]
|
||||
paths:
|
||||
- "@omniroute/opencode-plugin/**"
|
||||
pull_request:
|
||||
branches: [main, release/v3.8.2]
|
||||
paths:
|
||||
- "@omniroute/opencode-plugin/**"
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: "@omniroute/opencode-plugin"
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test (Node ${{ matrix.node }})
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node: ["22", "24"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: npm
|
||||
cache-dependency-path: "@omniroute/opencode-plugin/package-lock.json"
|
||||
- run: npm install --no-audit --no-fund
|
||||
- run: npm run build
|
||||
- run: npm test
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: npm
|
||||
cache-dependency-path: "@omniroute/opencode-plugin/package-lock.json"
|
||||
- run: npm install --no-audit --no-fund
|
||||
- run: npm run build
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: opencode-plugin-dist
|
||||
path: "@omniroute/opencode-plugin/dist"
|
||||
retention-days: 7
|
||||
61
.github/workflows/opencode-provider-ci.yml
vendored
Normal file
61
.github/workflows/opencode-provider-ci.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
name: opencode-provider CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, release/v3.8.0]
|
||||
paths:
|
||||
- "@omniroute/opencode-provider/**"
|
||||
pull_request:
|
||||
branches: [main, release/v3.8.0]
|
||||
paths:
|
||||
- "@omniroute/opencode-provider/**"
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: "@omniroute/opencode-provider"
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test (Node ${{ matrix.node }})
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node: ["20", "22", "24"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: npm
|
||||
cache-dependency-path: "@omniroute/opencode-provider/package-lock.json"
|
||||
- run: npm ci
|
||||
- run: npm test
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: npm
|
||||
cache-dependency-path: "@omniroute/opencode-provider/package-lock.json"
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: opencode-provider-dist
|
||||
path: "@omniroute/opencode-provider/dist"
|
||||
retention-days: 7
|
||||
119
.gitignore
vendored
119
.gitignore
vendored
@@ -5,6 +5,23 @@
|
||||
omnirouteCloud/
|
||||
omnirouteSite/
|
||||
|
||||
# Memory Bank and Cursor rules (local-only AI agent context)
|
||||
memory-bank/
|
||||
.cursor/rules/core.mdc
|
||||
.cursor/rules/memory-bank.mdc
|
||||
|
||||
# Claude Code local state — runtime files only; shared commands at .claude/commands/ are tracked
|
||||
.claude/scheduled_tasks.lock
|
||||
.claude/scheduled_tasks/
|
||||
.claude/sessions/
|
||||
.claude/state.json
|
||||
|
||||
# Root-level underscore-prefixed directories (private/draft — never commit)
|
||||
/_*/
|
||||
|
||||
# Draft features documentation (internal only)
|
||||
docs/new-features/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
/.pnp
|
||||
@@ -14,9 +31,12 @@ node_modules/
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
.data/
|
||||
.next-playwright/
|
||||
|
||||
# testing
|
||||
coverage/
|
||||
coverage**
|
||||
|
||||
# next.js
|
||||
.next/
|
||||
@@ -50,42 +70,15 @@ next-env.d.ts
|
||||
|
||||
# data and logs
|
||||
data/
|
||||
.data/
|
||||
logs/*
|
||||
test_output.log
|
||||
|
||||
# analysis directories (generated, not tracked)
|
||||
.analysis/
|
||||
antigravity-manager-analysis/
|
||||
|
||||
# docs (allow specific tracked files)
|
||||
docs/*
|
||||
!docs/ARCHITECTURE.md
|
||||
!docs/CODEBASE_DOCUMENTATION.md
|
||||
!docs/CONTRIBUTING.md
|
||||
!docs/USER_GUIDE.md
|
||||
!docs/API_REFERENCE.md
|
||||
!docs/TROUBLESHOOTING.md
|
||||
!docs/EXECUTION_CONTEXT_PROVIDER_SYNC.md
|
||||
!docs/TASK_NEBIUS_BACKEND_ENABLEMENT.md
|
||||
!docs/frontend-backend-provider-gap-report.md
|
||||
!docs/openapi.yaml
|
||||
!docs/RELEASE_CHECKLIST.md
|
||||
!docs/PLANO-IMPLANTACAO.md
|
||||
!docs/TASKS.md
|
||||
!docs/FASE-*.md
|
||||
!docs/adr/
|
||||
!docs/cli-tools/
|
||||
!docs/planning/
|
||||
!docs/improvement-plans/
|
||||
!docs/api/
|
||||
!docs/VM_DEPLOYMENT_GUIDE.md
|
||||
!docs/FEATURES.md
|
||||
!docs/screenshots/
|
||||
!docs/i18n/
|
||||
!docs/i18n/**
|
||||
!docs/A2A-SERVER.md
|
||||
!docs/AUTO-COMBO.md
|
||||
!docs/MCP-SERVER.md
|
||||
!docs/CLI-TOOLS.md
|
||||
.sisyphus/
|
||||
.plans/
|
||||
|
||||
# open-sse tests
|
||||
open-sse/test/*
|
||||
@@ -94,10 +87,12 @@ open-sse/test/*
|
||||
.github/instructions/codacy.instructions.md
|
||||
|
||||
# Playwright
|
||||
.playwright-mcp/
|
||||
test-results/
|
||||
playwright-report/
|
||||
blob-report/
|
||||
cloud/
|
||||
.tmp/
|
||||
|
||||
# Security Analysis (standalone project with own git)
|
||||
security-analysis/
|
||||
@@ -106,17 +101,22 @@ security-analysis/
|
||||
clipr/
|
||||
app.log
|
||||
*.tgz
|
||||
.gh-discussions.json
|
||||
deploy.sh
|
||||
docker-compose.minimal.yml
|
||||
|
||||
|
||||
# Backup directories
|
||||
app.__qa_backup/
|
||||
.app-build-backup-*/
|
||||
backup/
|
||||
|
||||
# Production standalone build (created by scripts/prepublish.mjs)
|
||||
# Conflicts with Next.js App Router detection in dev (root app/ shadows src/app/)
|
||||
# npm publish still includes it via package.json "files" field
|
||||
/app/
|
||||
|
||||
# Electron (subproject dependency lock and build artifacts)
|
||||
electron/package-lock.json
|
||||
# Electron
|
||||
electron/dist-electron/
|
||||
electron/node_modules/
|
||||
icon.iconset/
|
||||
@@ -128,3 +128,56 @@ vscode-extension/
|
||||
*.sqlite-shm
|
||||
*.sqlite-wal
|
||||
*.sqlite-journal
|
||||
|
||||
# Compiled npm-package build artifact (not source, should not be in git)
|
||||
/app
|
||||
|
||||
# IDEA
|
||||
.idea/
|
||||
|
||||
# Local OpenCode agent config
|
||||
.config/
|
||||
|
||||
# Empty/dangling files
|
||||
typescript
|
||||
|
||||
# Gemini Antigravity agent data
|
||||
.gemini/
|
||||
|
||||
# Superpowers plans/specs (internal tooling, not project code)
|
||||
docs/superpowers/
|
||||
|
||||
# GitNexus local index
|
||||
.gitnexus
|
||||
.worktrees
|
||||
bin/omniroute.mjs
|
||||
|
||||
# Consistent with .dockerignore / .npmignore
|
||||
.omc/
|
||||
audit-report.json
|
||||
bun.lock
|
||||
|
||||
# Private environment variables for .http-client
|
||||
http-client.private.env.json
|
||||
|
||||
# Note: _ideia/ (feature-triage drafts) is fully covered by the /_*/ rule above
|
||||
# and kept as a separate local-only git repo. Never committed to OmniRoute.
|
||||
|
||||
# i18n audit artifact (generated by scripts/i18n/audit-dashboard-pages.mjs)
|
||||
scripts/i18n/_audit.json
|
||||
scripts/i18n/_pending-keys.json
|
||||
|
||||
# Private workflow / skill / command implementations
|
||||
# These contain proprietary multi-phase logic and should not be committed
|
||||
.agents/workflows/implement-features-ag.md
|
||||
.agents/workflows/port-upstream-features-ag.md
|
||||
.agents/workflows/port-upstream-issues-ag.md
|
||||
.agents/skills/implement-features/
|
||||
.claude/commands/implement-features-cc.md
|
||||
.claude/commands/port-upstream-features-cc.md
|
||||
.claude/commands/port-upstream-issues-cc.md
|
||||
.claude/worktrees/
|
||||
.codegraph/
|
||||
|
||||
# Fumadocs generated source
|
||||
.source/
|
||||
|
||||
24
.husky/pre-commit
Normal file → Executable file
24
.husky/pre-commit
Normal file → Executable file
@@ -1,2 +1,24 @@
|
||||
#!/usr/bin/env sh
|
||||
if ! command -v npx >/dev/null 2>&1; then
|
||||
echo "⚠️ npx not found in PATH — skipping pre-commit hooks"
|
||||
echo " Run 'npm run lint && npm run check:any-budget:t11' manually before pushing."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
npx lint-staged
|
||||
node scripts/check-docs-sync.mjs
|
||||
node scripts/check/check-docs-sync.mjs
|
||||
npm run check:any-budget:t11
|
||||
|
||||
# Strict env-doc sync (FASE 2)
|
||||
node scripts/check/check-env-doc-sync.mjs
|
||||
|
||||
# CLI i18n consistency check — all t() keys must exist in en.json (FASE 8.3)
|
||||
node scripts/check/check-cli-i18n.mjs
|
||||
|
||||
# i18n docs drift advisory (FASE 5) — warn-only on pre-commit; CI enforces strict.
|
||||
node scripts/i18n/check-translation-drift.mjs --warn || \
|
||||
echo "⚠️ i18n drift detected. Run 'npm run i18n:run' to update locale mirrors."
|
||||
|
||||
# i18n UI coverage advisory (FASE 6) — pre-commit warns; CI enforces strict.
|
||||
node scripts/i18n/check-ui-keys-coverage.mjs --threshold=80 || \
|
||||
echo "⚠️ UI i18n coverage below 80% for at least one locale."
|
||||
|
||||
8
.husky/pre-push
Executable file
8
.husky/pre-push
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env sh
|
||||
#if ! command -v npm >/dev/null 2>&1; then
|
||||
# echo "⚠️ npm not found in PATH — skipping pre-push hooks"
|
||||
# echo " Run 'npm test' manually before pushing."
|
||||
# exit 0
|
||||
#fi
|
||||
|
||||
#npm run test:unit
|
||||
219
.i18n-state.json
Normal file
219
.i18n-state.json
Normal file
@@ -0,0 +1,219 @@
|
||||
{
|
||||
"sources": {
|
||||
"CLAUDE.md": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"locales": {
|
||||
"pt-BR": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "301b997e936b1d476e6094042666b96b33f42a4372fd2d9ccf904aacbfd7f023",
|
||||
"updated_at": "2026-05-22T20:13:39.165Z"
|
||||
},
|
||||
"az": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "c26844ec50b2abfbb002767fb5c6c9c3982ec65789435bbc134bf1a7b50bf84a",
|
||||
"updated_at": "2026-05-22T20:13:39.166Z"
|
||||
},
|
||||
"bn": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "0b1416ec3b5af8cc6415d12a9ff12ce078acca4a7bb52a7422ebde3b6ae22832",
|
||||
"updated_at": "2026-05-22T20:13:39.166Z"
|
||||
},
|
||||
"ar": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "bc747c5ea2a387dd37dea9dc1337d9734f2ec0da38a7134573d9743e3f4d2aef",
|
||||
"updated_at": "2026-05-22T20:13:39.167Z"
|
||||
},
|
||||
"cs": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "e37dce45820b53be9d3d5ead08cadb8d13e4c77597b3009bb0be4e485917f5c6",
|
||||
"updated_at": "2026-05-22T20:13:39.167Z"
|
||||
},
|
||||
"da": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "645c382bebe0931eaad6747e33667fd094b92b3f4042549b953db798de6b1f46",
|
||||
"updated_at": "2026-05-22T20:13:39.168Z"
|
||||
},
|
||||
"de": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "b6558f82eb67676baf4d78946a8d1e1b808f7763e5ebaf9f57948cbd1641dc0f",
|
||||
"updated_at": "2026-05-22T20:13:39.168Z"
|
||||
},
|
||||
"es": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "afb2a2dfa74a74c3105b0ac70181d33f5eefd201efe7edb6aba0740864639fe5",
|
||||
"updated_at": "2026-05-22T20:13:39.169Z"
|
||||
},
|
||||
"fa": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "58966769aeb71d23c55cf4aeb452f2bbe32036df8fba1d7596f3e861897d507b",
|
||||
"updated_at": "2026-05-22T20:13:39.169Z"
|
||||
},
|
||||
"fi": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "04cf72a3b370edcb3d54361c6cc250b3af227ba183376827006c7998839740aa",
|
||||
"updated_at": "2026-05-22T20:13:39.169Z"
|
||||
},
|
||||
"fr": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "8dca6de87c88e04396f1139ac8b6328d188defaa5abc99eeb4026f53de772ba1",
|
||||
"updated_at": "2026-05-22T20:13:39.170Z"
|
||||
},
|
||||
"he": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "0308a2c8c5a6b6261474a76c160f3c0658c75f56a633a57bd7300b83ccb32c9d",
|
||||
"updated_at": "2026-05-22T20:13:39.170Z"
|
||||
},
|
||||
"hi": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "a3e902c3b1812a41ccb14c9f16d2cf2e42b8c005a5d557043e582d48eda024c4",
|
||||
"updated_at": "2026-05-22T20:13:39.170Z"
|
||||
},
|
||||
"gu": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "1bc2419db39c7c7960b065222a3e3990e9e53c4d3427ec02422e3e506aa53733",
|
||||
"updated_at": "2026-05-22T20:13:39.171Z"
|
||||
},
|
||||
"hu": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "ec30f54810f8b5b84e3ac8bf7bc8d05acc8616d71801b672ec02f0bc225cf607",
|
||||
"updated_at": "2026-05-22T20:13:39.171Z"
|
||||
},
|
||||
"id": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "aebd8fdad7ec4857aa1b958cda37f59681846525207d71d98d729775031afb94",
|
||||
"updated_at": "2026-05-22T20:13:39.172Z"
|
||||
},
|
||||
"in": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "ac70a1282877f8c4f792e9235f7a6fbce3cbec4a424ce4947074bb210fe12d67",
|
||||
"updated_at": "2026-05-22T20:13:39.172Z"
|
||||
},
|
||||
"it": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "260f10dd121441d08e15a8d511789d8f8f7a788995305ab5e8dc6d0c1a3db06e",
|
||||
"updated_at": "2026-05-22T20:13:39.173Z"
|
||||
},
|
||||
"ja": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "83871c13e99d13d7928409b2aef182f55fe36fbb2425f8fa4bd0df0466afed28",
|
||||
"updated_at": "2026-05-22T20:13:39.173Z"
|
||||
},
|
||||
"mr": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "0c35c5261dd3b6c6b729d14653f95cc112f3ed9829d2d41b61e5a960abc68556",
|
||||
"updated_at": "2026-05-22T20:13:39.173Z"
|
||||
},
|
||||
"ko": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "4ea4079b37d90e90e9a32d0da290e04e38bcd6426512b50ca7be7139354bc329",
|
||||
"updated_at": "2026-05-22T20:13:39.174Z"
|
||||
},
|
||||
"ms": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "0e8031cd987766a69afc7aaf7c3560a57736d37e7238ddccfb848d6fbeb3f212",
|
||||
"updated_at": "2026-05-22T20:13:39.174Z"
|
||||
},
|
||||
"nl": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "27e554c3a9b86db1f04539fcac18d3e75e6599d4de9f5ac0cf789a136b5737d4",
|
||||
"updated_at": "2026-05-22T20:13:39.174Z"
|
||||
},
|
||||
"phi": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "f955af44cc0e8e87a2a7b12313f0dfad9aae1a50d7855e74c8155df3601279ad",
|
||||
"updated_at": "2026-05-22T20:13:39.175Z"
|
||||
},
|
||||
"no": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "de4e3d940ae485c85da13db4471fcc68926ec53e660d92633f01293c5db0a04d",
|
||||
"updated_at": "2026-05-22T20:13:39.175Z"
|
||||
},
|
||||
"pl": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "de5be8961e5184431404d0021e31c3ef0857f7439fbd1c71ddc0c6828bed86bb",
|
||||
"updated_at": "2026-05-22T20:13:39.175Z"
|
||||
},
|
||||
"ro": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "19d98b22bc77c1870b749fad6e62a41c6ef53f3cad303d1c471fba4bf7012797",
|
||||
"updated_at": "2026-05-22T20:13:39.175Z"
|
||||
},
|
||||
"ru": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "25e0c70ec25bd24b073b9d693299c3c3d32d66a410569362719e9c9ecacd759d",
|
||||
"updated_at": "2026-05-22T20:13:39.176Z"
|
||||
},
|
||||
"pt": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "0d31647b21af967d8d4e9ecbcfc4901a66db377a6853d7b59296a2d73f0cab12",
|
||||
"updated_at": "2026-05-22T20:13:39.176Z"
|
||||
},
|
||||
"sk": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "9d5d0ce1c51da4959d1c306969bff0bf36d3a3492ccdbdbd82e4f4d951f32c8b",
|
||||
"updated_at": "2026-05-22T20:13:39.176Z"
|
||||
},
|
||||
"sw": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "11cd3dc7a605eebddf4a34c13de66c713d67adb32e4242962a65c71e5a034c7e",
|
||||
"updated_at": "2026-05-22T20:13:39.177Z"
|
||||
},
|
||||
"sv": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "93b679feb6415315ab96e08298217e66be84265a277e267ecefd25b2cef04026",
|
||||
"updated_at": "2026-05-22T20:13:39.177Z"
|
||||
},
|
||||
"ta": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "55f3df128513a1b4a8cc3f58eb84769814589d80e1c8b8f03ab0635750a76d2d",
|
||||
"updated_at": "2026-05-22T20:13:39.177Z"
|
||||
},
|
||||
"te": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "81019604dd1fb38dfda55c1b8cf5cf8243347be08221a5e1eb11e8c76e095fab",
|
||||
"updated_at": "2026-05-22T20:13:39.178Z"
|
||||
},
|
||||
"tr": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "b67efd07b179be016b388dff4b8979597c0a7b2db602629cef539dbec14913ab",
|
||||
"updated_at": "2026-05-22T20:13:39.178Z"
|
||||
},
|
||||
"th": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "a793a12dc0cba5e3641cd544f63ab23e9cfe1b80568a08770a81ffd890287eda",
|
||||
"updated_at": "2026-05-22T20:13:39.179Z"
|
||||
},
|
||||
"ur": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "6c03049aee6d85b6fd4a3bf9b89a6204e461d3f05e9cb767e36c80af796452df",
|
||||
"updated_at": "2026-05-22T20:13:39.179Z"
|
||||
},
|
||||
"vi": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "4d35c6898f98913a02551dcfbf4d3748b2461e2d8206d292f292acacf0fc7133",
|
||||
"updated_at": "2026-05-22T20:13:39.180Z"
|
||||
},
|
||||
"zh-CN": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "d3f574c244d157c1fe1b9fc86192d1565df2dc6343ac9ef63c0fd3f91d97f492",
|
||||
"updated_at": "2026-05-22T20:13:39.180Z"
|
||||
},
|
||||
"uk-UA": {
|
||||
"source_hash": "c808d01e42e9aaaede590048dc327347c9e814b152c6f45f54be6df64f4f19df",
|
||||
"target_hash": "ffe6357e38d56ba2595e8fb8e21db4ae91bf03d10fca50a2b722e0d0bb6c01d9",
|
||||
"updated_at": "2026-05-22T20:13:39.181Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
"docs/architecture/ARCHITECTURE.md": {
|
||||
"source_hash": "f9b4f17a1b0331fb5500768943438411ed88a38278381680caacc37c90bfb869",
|
||||
"locales": {
|
||||
"pt-BR": {
|
||||
"source_hash": "f9b4f17a1b0331fb5500768943438411ed88a38278381680caacc37c90bfb869",
|
||||
"target_hash": "e320c6172a88a0f3b698ba1a2e9e938424ece66625765395837bcf55cc29454e",
|
||||
"updated_at": "2026-05-22T20:13:39.183Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
.node-version
Normal file
1
.node-version
Normal file
@@ -0,0 +1 @@
|
||||
24
|
||||
65
.npmignore
65
.npmignore
@@ -3,6 +3,11 @@ data/
|
||||
**/data/
|
||||
**/db.json
|
||||
|
||||
# VS Code extension test runtime (large binary, not needed in npm package)
|
||||
app/vscode-extension/
|
||||
**/data/
|
||||
**/db.json
|
||||
|
||||
# Source code (pre-built app/ is published instead)
|
||||
src/
|
||||
open-sse/
|
||||
@@ -21,14 +26,21 @@ scripts/
|
||||
.github/
|
||||
.husky/
|
||||
.vscode/
|
||||
.agents/
|
||||
.env*
|
||||
app/.env
|
||||
app/.env*
|
||||
eslint.config.mjs
|
||||
prettier.config.mjs
|
||||
postcss.config.mjs
|
||||
next.config.mjs
|
||||
tsconfig.json
|
||||
tsconfig.typecheck-core.json
|
||||
tsconfig.typecheck-noimplicit-core.json
|
||||
playwright.config.ts
|
||||
vitest.config.ts
|
||||
next-env.d.ts
|
||||
llm.txt
|
||||
|
||||
# Docker
|
||||
docker-compose*.yml
|
||||
@@ -36,9 +48,56 @@ Dockerfile
|
||||
.dockerignore
|
||||
|
||||
# Misc
|
||||
restart.sh
|
||||
AGENTS.md
|
||||
bun.lock
|
||||
|
||||
# Build artifacts (pre-built goes inside app/)
|
||||
.next/
|
||||
node_modules/
|
||||
/.next/
|
||||
/node_modules/
|
||||
|
||||
# Ignore large binary files and other build directories
|
||||
*.tgz
|
||||
*.AppImage
|
||||
*.deb
|
||||
*.rpm
|
||||
electron/
|
||||
app/electron/
|
||||
app/vscode-extension/
|
||||
|
||||
# Subprojects
|
||||
clipr/
|
||||
omnirouteCloud/
|
||||
omnirouteSite/
|
||||
vscode-extension/
|
||||
|
||||
# Root-level underscore-prefixed directories (private/draft — never publish)
|
||||
/_*/
|
||||
app/_*/
|
||||
app/coverage/
|
||||
app/logs/
|
||||
app/tests/
|
||||
|
||||
# Consistent with .gitignore and .dockerignore
|
||||
.DS_Store
|
||||
.idea/
|
||||
.config/
|
||||
.data/
|
||||
.omnivscodeagent/
|
||||
.omc/
|
||||
*.sqlite-*
|
||||
*.tsbuildinfo
|
||||
security-analysis/
|
||||
.analysis/
|
||||
antigravity-manager-analysis/
|
||||
.sisyphus/
|
||||
.plans/
|
||||
app.__qa_backup/
|
||||
.app-build-backup-*/
|
||||
.gitnexus
|
||||
.worktrees
|
||||
.next-playwright/
|
||||
test-results/
|
||||
playwright-report/
|
||||
blob-report/
|
||||
coverage/
|
||||
@omniroute/
|
||||
|
||||
4
.npmrc
Normal file
4
.npmrc
Normal file
@@ -0,0 +1,4 @@
|
||||
# @lobehub/icons declares UI peers that are not needed by our deep icon imports.
|
||||
# Keeping peer auto-install disabled prevents npm from pulling @lobehub/ui/mermaid
|
||||
# back into the tree and reopening npm audit findings for unused packages.
|
||||
legacy-peer-deps=true
|
||||
31
.semgrep/rules/cli-no-sqlite.yaml
Normal file
31
.semgrep/rules/cli-no-sqlite.yaml
Normal file
@@ -0,0 +1,31 @@
|
||||
rules:
|
||||
- id: cli-no-sqlite-direct
|
||||
patterns:
|
||||
- pattern: new Database(...)
|
||||
paths:
|
||||
include:
|
||||
- "/bin/**"
|
||||
exclude:
|
||||
- "/bin/cli/sqlite.mjs"
|
||||
message: >
|
||||
Direct SQLite access in bin/ is banned. Use src/lib/db/* helpers or
|
||||
withRuntime() from bin/cli/runtime.mjs. See CLAUDE.md hard rule #5 and
|
||||
bin/cli/CONVENTIONS.md.
|
||||
languages: [js]
|
||||
severity: ERROR
|
||||
|
||||
- id: cli-no-raw-sql
|
||||
patterns:
|
||||
- pattern: $DB.prepare("INSERT INTO ...")
|
||||
- pattern: $DB.prepare("DELETE FROM ...")
|
||||
- pattern: $DB.prepare("UPDATE $TABLE SET ...")
|
||||
paths:
|
||||
include:
|
||||
- "/bin/**"
|
||||
exclude:
|
||||
- "/bin/cli/sqlite.mjs"
|
||||
message: >
|
||||
Raw SQL in bin/ is banned. Use src/lib/db/* helpers. See CLAUDE.md
|
||||
hard rule #5 and bin/cli/CONVENTIONS.md.
|
||||
languages: [js]
|
||||
severity: ERROR
|
||||
12
.source/browser.ts
Normal file
12
.source/browser.ts
Normal file
File diff suppressed because one or more lines are too long
8
.source/dynamic.ts
Normal file
8
.source/dynamic.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// @ts-nocheck
|
||||
import { dynamic } from 'fumadocs-mdx/runtime/dynamic';
|
||||
import * as Config from '../source.config';
|
||||
|
||||
const create = await dynamic<typeof Config, import("fumadocs-mdx/runtime/types").InternalTypeConfig & {
|
||||
DocData: {
|
||||
}
|
||||
}>(Config, {"configPath":"source.config.ts","environment":"next","outDir":".source"}, {"doc":{"passthroughs":["extractedReferences"]}});
|
||||
74
.source/server.ts
Normal file
74
.source/server.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
// @ts-nocheck
|
||||
import { default as __fd_glob_63 } from "../docs/security/meta.json?collection=docs"
|
||||
import { default as __fd_glob_62 } from "../docs/routing/meta.json?collection=docs"
|
||||
import { default as __fd_glob_61 } from "../docs/reference/openapi.yaml?collection=docs"
|
||||
import { default as __fd_glob_60 } from "../docs/reference/meta.json?collection=docs"
|
||||
import { default as __fd_glob_59 } from "../docs/ops/meta.json?collection=docs"
|
||||
import { default as __fd_glob_58 } from "../docs/guides/meta.json?collection=docs"
|
||||
import { default as __fd_glob_57 } from "../docs/frameworks/meta.json?collection=docs"
|
||||
import { default as __fd_glob_56 } from "../docs/compression/meta.json?collection=docs"
|
||||
import { default as __fd_glob_55 } from "../docs/architecture/meta.json?collection=docs"
|
||||
import { default as __fd_glob_54 } from "../docs/meta.json?collection=docs"
|
||||
import * as __fd_glob_53 from "../docs/security/STEALTH_GUIDE.md?collection=docs"
|
||||
import * as __fd_glob_52 from "../docs/security/ROUTE_GUARD_TIERS.md?collection=docs"
|
||||
import * as __fd_glob_51 from "../docs/security/PUBLIC_CREDS.md?collection=docs"
|
||||
import * as __fd_glob_50 from "../docs/security/GUARDRAILS.md?collection=docs"
|
||||
import * as __fd_glob_49 from "../docs/security/ERROR_SANITIZATION.md?collection=docs"
|
||||
import * as __fd_glob_48 from "../docs/security/COMPLIANCE.md?collection=docs"
|
||||
import * as __fd_glob_47 from "../docs/security/CLI_TOKEN_AUTH.md?collection=docs"
|
||||
import * as __fd_glob_46 from "../docs/security/CLI_TOKEN.md?collection=docs"
|
||||
import * as __fd_glob_45 from "../docs/routing/REASONING_REPLAY.md?collection=docs"
|
||||
import * as __fd_glob_44 from "../docs/routing/AUTO-COMBO.md?collection=docs"
|
||||
import * as __fd_glob_43 from "../docs/reference/PROVIDER_REFERENCE.md?collection=docs"
|
||||
import * as __fd_glob_42 from "../docs/reference/FREE_TIERS.md?collection=docs"
|
||||
import * as __fd_glob_41 from "../docs/reference/ENVIRONMENT.md?collection=docs"
|
||||
import * as __fd_glob_40 from "../docs/reference/CLI-TOOLS.md?collection=docs"
|
||||
import * as __fd_glob_39 from "../docs/reference/API_REFERENCE.md?collection=docs"
|
||||
import * as __fd_glob_38 from "../docs/ops/VM_DEPLOYMENT_GUIDE.md?collection=docs"
|
||||
import * as __fd_glob_37 from "../docs/ops/TUNNELS_GUIDE.md?collection=docs"
|
||||
import * as __fd_glob_36 from "../docs/ops/SQLITE_RUNTIME.md?collection=docs"
|
||||
import * as __fd_glob_35 from "../docs/ops/RELEASE_CHECKLIST.md?collection=docs"
|
||||
import * as __fd_glob_34 from "../docs/ops/PROXY_GUIDE.md?collection=docs"
|
||||
import * as __fd_glob_33 from "../docs/ops/FLY_IO_DEPLOYMENT_GUIDE.md?collection=docs"
|
||||
import * as __fd_glob_32 from "../docs/ops/E2E_DASHBOARD_SHAKEDOWN_v3.8.0.md?collection=docs"
|
||||
import * as __fd_glob_31 from "../docs/ops/COVERAGE_PLAN.md?collection=docs"
|
||||
import * as __fd_glob_30 from "../docs/guides/USER_GUIDE.md?collection=docs"
|
||||
import * as __fd_glob_29 from "../docs/guides/UNINSTALL.md?collection=docs"
|
||||
import * as __fd_glob_28 from "../docs/guides/TROUBLESHOOTING.md?collection=docs"
|
||||
import * as __fd_glob_27 from "../docs/guides/TERMUX_GUIDE.md?collection=docs"
|
||||
import * as __fd_glob_26 from "../docs/guides/SETUP_GUIDE.md?collection=docs"
|
||||
import * as __fd_glob_25 from "../docs/guides/PWA_GUIDE.md?collection=docs"
|
||||
import * as __fd_glob_24 from "../docs/guides/KIRO_SETUP.md?collection=docs"
|
||||
import * as __fd_glob_23 from "../docs/guides/I18N.md?collection=docs"
|
||||
import * as __fd_glob_22 from "../docs/guides/FEATURES.md?collection=docs"
|
||||
import * as __fd_glob_21 from "../docs/guides/ELECTRON_GUIDE.md?collection=docs"
|
||||
import * as __fd_glob_20 from "../docs/guides/DOCKER_GUIDE.md?collection=docs"
|
||||
import * as __fd_glob_19 from "../docs/frameworks/WEBHOOKS.md?collection=docs"
|
||||
import * as __fd_glob_18 from "../docs/frameworks/SKILLS.md?collection=docs"
|
||||
import * as __fd_glob_17 from "../docs/frameworks/OPENCODE.md?collection=docs"
|
||||
import * as __fd_glob_16 from "../docs/frameworks/MEMORY.md?collection=docs"
|
||||
import * as __fd_glob_15 from "../docs/frameworks/MCP-SERVER.md?collection=docs"
|
||||
import * as __fd_glob_14 from "../docs/frameworks/GAMIFICATION.md?collection=docs"
|
||||
import * as __fd_glob_13 from "../docs/frameworks/EVALS.md?collection=docs"
|
||||
import * as __fd_glob_12 from "../docs/frameworks/CLOUD_AGENT.md?collection=docs"
|
||||
import * as __fd_glob_11 from "../docs/frameworks/AGENT_PROTOCOLS_GUIDE.md?collection=docs"
|
||||
import * as __fd_glob_10 from "../docs/frameworks/A2A-SERVER.md?collection=docs"
|
||||
import * as __fd_glob_9 from "../docs/compression/RTK_COMPRESSION.md?collection=docs"
|
||||
import * as __fd_glob_8 from "../docs/compression/COMPRESSION_RULES_FORMAT.md?collection=docs"
|
||||
import * as __fd_glob_7 from "../docs/compression/COMPRESSION_LANGUAGE_PACKS.md?collection=docs"
|
||||
import * as __fd_glob_6 from "../docs/compression/COMPRESSION_GUIDE.md?collection=docs"
|
||||
import * as __fd_glob_5 from "../docs/compression/COMPRESSION_ENGINES.md?collection=docs"
|
||||
import * as __fd_glob_4 from "../docs/architecture/RESILIENCE_GUIDE.md?collection=docs"
|
||||
import * as __fd_glob_3 from "../docs/architecture/REPOSITORY_MAP.md?collection=docs"
|
||||
import * as __fd_glob_2 from "../docs/architecture/CODEBASE_DOCUMENTATION.md?collection=docs"
|
||||
import * as __fd_glob_1 from "../docs/architecture/AUTHZ_GUIDE.md?collection=docs"
|
||||
import * as __fd_glob_0 from "../docs/architecture/ARCHITECTURE.md?collection=docs"
|
||||
import { server } from 'fumadocs-mdx/runtime/server';
|
||||
import type * as Config from '../source.config';
|
||||
|
||||
const create = server<typeof Config, import("fumadocs-mdx/runtime/types").InternalTypeConfig & {
|
||||
DocData: {
|
||||
}
|
||||
}>({"doc":{"passthroughs":["extractedReferences"]}});
|
||||
|
||||
export const docs = await create.docs("docs", "docs", {"meta.json": __fd_glob_54, "architecture/meta.json": __fd_glob_55, "compression/meta.json": __fd_glob_56, "frameworks/meta.json": __fd_glob_57, "guides/meta.json": __fd_glob_58, "ops/meta.json": __fd_glob_59, "reference/meta.json": __fd_glob_60, "reference/openapi.yaml": __fd_glob_61, "routing/meta.json": __fd_glob_62, "security/meta.json": __fd_glob_63, }, {"architecture/ARCHITECTURE.md": __fd_glob_0, "architecture/AUTHZ_GUIDE.md": __fd_glob_1, "architecture/CODEBASE_DOCUMENTATION.md": __fd_glob_2, "architecture/REPOSITORY_MAP.md": __fd_glob_3, "architecture/RESILIENCE_GUIDE.md": __fd_glob_4, "compression/COMPRESSION_ENGINES.md": __fd_glob_5, "compression/COMPRESSION_GUIDE.md": __fd_glob_6, "compression/COMPRESSION_LANGUAGE_PACKS.md": __fd_glob_7, "compression/COMPRESSION_RULES_FORMAT.md": __fd_glob_8, "compression/RTK_COMPRESSION.md": __fd_glob_9, "frameworks/A2A-SERVER.md": __fd_glob_10, "frameworks/AGENT_PROTOCOLS_GUIDE.md": __fd_glob_11, "frameworks/CLOUD_AGENT.md": __fd_glob_12, "frameworks/EVALS.md": __fd_glob_13, "frameworks/GAMIFICATION.md": __fd_glob_14, "frameworks/MCP-SERVER.md": __fd_glob_15, "frameworks/MEMORY.md": __fd_glob_16, "frameworks/OPENCODE.md": __fd_glob_17, "frameworks/SKILLS.md": __fd_glob_18, "frameworks/WEBHOOKS.md": __fd_glob_19, "guides/DOCKER_GUIDE.md": __fd_glob_20, "guides/ELECTRON_GUIDE.md": __fd_glob_21, "guides/FEATURES.md": __fd_glob_22, "guides/I18N.md": __fd_glob_23, "guides/KIRO_SETUP.md": __fd_glob_24, "guides/PWA_GUIDE.md": __fd_glob_25, "guides/SETUP_GUIDE.md": __fd_glob_26, "guides/TERMUX_GUIDE.md": __fd_glob_27, "guides/TROUBLESHOOTING.md": __fd_glob_28, "guides/UNINSTALL.md": __fd_glob_29, "guides/USER_GUIDE.md": __fd_glob_30, "ops/COVERAGE_PLAN.md": __fd_glob_31, "ops/E2E_DASHBOARD_SHAKEDOWN_v3.8.0.md": __fd_glob_32, "ops/FLY_IO_DEPLOYMENT_GUIDE.md": __fd_glob_33, "ops/PROXY_GUIDE.md": __fd_glob_34, "ops/RELEASE_CHECKLIST.md": __fd_glob_35, "ops/SQLITE_RUNTIME.md": __fd_glob_36, "ops/TUNNELS_GUIDE.md": __fd_glob_37, "ops/VM_DEPLOYMENT_GUIDE.md": __fd_glob_38, "reference/API_REFERENCE.md": __fd_glob_39, "reference/CLI-TOOLS.md": __fd_glob_40, "reference/ENVIRONMENT.md": __fd_glob_41, "reference/FREE_TIERS.md": __fd_glob_42, "reference/PROVIDER_REFERENCE.md": __fd_glob_43, "routing/AUTO-COMBO.md": __fd_glob_44, "routing/REASONING_REPLAY.md": __fd_glob_45, "security/CLI_TOKEN.md": __fd_glob_46, "security/CLI_TOKEN_AUTH.md": __fd_glob_47, "security/COMPLIANCE.md": __fd_glob_48, "security/ERROR_SANITIZATION.md": __fd_glob_49, "security/GUARDRAILS.md": __fd_glob_50, "security/PUBLIC_CREDS.md": __fd_glob_51, "security/ROUTE_GUARD_TIERS.md": __fd_glob_52, "security/STEALTH_GUIDE.md": __fd_glob_53, });
|
||||
22
.source/source.config.mjs
Normal file
22
.source/source.config.mjs
Normal file
@@ -0,0 +1,22 @@
|
||||
// source.config.ts
|
||||
import { defineDocs, defineConfig } from "fumadocs-mdx/config";
|
||||
var docs = defineDocs({
|
||||
dir: "docs",
|
||||
docs: {
|
||||
files: [
|
||||
"./architecture/**/*.md",
|
||||
"./guides/**/*.md",
|
||||
"./reference/**/*.md",
|
||||
"./frameworks/**/*.md",
|
||||
"./routing/**/*.md",
|
||||
"./security/**/*.md",
|
||||
"./compression/**/*.md",
|
||||
"./ops/**/*.md"
|
||||
]
|
||||
}
|
||||
});
|
||||
var source_config_default = defineConfig();
|
||||
export {
|
||||
source_config_default as default,
|
||||
docs
|
||||
};
|
||||
31
.vscode/launch.json
vendored
Normal file
31
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug Dev Server",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "${env:HOME}/.nvm/versions/node/v22.22.2/bin/node",
|
||||
"program": "${workspaceFolder}/scripts/run-next.mjs",
|
||||
"args": ["dev"],
|
||||
"console": "integratedTerminal",
|
||||
"skipFiles": ["<node_internals>/**", "node_modules/**"]
|
||||
},
|
||||
{
|
||||
"name": "Debug Prod Server",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/scripts/run-next.mjs",
|
||||
"args": ["start"],
|
||||
"console": "integratedTerminal",
|
||||
"skipFiles": ["<node_internals>/**", "node_modules/**"]
|
||||
},
|
||||
{
|
||||
"name": "Attach to Running Server",
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"port": 9229,
|
||||
"skipFiles": ["<node_internals>/**", "node_modules/**"]
|
||||
}
|
||||
]
|
||||
}
|
||||
30
.vscode/settings.json
vendored
30
.vscode/settings.json
vendored
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"workbench.sideBar.location": "left",
|
||||
"css.lint.unknownAtRules": "ignore",
|
||||
"sonarlint.rules": {
|
||||
"css:S4662": {
|
||||
@@ -16,5 +17,34 @@
|
||||
"javascript:S3776": {
|
||||
"level": "off"
|
||||
}
|
||||
},
|
||||
"git.ignoreLimitWarning": true,
|
||||
// "files.exclude": {
|
||||
// "**/_references": true,
|
||||
// "**/_mono_repo": true,
|
||||
// "**/electron": true,
|
||||
// "**/node_modules": true,
|
||||
// "**/.next": true,
|
||||
// "**/coverage": true,
|
||||
// "**/omniroute-*.tgz": true,
|
||||
// "**/_tasks": true
|
||||
// },
|
||||
"files.watcherExclude": {
|
||||
"**/_references/**": true,
|
||||
"**/_mono_repo/**": true,
|
||||
"**/electron/**": true,
|
||||
"**/node_modules/**": true,
|
||||
"**/.next/**": true,
|
||||
"**/coverage/**": true,
|
||||
"**/_tasks/**": true
|
||||
},
|
||||
"search.exclude": {
|
||||
"**/_references": true,
|
||||
"**/_mono_repo": true,
|
||||
"**/electron": true,
|
||||
"**/node_modules": true,
|
||||
"**/.next": true,
|
||||
"**/coverage": true,
|
||||
"**/_tasks": true
|
||||
}
|
||||
}
|
||||
|
||||
4
@omniroute/opencode-plugin/.gitignore
vendored
Normal file
4
@omniroute/opencode-plugin/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
dist
|
||||
*.log
|
||||
.DS_Store
|
||||
21
@omniroute/opencode-plugin/LICENSE
Normal file
21
@omniroute/opencode-plugin/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 OmniRoute contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
255
@omniroute/opencode-plugin/README.md
Normal file
255
@omniroute/opencode-plugin/README.md
Normal file
@@ -0,0 +1,255 @@
|
||||
# @omniroute/opencode-plugin
|
||||
|
||||
First-class OpenCode plugin for the [OmniRoute AI Gateway](https://github.com/diegosouzapw/OmniRoute). Pulls a live model catalog from `/v1/models` (including `-low`/`-medium`/`-high`/`-thinking` variants as first-class IDs), aggregates combos via `/api/combos` using a least-common-denominator capability/limit join, sanitizes Gemini tool schemas in flight, and supports multiple side-by-side OmniRoute instances out of the box.
|
||||
|
||||
## Install
|
||||
|
||||
Once published to npm:
|
||||
|
||||
```sh
|
||||
npm install @omniroute/opencode-plugin
|
||||
```
|
||||
|
||||
Until then (or for local development), reference the built artifact directly. Either extract the package into your OpenCode plugins dir and point at the extracted `dist/index.js`:
|
||||
|
||||
```sh
|
||||
# from inside the OmniRoute repo
|
||||
cd @omniroute/opencode-plugin && npm run build && npm pack
|
||||
# then extract into ~/.config/opencode/plugins/omniroute-opencode-plugin/
|
||||
```
|
||||
|
||||
Peer dep: `@opencode-ai/plugin` (managed by your OpenCode install).
|
||||
|
||||
## Quick start (single instance)
|
||||
|
||||
```jsonc
|
||||
// opencode.json
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"plugin": [
|
||||
[
|
||||
"@omniroute/opencode-plugin",
|
||||
{
|
||||
"providerId": "omniroute",
|
||||
"baseURL": "https://or.example.com",
|
||||
},
|
||||
],
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
```sh
|
||||
opencode auth login --provider omniroute
|
||||
# prompts for the OmniRoute API key, writes to ~/.local/share/opencode/auth.json
|
||||
```
|
||||
|
||||
> ⚠ Use the `--provider` flag explicitly. `opencode auth login omniroute` is parsed as a positional `url` argument by current OC releases (≤1.15.5) and fails with `fetch() URL is invalid`. Tracked upstream.
|
||||
|
||||
Restart OpenCode. `/models` lists the full live catalog. Variants (`-low`, `-medium`, `-high`, `-thinking`) and combos appear as first-class IDs — OmniRoute is the source of truth, no client-side synthesis.
|
||||
|
||||
## Multi-instance (prod + preprod side-by-side)
|
||||
|
||||
> ⚠ OC ≤1.15.5 dedupes plugin loads by absolute module path. Two `plugin:` entries pointing at the same `dist/index.js` collapse into one (last-listed options win). Workaround: install the plugin twice into separate directories so each entry resolves to a distinct module file. v0.2.x will introduce an `instances: [...]` shape that registers N providers from a single load.
|
||||
|
||||
### Dual-install workaround (works today on OC ≤1.15.5)
|
||||
|
||||
Pack the plugin once, extract it twice into named directories, then point each `plugin:` entry at its own copy:
|
||||
|
||||
```sh
|
||||
# 1. Build + pack the plugin (run from the plugin worktree)
|
||||
cd /path/to/OmniRoute/@omniroute/opencode-plugin
|
||||
npm run build
|
||||
npm pack
|
||||
# produces omniroute-opencode-plugin-0.1.0.tgz
|
||||
|
||||
# 2. Extract one copy per OmniRoute endpoint
|
||||
mkdir -p ~/.config/opencode/plugins/omniroute-opencode-plugin-prod
|
||||
mkdir -p ~/.config/opencode/plugins/omniroute-opencode-plugin-preprod
|
||||
tar -xzf omniroute-opencode-plugin-0.1.0.tgz -C ~/.config/opencode/plugins/omniroute-opencode-plugin-prod --strip-components=1
|
||||
tar -xzf omniroute-opencode-plugin-0.1.0.tgz -C ~/.config/opencode/plugins/omniroute-opencode-plugin-preprod --strip-components=1
|
||||
```
|
||||
|
||||
Then in `~/.config/opencode/opencode.json` reference each directory by absolute path:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"plugin": [
|
||||
[
|
||||
"./plugins/omniroute-opencode-plugin-prod/dist/index.js",
|
||||
{
|
||||
"providerId": "omniroute",
|
||||
"displayName": "OmniRoute",
|
||||
"baseURL": "https://or.example.com",
|
||||
},
|
||||
],
|
||||
[
|
||||
"./plugins/omniroute-opencode-plugin-preprod/dist/index.js",
|
||||
{
|
||||
"providerId": "omniroute-preprod",
|
||||
"displayName": "OmniRoute Preprod",
|
||||
"baseURL": "https://or-preprod.example.com",
|
||||
},
|
||||
],
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
Paths are relative to `~/.config/opencode/`. Each entry now resolves to a distinct module file, so OC loads them as two separate plugin instances. Authenticate each:
|
||||
|
||||
```sh
|
||||
opencode auth login --provider omniroute
|
||||
opencode auth login --provider omniroute-preprod
|
||||
```
|
||||
|
||||
Each entry gets its own provider id, its own model picker entry, its own slot in `auth.json`, and its own TTL cache. Closures are isolated per plugin instance — no cross-talk.
|
||||
|
||||
### After publish (`@omniroute/opencode-plugin` npm)
|
||||
|
||||
Once the package is published, the dual-install becomes two `npm install --prefix` commands instead of `tar -xzf`:
|
||||
|
||||
```sh
|
||||
mkdir -p ~/.config/opencode/plugins/omniroute-opencode-plugin-prod
|
||||
mkdir -p ~/.config/opencode/plugins/omniroute-opencode-plugin-preprod
|
||||
npm install --prefix ~/.config/opencode/plugins/omniroute-opencode-plugin-prod @omniroute/opencode-plugin
|
||||
npm install --prefix ~/.config/opencode/plugins/omniroute-opencode-plugin-preprod @omniroute/opencode-plugin
|
||||
```
|
||||
|
||||
`opencode.json` paths become `./plugins/omniroute-opencode-plugin-prod/node_modules/@omniroute/opencode-plugin/dist/index.js` (and the preprod equivalent).
|
||||
|
||||
## Features
|
||||
|
||||
| Feature | What it does | Hook |
|
||||
| ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
|
||||
| Dynamic `/v1/models` | Pulls live catalog (455+ entries on prod) on each refresh, TTL-cached | `provider.models` |
|
||||
| Variants pass-through | `-low`/`-medium`/`-high`/`-thinking` ship as first-class IDs from OmniRoute (no client synthesis) | `provider.models` |
|
||||
| Combo LCD aggregation | Combos appear with intersected capabilities + min context/output across members | `provider.models` + `config` |
|
||||
| `combo/<slug>` namespace + `Combo: ` prefix | Combos surface under `combo/claude-primary` (not the upstream UUID) and the picker shows `Combo: claude-primary` so they stand apart from raw provider/model pairs | both hooks |
|
||||
| Nice names + cost | `/api/pricing/models` display names AND `/api/pricing` per-million-token cost overlaid onto the live catalog | both hooks |
|
||||
| Canonical-twin dedup + alias-fallback | `/v1/models` exposes the same upstream model under both short alias (`cc/claude-opus-4-7`) and canonical name (`claude/claude-opus-4-7`); the plugin drops the canonical twin when an alias twin exists (no duplicate rows in the picker) and reverse-maps canonical → alias to pick up enrichment for short aliases (`dg/nova-3 → Deepgram - Nova 3`) that `/api/pricing/models` only indexes by canonical | both hooks |
|
||||
| Compression pipeline tags | Combo names get tagged with their compression pipeline (e.g. `Combo: claude-primary [rtk🟡 → caveman🟠]`) when `features.compressionMetadata: true`. Intensity tokens render as a traffic-light emoji: 🟢 lite/minimal · 🟡 standard · 🟠 aggressive/full · 🔴 ultra | both hooks |
|
||||
| Provider-tag prefix | Prepend short upstream-provider label to enriched names (e.g. `Claude - Claude Opus 4.7` vs `Kiro - Claude Opus 4.7`, `GHM - GPT 5`) so same-id models routed via different upstream connections group visibly in the picker (default-on, opt-out via `features.providerTag: false`) | both hooks |
|
||||
| Usable-only filter | Filter to providers with at least one healthy connection in `/api/providers` (opt-in via `features.usableOnly`) | both hooks |
|
||||
| Disk-cache fallback | Last-known-good catalog persisted to disk; hydrates on a cold start when `/v1/models` is unreachable (default-on, opt-out via `features.diskCache: false`) | `config` |
|
||||
| Bearer injection + suffix-spoof guard | Adds `Authorization` on baseURL-matched requests only | `auth.loader.fetch` |
|
||||
| Gemini schema sanitization | Strips `$schema`/`$ref`/`additionalProperties` for `gemini-*`/`google-vertex-gemini/*` | `auth.loader.fetch` wrap |
|
||||
| Multi-instance | Each plugin entry binds to its own `providerId`; closures isolated | factory |
|
||||
| Config-hook shim | OC ≤1.15.5 fallback: writes static catalog into `config.provider[id]` (config hook is the only one that fires in `serve` mode on these versions) | `config` |
|
||||
|
||||
## Plugin options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
| --------------- | -------- | ------------------------------------------ | ---------------------------------------------------------- |
|
||||
| `providerId` | `string` | `"omniroute"` | OpenCode provider id; must be unique across plugin entries |
|
||||
| `displayName` | `string` | `"OmniRoute"` or `OmniRoute (<id>)` | Label in the OC UI |
|
||||
| `modelCacheTtl` | `number` | `300000` (5 min) | `/v1/models` TTL in ms |
|
||||
| `baseURL` | `string` | resolved from `auth.json` after `/connect` | Override OmniRoute base URL |
|
||||
| `features` | `object` | see below | Feature toggles (all opt-in/out, defaults preserve v0.1.0) |
|
||||
|
||||
### `features` block
|
||||
|
||||
Every field is optional. Defaults mirror v0.1.0 behaviour so existing `opencode.json` files do not need to change.
|
||||
|
||||
| Feature | Type | Default | What it does |
|
||||
| --------------------- | --------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `combos` | `boolean` | `true` | Discover `/api/combos` and surface them as pseudo-models with LCD capabilities. Combos are keyed under the `combo/<slug>` namespace and labelled `Combo: <name>` in the model picker so they're distinguishable from raw provider/model pairs. |
|
||||
| `enrichment` | `boolean` | `true` | Pull display names from `/api/pricing/models` AND per-million-token pricing (`input`, `output`, `cached` → `cacheRead`, `cache_creation` → `cacheWrite`) from `/api/pricing`, then overlay both onto the live catalog (so the UI shows `Claude 4.7 Opus` with `cost.input: 5`, `cost.output: 25` instead of raw IDs and zeroed cost). |
|
||||
| `compressionMetadata` | `boolean` | `false` | Pull `/api/context/combos` so combo names get tagged with their compression pipeline, e.g. `Combo: claude-primary [rtk🟡 → caveman🟠]`. Intensity tokens render as traffic-light emoji (🟢 lite/minimal · 🟡 standard · 🟠 aggressive/full · 🔴 ultra) so the picker advertises "how compressed" each combo is at a glance. |
|
||||
| `providerTag` | `boolean` | `true` | Prepend a short upstream-provider label to the enriched display name with `" - "` separator, so `cc/claude-opus-4-7 → Claude - Claude Opus 4.7` differs visibly from `kr/claude-opus-4-7 → Kiro - Claude Opus 4.7` in the OC TUI model picker. Label resolution: use `/api/pricing/models[<alias>].name` verbatim when ≤8 chars (e.g. `Claude`, `Kiro`, `Codex`, `Qwen`), otherwise fall back to `UPPER(alias)` (e.g. `GitHub Models` → `GHM`, `Gemini-cli` → `GEMINI-CLI`). Idempotent. Combos intentionally skipped (the `Combo: ` prefix already conveys multi-upstream). |
|
||||
| `usableOnly` | `boolean` | `false` | Read `/api/providers` and filter the catalog to providers that have at least one connection with `isActive: true` AND `testStatus: 'active'`. Subtract-filter semantics: providers unknown to BOTH the pricing-models catalog AND the connection table pass through (so synthetic prefixes like `agentrouter/*` survive). On fetch failure the filter is disabled for the refresh — never hides the whole catalog. |
|
||||
| `diskCache` | `boolean` | `true` | Persist the last successful `/v1/models` + `/api/combos` + enrichment + connections + compression snapshot to `${OPENCODE_DATA_DIR ?? ~/.local/share/opencode}/plugins/omniroute-<providerId>.json`. On a subsequent cold start where `/v1/models` throws (network down / IP whitelist drop / 5xx) the static block hydrates from the snapshot so OC's model picker survives offline. Soft-fail on read/write — never blocks publishing. |
|
||||
| `geminiSanitization` | `boolean` | `true` | Strip `$schema`/`$ref`/`additionalProperties` from tool params when the model id matches `gemini` |
|
||||
| `mcpAutoEmit` | `boolean` | `false` | Auto-write an `mcp.<providerId>` remote entry into the OC config pointing at `<baseURL>/api/mcp/stream` with the resolved Bearer token |
|
||||
| `mcpToken` | `string` | _unset_ | Optional separate Bearer for the auto-emitted MCP entry. Falls back to the provider's `apiKey` (from `auth.json`) when unset |
|
||||
| `fetchInterceptor` | `boolean` | `true` | Inject `Authorization: Bearer` + default `Content-Type` on every outbound request targeting `baseURL` (suffix-spoof guarded) |
|
||||
|
||||
#### Example — enrichment + compression tags + MCP auto-emit
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"plugin": [
|
||||
[
|
||||
"@omniroute/opencode-plugin",
|
||||
{
|
||||
"providerId": "omniroute",
|
||||
"baseURL": "https://or.example.com",
|
||||
"features": {
|
||||
"combos": true,
|
||||
"enrichment": true,
|
||||
"compressionMetadata": true,
|
||||
"mcpAutoEmit": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
With `mcpAutoEmit: true`, the plugin synthesises an `mcp.omniroute` entry equivalent to a manual:
|
||||
|
||||
```jsonc
|
||||
"mcp": {
|
||||
"omniroute": {
|
||||
"type": "remote",
|
||||
"url": "https://or.example.com/api/mcp/stream",
|
||||
"enabled": true,
|
||||
"headers": { "Authorization": "Bearer <apiKey-from-auth.json>" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If you want a narrower-scoped Bearer for MCP (different from the chat/inference key), set `features.mcpToken`. Operator overrides win: if you already set `mcp.omniroute` in `opencode.json`, the plugin will not overwrite it.
|
||||
|
||||
#### Example — production-leaning defaults (clean picker, offline resilience)
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"plugin": [
|
||||
[
|
||||
"@omniroute/opencode-plugin",
|
||||
{
|
||||
"providerId": "omniroute",
|
||||
"baseURL": "https://or.example.com",
|
||||
"features": {
|
||||
"combos": true,
|
||||
"enrichment": true,
|
||||
"compressionMetadata": true,
|
||||
"usableOnly": true,
|
||||
"diskCache": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
- `usableOnly: true` drops models whose canonical provider has no healthy connection in your OmniRoute instance — your `/models` picker stays focused on what you can actually call.
|
||||
- `diskCache: true` (default) writes a snapshot to `${OPENCODE_DATA_DIR}/plugins/omniroute-<providerId>.json` on every healthy refresh. On a cold start where `/v1/models` is unreachable (laptop offline, IP whitelist drop), the snapshot hydrates the static block so OC still shows the catalog instead of a stub.
|
||||
- `compressionMetadata: true` annotates combo display names with their pipeline using traffic-light emoji for intensity (e.g. `Combo: claude-primary [rtk🟡 → caveman🟠]`) so the picker advertises which compression each combo applies and how heavy it is at a glance. Palette: 🟢 lite/minimal · 🟡 standard · 🟠 aggressive/full · 🔴 ultra. Unknown intensities fall through to raw text (`[rtk:custom-thing]`) so the plugin never hides a value OmniRoute knows but the plugin doesn't.
|
||||
- `providerTag: true` (default) prepends a short upstream-provider label so the picker shows `Claude - Claude Opus 4.7` for `cc/claude-opus-4-7`, `Kiro - Claude Opus 4.7` for `kr/claude-opus-4-7`, and `GHM - GPT 5` for `ghm/gpt-5` (slot.name `GitHub Models` > 8 chars → abbreviated). Critical when the same model id is sold through multiple upstream connections with different cost/auth/rate-limit profiles. Set to `false` to keep the pre-v3.8.3 unsuffixed format.
|
||||
|
||||
## Comparison vs `@omniroute/opencode-provider`
|
||||
|
||||
[`@omniroute/opencode-provider`](https://github.com/diegosouzapw/OmniRoute/tree/main/%40omniroute/opencode-provider) is the existing config-generator package — it writes a frozen `provider.<id>` block into `opencode.json` at build time. This plugin is the runtime integration.
|
||||
|
||||
| | `@omniroute/opencode-plugin` (this) | `@omniroute/opencode-provider` |
|
||||
| ----------------- | ----------------------------------- | --------------------------------- |
|
||||
| Type | OC plugin | Config generator (CLI/build-time) |
|
||||
| Models | Live from `/v1/models` | Frozen at scaffold |
|
||||
| Combos | LCD-aggregated live | None |
|
||||
| Gemini sanitize | Yes | N/A |
|
||||
| OC UI integration | `/connect`, `/models` | None |
|
||||
| Multi-instance | Native | Manual |
|
||||
|
||||
Both can coexist; pick the one that fits your environment.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node `>=22.22.3` (per `engines.node`); tested on Node 22 and 24.
|
||||
- OpenCode: verified end-to-end against `opencode@1.15.5` with `@opencode-ai/plugin@1.15.6`.
|
||||
- OC plugin peer (`@opencode-ai/plugin`) `>=1.14.49` for the full feature set (provider hook surfaces models in `/models`). On `<=1.14.48`, the plugin falls back to its `config` hook, writing a static catalog snapshot into `config.provider[id]` so models still appear.
|
||||
- The plugin uses the OC v1 plugin shape (`default: { id, server }`) — older OC releases that only walk named exports will reject it. Stay on OC ≥1.15.
|
||||
|
||||
## License
|
||||
|
||||
MIT. See [LICENSE](./LICENSE).
|
||||
2414
@omniroute/opencode-plugin/package-lock.json
generated
Normal file
2414
@omniroute/opencode-plugin/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
73
@omniroute/opencode-plugin/package.json
Normal file
73
@omniroute/opencode-plugin/package.json
Normal file
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"name": "@omniroute/opencode-plugin",
|
||||
"version": "0.1.0",
|
||||
"description": "OpenCode plugin for the OmniRoute AI Gateway. Drives dynamic model discovery, /connect auth flow, and multi-instance OmniRoute providers via the official @opencode-ai/plugin contract.",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/index.d.cts",
|
||||
"default": "./dist/index.cjs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"clean": "rm -rf dist",
|
||||
"test": "node --import tsx/esm --test tests/scaffold.test.ts tests/auth.test.ts tests/options-schema.test.ts tests/multi-instance.test.ts tests/fetch-interceptor.test.ts tests/provider.test.ts tests/gemini-sanitize.test.ts tests/combos.test.ts tests/config-shim.test.ts tests/features.test.ts tests/usable-combo.test.ts tests/disk-snapshot-perms.test.ts",
|
||||
"prepublishOnly": "npm run clean && npm run build && npm test"
|
||||
},
|
||||
"keywords": [
|
||||
"omniroute",
|
||||
"opencode",
|
||||
"opencode-plugin",
|
||||
"ai-sdk",
|
||||
"openai-compatible",
|
||||
"provider",
|
||||
"gemini",
|
||||
"combos",
|
||||
"mcp"
|
||||
],
|
||||
"author": "OmniRoute contributors",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/diegosouzapw/OmniRoute.git",
|
||||
"directory": "@omniroute/opencode-plugin"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/diegosouzapw/OmniRoute/issues"
|
||||
},
|
||||
"homepage": "https://github.com/diegosouzapw/OmniRoute/tree/main/%40omniroute/opencode-plugin#readme",
|
||||
"engines": {
|
||||
"node": ">=22.22.3"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opencode-ai/plugin": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@opencode-ai/plugin": "^1.15.6",
|
||||
"@types/node": "^22.19.19",
|
||||
"tsup": "^8.5.1",
|
||||
"tsx": "^4.22.3",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
3561
@omniroute/opencode-plugin/src/index.ts
Normal file
3561
@omniroute/opencode-plugin/src/index.ts
Normal file
File diff suppressed because it is too large
Load Diff
147
@omniroute/opencode-plugin/tests/auth.test.ts
Normal file
147
@omniroute/opencode-plugin/tests/auth.test.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* T-02 auth-hook contract tests.
|
||||
*
|
||||
* Covers the `createOmniRouteAuthHook(opts)` factory and its loader behaviour
|
||||
* against every Auth flavor (`api`, `oauth`, null, empty key). Validates the
|
||||
* multi-instance fix: provider id flows from plugin options, not a module
|
||||
* constant.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createOmniRouteAuthHook } from "../src/index.js";
|
||||
|
||||
test("createOmniRouteAuthHook: default providerId is 'omniroute'", () => {
|
||||
const hook = createOmniRouteAuthHook();
|
||||
assert.equal(hook.provider, "omniroute");
|
||||
});
|
||||
|
||||
test("createOmniRouteAuthHook: custom providerId binds to hook.provider (multi-instance)", () => {
|
||||
const hook = createOmniRouteAuthHook({ providerId: "omniroute-preprod" });
|
||||
assert.equal(hook.provider, "omniroute-preprod");
|
||||
});
|
||||
|
||||
test("createOmniRouteAuthHook: methods[0] is type 'api' with label including displayName", () => {
|
||||
const hook = createOmniRouteAuthHook();
|
||||
assert.equal(Array.isArray(hook.methods), true);
|
||||
assert.equal(hook.methods.length, 1);
|
||||
const m = hook.methods[0];
|
||||
assert.equal(m.type, "api");
|
||||
assert.equal(m.label, "OmniRoute API Key");
|
||||
|
||||
const custom = createOmniRouteAuthHook({ providerId: "omniroute-preprod" });
|
||||
assert.equal(custom.methods[0].label, "OmniRoute (omniroute-preprod) API Key");
|
||||
});
|
||||
|
||||
test("createOmniRouteAuthHook: prompts[0] uses key='apiKey' per @opencode-ai/plugin contract", () => {
|
||||
// NOTE: spec referenced `name: "apiKey"`; the official
|
||||
// @opencode-ai/plugin@1.15.6 prompt shape uses `key` + `message` (no
|
||||
// `name`/`label`/`mask` fields). Asserting against the real type contract.
|
||||
const hook = createOmniRouteAuthHook();
|
||||
const m = hook.methods[0];
|
||||
assert.equal(m.type, "api");
|
||||
// narrow: api method may carry prompts
|
||||
const prompts = "prompts" in m ? m.prompts : undefined;
|
||||
assert.ok(Array.isArray(prompts) && prompts.length === 1, "expected one prompt");
|
||||
const p = prompts![0];
|
||||
assert.equal(p.type, "text");
|
||||
assert.equal((p as { key: string }).key, "apiKey");
|
||||
assert.ok(
|
||||
typeof (p as { message: string }).message === "string" &&
|
||||
(p as { message: string }).message.includes("omniroute"),
|
||||
"prompt message should mention provider id"
|
||||
);
|
||||
});
|
||||
|
||||
test("loader: valid api auth → {apiKey} when no baseURL option (T-04: fetch omitted)", async () => {
|
||||
// T-04 changed the loader return shape: without a resolvable baseURL the
|
||||
// interceptor cannot gate-keep requests, so the loader falls back to
|
||||
// apiKey-only and the AI-SDK uses its default fetch. See fetch-interceptor
|
||||
// tests for the wired-fetch branches.
|
||||
const hook = createOmniRouteAuthHook();
|
||||
assert.ok(hook.loader, "loader must be defined");
|
||||
const result = await hook.loader!(
|
||||
async () => ({ type: "api", key: "sk-test" }) as never,
|
||||
{} as never
|
||||
);
|
||||
assert.deepEqual(result, { apiKey: "sk-test" });
|
||||
});
|
||||
|
||||
test("loader: valid api auth → {apiKey, baseURL, fetch} when baseURL option set (T-04)", async () => {
|
||||
const hook = createOmniRouteAuthHook({ baseURL: "https://or.example.com/v1" });
|
||||
const result = await hook.loader!(
|
||||
async () => ({ type: "api", key: "sk-x" }) as never,
|
||||
{} as never
|
||||
);
|
||||
assert.equal((result as { apiKey: string }).apiKey, "sk-x");
|
||||
assert.equal((result as { baseURL: string }).baseURL, "https://or.example.com/v1");
|
||||
assert.equal(
|
||||
typeof (result as { fetch?: unknown }).fetch,
|
||||
"function",
|
||||
"T-04: loader must wire fetch interceptor when baseURL resolves"
|
||||
);
|
||||
});
|
||||
|
||||
test("loader: features.fetchInterceptor=false AND geminiSanitization=false → no custom fetch (flags honored)", async () => {
|
||||
// Regression: both fetch-layer flags were documented + schema-validated but
|
||||
// silently ignored. Disabling both must fall back to the SDK default fetch.
|
||||
const hook = createOmniRouteAuthHook({
|
||||
baseURL: "https://or.example.com/v1",
|
||||
features: { fetchInterceptor: false, geminiSanitization: false },
|
||||
});
|
||||
const result = await hook.loader!(
|
||||
async () => ({ type: "api", key: "sk-x" }) as never,
|
||||
{} as never
|
||||
);
|
||||
assert.deepEqual(result, { apiKey: "sk-x", baseURL: "https://or.example.com/v1" });
|
||||
assert.equal(
|
||||
(result as { fetch?: unknown }).fetch,
|
||||
undefined,
|
||||
"both flags off must omit the custom fetch"
|
||||
);
|
||||
});
|
||||
|
||||
test("loader: features.fetchInterceptor=false but geminiSanitization=true → fetch still wired (sanitizer only)", async () => {
|
||||
const hook = createOmniRouteAuthHook({
|
||||
baseURL: "https://or.example.com/v1",
|
||||
features: { fetchInterceptor: false, geminiSanitization: true },
|
||||
});
|
||||
const result = await hook.loader!(
|
||||
async () => ({ type: "api", key: "sk-x" }) as never,
|
||||
{} as never
|
||||
);
|
||||
assert.equal(
|
||||
typeof (result as { fetch?: unknown }).fetch,
|
||||
"function",
|
||||
"geminiSanitization alone must still provide a fetch wrapper"
|
||||
);
|
||||
});
|
||||
|
||||
test("loader: null/undefined auth → {} (no creds yet, OC surfaces /connect)", async () => {
|
||||
const hook = createOmniRouteAuthHook();
|
||||
const r1 = await hook.loader!(async () => null as never, {} as never);
|
||||
assert.deepEqual(r1, {});
|
||||
const r2 = await hook.loader!(async () => undefined as never, {} as never);
|
||||
assert.deepEqual(r2, {});
|
||||
});
|
||||
|
||||
test("loader: oauth-flavored auth → {} (wrong method type, ignored)", async () => {
|
||||
const hook = createOmniRouteAuthHook();
|
||||
const result = await hook.loader!(
|
||||
async () =>
|
||||
({
|
||||
type: "oauth",
|
||||
refresh: "r",
|
||||
access: "a",
|
||||
expires: 0,
|
||||
}) as never,
|
||||
{} as never
|
||||
);
|
||||
assert.deepEqual(result, {});
|
||||
});
|
||||
|
||||
test("loader: api auth with empty key → {} (empty creds rejected)", async () => {
|
||||
const hook = createOmniRouteAuthHook();
|
||||
const result = await hook.loader!(async () => ({ type: "api", key: "" }) as never, {} as never);
|
||||
assert.deepEqual(result, {});
|
||||
});
|
||||
641
@omniroute/opencode-plugin/tests/combos.test.ts
Normal file
641
@omniroute/opencode-plugin/tests/combos.test.ts
Normal file
@@ -0,0 +1,641 @@
|
||||
/**
|
||||
* T-05 combo-discovery contract tests.
|
||||
*
|
||||
* Covers:
|
||||
* - `defaultOmniRouteCombosFetcher(baseURL, apiKey, timeoutMs?)`
|
||||
* — envelope tolerance (`{combos: [...]}` and bare array), non-2xx errors.
|
||||
* - `mapComboToModelV2(combo, members, providerId, baseURL)`
|
||||
* — LCD policy across capabilities, limits, modalities; defensive
|
||||
* posture on empty members; nice-name preference.
|
||||
* - `createOmniRouteProviderHook(opts, deps)` extension
|
||||
* — combos merged into the models map; collision resolution (combo
|
||||
* wins, warn-once); soft-fail when the combos fetcher throws;
|
||||
* combos cached + reused under the same TTL key as models.
|
||||
*
|
||||
* Mocking strategy mirrors `provider.test.ts`: both fetchers are
|
||||
* dependency-injected at hook construction, no `fetch` monkey-patch.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
createOmniRouteProviderHook,
|
||||
defaultOmniRouteCombosFetcher,
|
||||
mapComboToModelV2,
|
||||
type OmniRouteCombosFetcher,
|
||||
type OmniRouteModelsFetcher,
|
||||
type OmniRouteRawCombo,
|
||||
type OmniRouteRawModelEntry,
|
||||
} from "../src/index.js";
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Fixtures
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const MODEL_PRIMARY: OmniRouteRawModelEntry = {
|
||||
id: "claude-primary",
|
||||
capabilities: {
|
||||
tool_calling: true,
|
||||
reasoning: true,
|
||||
vision: true,
|
||||
thinking: true,
|
||||
temperature: true,
|
||||
},
|
||||
context_length: 200_000,
|
||||
max_output_tokens: 64_000,
|
||||
max_input_tokens: 180_000,
|
||||
input_modalities: ["text", "image"],
|
||||
output_modalities: ["text"],
|
||||
};
|
||||
|
||||
const MODEL_SECONDARY: OmniRouteRawModelEntry = {
|
||||
id: "claude-secondary",
|
||||
capabilities: {
|
||||
tool_calling: true,
|
||||
reasoning: false,
|
||||
vision: true,
|
||||
thinking: false,
|
||||
temperature: true,
|
||||
},
|
||||
context_length: 100_000,
|
||||
max_output_tokens: 32_000,
|
||||
max_input_tokens: 96_000,
|
||||
input_modalities: ["text", "image"],
|
||||
output_modalities: ["text"],
|
||||
};
|
||||
|
||||
const MODEL_NO_TOOLS: OmniRouteRawModelEntry = {
|
||||
id: "gemini-3-flash",
|
||||
capabilities: { tool_calling: false, reasoning: false, vision: false, thinking: false },
|
||||
context_length: 1_000_000,
|
||||
max_output_tokens: 8_192,
|
||||
input_modalities: ["text"],
|
||||
output_modalities: ["text"],
|
||||
};
|
||||
|
||||
const COMBO_CLAUDE_TIER: OmniRouteRawCombo = {
|
||||
id: "combo-claude-tier",
|
||||
name: "Claude Tier",
|
||||
strategy: "priority",
|
||||
models: [
|
||||
{ id: "s1", kind: "model", model: "claude-primary", weight: 100 },
|
||||
{ id: "s2", kind: "model", model: "claude-secondary", weight: 80 },
|
||||
],
|
||||
};
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Helpers
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function stubModelsFetcher(
|
||||
payload: OmniRouteRawModelEntry[]
|
||||
): OmniRouteModelsFetcher & { callCount: () => number } {
|
||||
let n = 0;
|
||||
const f: OmniRouteModelsFetcher = async () => {
|
||||
n++;
|
||||
return payload;
|
||||
};
|
||||
return Object.assign(f, { callCount: () => n });
|
||||
}
|
||||
|
||||
function stubCombosFetcher(
|
||||
payload: OmniRouteRawCombo[]
|
||||
): OmniRouteCombosFetcher & { callCount: () => number; callsBy: () => Array<[string, string]> } {
|
||||
let n = 0;
|
||||
const calls: Array<[string, string]> = [];
|
||||
const f: OmniRouteCombosFetcher = async (baseURL, apiKey) => {
|
||||
n++;
|
||||
calls.push([baseURL, apiKey]);
|
||||
return payload;
|
||||
};
|
||||
return Object.assign(f, {
|
||||
callCount: () => n,
|
||||
callsBy: () => calls,
|
||||
});
|
||||
}
|
||||
|
||||
function failingCombosFetcher(
|
||||
err = new Error("boom")
|
||||
): OmniRouteCombosFetcher & { callCount: () => number } {
|
||||
let n = 0;
|
||||
const f: OmniRouteCombosFetcher = async () => {
|
||||
n++;
|
||||
throw err;
|
||||
};
|
||||
return Object.assign(f, { callCount: () => n });
|
||||
}
|
||||
|
||||
const apiAuth = (key: string): unknown => ({ type: "api", key });
|
||||
|
||||
// Capture console.warn invocations for the duration of a callback, then
|
||||
// restore the original. Needed because the collision + soft-fail paths
|
||||
// emit warnings we want to assert on.
|
||||
async function withWarnCapture<T>(
|
||||
fn: (warnings: Array<{ args: unknown[] }>) => Promise<T>
|
||||
): Promise<{ result: T; warnings: Array<{ args: unknown[] }> }> {
|
||||
const original = console.warn;
|
||||
const warnings: Array<{ args: unknown[] }> = [];
|
||||
console.warn = (...args: unknown[]) => {
|
||||
warnings.push({ args });
|
||||
};
|
||||
try {
|
||||
const result = await fn(warnings);
|
||||
return { result, warnings };
|
||||
} finally {
|
||||
console.warn = original;
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// defaultOmniRouteCombosFetcher — envelope tolerance + error surfacing
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("defaultOmniRouteCombosFetcher: parses {combos:[…]} envelope", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async (input: unknown) => {
|
||||
const url = typeof input === "string" ? input : (input as { url: string }).url;
|
||||
assert.equal(url, "https://or.example.com/api/combos");
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
combos: [
|
||||
{ id: "c1", name: "Combo One", strategy: "priority", models: [] },
|
||||
{ id: "c2", name: "Combo Two", strategy: "weighted", models: [] },
|
||||
],
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
}) as typeof fetch;
|
||||
try {
|
||||
const combos = await defaultOmniRouteCombosFetcher("https://or.example.com", "sk-test");
|
||||
assert.equal(combos.length, 2);
|
||||
assert.equal(combos[0].id, "c1");
|
||||
assert.equal(combos[1].id, "c2");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("defaultOmniRouteCombosFetcher: parses bare array envelope", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async () => {
|
||||
return new Response(JSON.stringify([{ id: "c1" }, { id: "c2" }, { not_an_id: 42 }]), {
|
||||
status: 200,
|
||||
});
|
||||
}) as typeof fetch;
|
||||
try {
|
||||
const combos = await defaultOmniRouteCombosFetcher("https://or.example.com/v1", "sk-test");
|
||||
// Strip /v1 before /api/combos, AND filter out entries with no string id.
|
||||
assert.equal(combos.length, 2);
|
||||
assert.equal(combos[0].id, "c1");
|
||||
assert.equal(combos[1].id, "c2");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("defaultOmniRouteCombosFetcher: strips trailing /v1 before /api/combos", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let observedUrl = "";
|
||||
globalThis.fetch = (async (input: unknown) => {
|
||||
observedUrl = typeof input === "string" ? input : (input as { url: string }).url;
|
||||
return new Response(JSON.stringify({ combos: [] }), { status: 200 });
|
||||
}) as typeof fetch;
|
||||
try {
|
||||
await defaultOmniRouteCombosFetcher("https://or.example.com/v1/", "sk-test");
|
||||
assert.equal(observedUrl, "https://or.example.com/api/combos");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("defaultOmniRouteCombosFetcher: throws on non-2xx with status code in message", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async () => {
|
||||
return new Response(JSON.stringify({ error: "Invalid token" }), {
|
||||
status: 403,
|
||||
statusText: "Forbidden",
|
||||
});
|
||||
}) as typeof fetch;
|
||||
try {
|
||||
await assert.rejects(
|
||||
async () => {
|
||||
await defaultOmniRouteCombosFetcher("https://or.example.com", "sk-bad");
|
||||
},
|
||||
(err: unknown) => {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
assert.match(msg, /403/, "status code must appear in message");
|
||||
assert.match(msg, /\/api\/combos/, "url must appear in message");
|
||||
return true;
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("defaultOmniRouteCombosFetcher: throws when apiKey missing", async () => {
|
||||
await assert.rejects(
|
||||
async () => defaultOmniRouteCombosFetcher("https://or.example.com", ""),
|
||||
/apiKey required/
|
||||
);
|
||||
});
|
||||
|
||||
test("defaultOmniRouteCombosFetcher: throws when baseURL missing", async () => {
|
||||
await assert.rejects(
|
||||
async () => defaultOmniRouteCombosFetcher("", "sk-test"),
|
||||
/baseURL required/
|
||||
);
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// mapComboToModelV2 — LCD semantics
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("mapComboToModelV2: empty members → capabilities all false (defensive)", () => {
|
||||
const m = mapComboToModelV2(
|
||||
{ id: "combo-empty", name: "Empty Combo" },
|
||||
[],
|
||||
"omniroute",
|
||||
"https://or.example.com/v1"
|
||||
);
|
||||
assert.equal(m.id, "combo-empty");
|
||||
assert.equal(m.name, "Empty Combo");
|
||||
assert.equal(m.capabilities.temperature, false);
|
||||
assert.equal(m.capabilities.reasoning, false);
|
||||
assert.equal(m.capabilities.attachment, false);
|
||||
assert.equal(m.capabilities.toolcall, false);
|
||||
assert.equal(m.capabilities.input.text, false);
|
||||
assert.equal(m.capabilities.output.text, false);
|
||||
assert.equal(m.limit.context, 0);
|
||||
assert.equal(m.limit.output, 0);
|
||||
assert.equal(m.limit.input, undefined);
|
||||
assert.deepEqual(m.cost, { input: 0, output: 0, cache: { read: 0, write: 0 } });
|
||||
});
|
||||
|
||||
test("mapComboToModelV2: all members reasoning=true → combo reasoning=true", () => {
|
||||
const m = mapComboToModelV2(
|
||||
{ id: "c", models: [] },
|
||||
[
|
||||
MODEL_PRIMARY,
|
||||
{
|
||||
...MODEL_PRIMARY,
|
||||
id: "p2",
|
||||
capabilities: { ...MODEL_PRIMARY.capabilities, thinking: false, reasoning: true },
|
||||
},
|
||||
],
|
||||
"omniroute",
|
||||
"https://or.example.com/v1"
|
||||
);
|
||||
assert.equal(m.capabilities.reasoning, true);
|
||||
});
|
||||
|
||||
test("mapComboToModelV2: any member reasoning=false → combo reasoning=false", () => {
|
||||
const m = mapComboToModelV2(
|
||||
{ id: "c", models: [] },
|
||||
[MODEL_PRIMARY, MODEL_NO_TOOLS], // gemini-3-flash has reasoning:false, thinking:false
|
||||
"omniroute",
|
||||
"https://or.example.com/v1"
|
||||
);
|
||||
assert.equal(m.capabilities.reasoning, false);
|
||||
});
|
||||
|
||||
test("mapComboToModelV2: limit.context is min of members'", () => {
|
||||
const m = mapComboToModelV2(
|
||||
{ id: "c", models: [] },
|
||||
[MODEL_PRIMARY, MODEL_SECONDARY, MODEL_NO_TOOLS],
|
||||
"omniroute",
|
||||
"https://or.example.com/v1"
|
||||
);
|
||||
// min(200_000, 100_000, 1_000_000) = 100_000
|
||||
assert.equal(m.limit.context, 100_000);
|
||||
// min(64_000, 32_000, 8_192) = 8_192
|
||||
assert.equal(m.limit.output, 8_192);
|
||||
});
|
||||
|
||||
test("mapComboToModelV2: limit.input only emitted when EVERY member declares one", () => {
|
||||
const m1 = mapComboToModelV2(
|
||||
{ id: "c", models: [] },
|
||||
[MODEL_PRIMARY, MODEL_SECONDARY],
|
||||
"omniroute",
|
||||
"https://or.example.com/v1"
|
||||
);
|
||||
// Both declare max_input_tokens → limit.input = min(180000, 96000)
|
||||
assert.equal(m1.limit.input, 96_000);
|
||||
|
||||
const m2 = mapComboToModelV2(
|
||||
{ id: "c", models: [] },
|
||||
[MODEL_PRIMARY, MODEL_NO_TOOLS], // gemini-3-flash doesn't declare max_input_tokens
|
||||
"omniroute",
|
||||
"https://or.example.com/v1"
|
||||
);
|
||||
assert.equal(m2.limit.input, undefined);
|
||||
});
|
||||
|
||||
test("mapComboToModelV2: nice name preferred from combo.name", () => {
|
||||
const m1 = mapComboToModelV2(
|
||||
{ id: "combo-x", name: "Pretty Name" },
|
||||
[MODEL_PRIMARY],
|
||||
"omniroute",
|
||||
"https://or.example.com/v1"
|
||||
);
|
||||
assert.equal(m1.name, "Pretty Name");
|
||||
|
||||
// Falls back to id when name is absent or empty.
|
||||
const m2 = mapComboToModelV2(
|
||||
{ id: "combo-y" },
|
||||
[MODEL_PRIMARY],
|
||||
"omniroute",
|
||||
"https://or.example.com/v1"
|
||||
);
|
||||
assert.equal(m2.name, "combo-y");
|
||||
|
||||
const m3 = mapComboToModelV2(
|
||||
{ id: "combo-z", name: " " },
|
||||
[MODEL_PRIMARY],
|
||||
"omniroute",
|
||||
"https://or.example.com/v1"
|
||||
);
|
||||
assert.equal(m3.name, "combo-z");
|
||||
});
|
||||
|
||||
test("mapComboToModelV2: attachment AND vision flag both honored across members", () => {
|
||||
// MODEL_PRIMARY: vision=true; MODEL_SECONDARY: vision=true → combo attachment=true
|
||||
const yes = mapComboToModelV2(
|
||||
{ id: "c1", models: [] },
|
||||
[MODEL_PRIMARY, MODEL_SECONDARY],
|
||||
"omniroute",
|
||||
"https://or.example.com/v1"
|
||||
);
|
||||
assert.equal(yes.capabilities.attachment, true);
|
||||
|
||||
// Add a member with no vision/attachment → AND collapses to false
|
||||
const no = mapComboToModelV2(
|
||||
{ id: "c2", models: [] },
|
||||
[MODEL_PRIMARY, MODEL_NO_TOOLS],
|
||||
"omniroute",
|
||||
"https://or.example.com/v1"
|
||||
);
|
||||
assert.equal(no.capabilities.attachment, false);
|
||||
});
|
||||
|
||||
test("mapComboToModelV2: modalities AND'd across members", () => {
|
||||
const m = mapComboToModelV2(
|
||||
{ id: "c", models: [] },
|
||||
[MODEL_PRIMARY, MODEL_SECONDARY], // both have text+image
|
||||
"omniroute",
|
||||
"https://or.example.com/v1"
|
||||
);
|
||||
assert.equal(m.capabilities.input.text, true);
|
||||
assert.equal(m.capabilities.input.image, true);
|
||||
assert.equal(m.capabilities.input.audio, false);
|
||||
|
||||
// Add a text-only member → image collapses to false.
|
||||
const m2 = mapComboToModelV2(
|
||||
{ id: "c", models: [] },
|
||||
[MODEL_PRIMARY, MODEL_NO_TOOLS],
|
||||
"omniroute",
|
||||
"https://or.example.com/v1"
|
||||
);
|
||||
assert.equal(m2.capabilities.input.text, true);
|
||||
assert.equal(m2.capabilities.input.image, false);
|
||||
});
|
||||
|
||||
test("mapComboToModelV2: api block matches providerId + baseURL", () => {
|
||||
const m = mapComboToModelV2(
|
||||
{ id: "c" },
|
||||
[MODEL_PRIMARY],
|
||||
"omniroute-preprod",
|
||||
"https://or-preprod.example.com/v1"
|
||||
);
|
||||
assert.equal(m.providerID, "omniroute-preprod");
|
||||
assert.equal(m.api.id, "openai-compatible");
|
||||
assert.equal(m.api.url, "https://or-preprod.example.com/v1");
|
||||
assert.equal(m.api.npm, "@ai-sdk/openai-compatible");
|
||||
assert.equal(m.status, "active");
|
||||
});
|
||||
|
||||
test("mapComboToModelV2: explicit member temperature=false drops combo temperature=false", () => {
|
||||
const tempFalse: OmniRouteRawModelEntry = {
|
||||
id: "no-temp",
|
||||
capabilities: { tool_calling: true, temperature: false },
|
||||
context_length: 100_000,
|
||||
max_output_tokens: 8_000,
|
||||
input_modalities: ["text"],
|
||||
output_modalities: ["text"],
|
||||
};
|
||||
const m = mapComboToModelV2(
|
||||
{ id: "c" },
|
||||
[MODEL_PRIMARY, tempFalse],
|
||||
"omniroute",
|
||||
"https://or.example.com/v1"
|
||||
);
|
||||
assert.equal(m.capabilities.temperature, false);
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// createOmniRouteProviderHook — combos merge + collision + soft-fail + cache
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("models() returns combo entries merged into the map", async () => {
|
||||
const modelsFetcher = stubModelsFetcher([MODEL_PRIMARY, MODEL_SECONDARY, MODEL_NO_TOOLS]);
|
||||
const combosFetcher = stubCombosFetcher([COMBO_CLAUDE_TIER]);
|
||||
const hook = createOmniRouteProviderHook(
|
||||
{ baseURL: "https://or.example.com/v1" },
|
||||
{ fetcher: modelsFetcher, combosFetcher }
|
||||
);
|
||||
const out = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
|
||||
|
||||
// 3 raw models + 1 combo = 4 entries
|
||||
assert.equal(Object.keys(out).length, 4);
|
||||
assert.ok(out["claude-primary"]);
|
||||
assert.ok(out["claude-secondary"]);
|
||||
assert.ok(out["gemini-3-flash"]);
|
||||
assert.ok(out["combo/claude-tier"]);
|
||||
|
||||
const combo = out["combo/claude-tier"];
|
||||
assert.equal(combo.name, "Combo: Claude Tier");
|
||||
assert.equal(combo.providerID, "omniroute");
|
||||
// LCD over claude-primary (200k, reasoning) + claude-secondary (100k, no reasoning)
|
||||
assert.equal(combo.limit.context, 100_000);
|
||||
assert.equal(combo.capabilities.reasoning, false);
|
||||
assert.equal(combo.capabilities.toolcall, true);
|
||||
});
|
||||
|
||||
test("models(): combo with unknown member ids degrades to all-false LCD posture", async () => {
|
||||
const modelsFetcher = stubModelsFetcher([MODEL_PRIMARY]); // catalog only has claude-primary
|
||||
const combosFetcher = stubCombosFetcher([
|
||||
{
|
||||
id: "phantom",
|
||||
name: "Phantom Combo",
|
||||
models: [
|
||||
{ id: "s1", kind: "model", model: "does-not-exist-1", weight: 50 },
|
||||
{ id: "s2", kind: "model", model: "does-not-exist-2", weight: 50 },
|
||||
],
|
||||
},
|
||||
]);
|
||||
const hook = createOmniRouteProviderHook(
|
||||
{ baseURL: "https://or.example.com/v1" },
|
||||
{ fetcher: modelsFetcher, combosFetcher }
|
||||
);
|
||||
const out = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
|
||||
assert.ok(out["combo/phantom-combo"]);
|
||||
// With zero resolvable members, LCD = all-false (defensive posture).
|
||||
assert.equal(out["combo/phantom-combo"].capabilities.toolcall, false);
|
||||
assert.equal(out["combo/phantom-combo"].capabilities.reasoning, false);
|
||||
assert.equal(out["combo/phantom-combo"].limit.context, 0);
|
||||
});
|
||||
|
||||
test("models(): hidden combos are excluded from the map", async () => {
|
||||
const modelsFetcher = stubModelsFetcher([MODEL_PRIMARY]);
|
||||
const combosFetcher = stubCombosFetcher([
|
||||
{
|
||||
id: "visible",
|
||||
name: "Visible",
|
||||
models: [{ id: "s1", kind: "model", model: "claude-primary", weight: 100 }],
|
||||
},
|
||||
{
|
||||
id: "hidden",
|
||||
name: "Hidden",
|
||||
isHidden: true,
|
||||
models: [{ id: "s1", kind: "model", model: "claude-primary", weight: 100 }],
|
||||
},
|
||||
]);
|
||||
const hook = createOmniRouteProviderHook(
|
||||
{ baseURL: "https://or.example.com/v1" },
|
||||
{ fetcher: modelsFetcher, combosFetcher }
|
||||
);
|
||||
const out = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
|
||||
assert.ok(out["combo/visible"]);
|
||||
assert.ok(!out["combo/hidden"], "hidden combo must be omitted");
|
||||
});
|
||||
|
||||
test("models(): combo name exactly matches raw model id → raw deleted, combo lives at combo/ key, no warn", async () => {
|
||||
// Combo.name === raw model id triggers the dedup deletion. This mirrors
|
||||
// the real OmniRoute payload where /v1/models pre-mirrors combos as
|
||||
// no-slash raw entries whose ids match /api/combos friendly names.
|
||||
const colliderCombo: OmniRouteRawCombo = {
|
||||
id: "uuid-collider",
|
||||
name: "claude-primary", // EXACT match to MODEL_PRIMARY.id
|
||||
models: [{ id: "s1", kind: "model", model: "claude-secondary", weight: 100 }],
|
||||
};
|
||||
const modelsFetcher = stubModelsFetcher([MODEL_PRIMARY, MODEL_SECONDARY]);
|
||||
const combosFetcher = stubCombosFetcher([colliderCombo]);
|
||||
const hook = createOmniRouteProviderHook(
|
||||
{ baseURL: "https://or.example.com/v1" },
|
||||
{ fetcher: modelsFetcher, combosFetcher }
|
||||
);
|
||||
|
||||
const { result: out, warnings } = await withWarnCapture(async (_w) => {
|
||||
return hook.models!({} as never, { auth: apiAuth("sk-z") as never });
|
||||
});
|
||||
|
||||
// Raw model deleted by combo-name dedup; combo surfaces under combo/<slug>.
|
||||
assert.equal(out["claude-primary"], undefined, "raw deleted by combo-name dedup");
|
||||
assert.ok(out["combo/claude-primary"], "combo surfaces under combo/ namespace");
|
||||
assert.equal(out["combo/claude-primary"].name, "Combo: claude-primary");
|
||||
|
||||
// No collision warning fires — dedup makes keys disjoint.
|
||||
const collisionWarns = warnings.filter((w) => {
|
||||
const msg = w.args[0];
|
||||
return typeof msg === "string" && msg.includes("collides");
|
||||
});
|
||||
assert.equal(collisionWarns.length, 0, "no collision warn after dedup");
|
||||
});
|
||||
|
||||
test("models(): two combos with same slug → second gets disambiguator suffix", async () => {
|
||||
// Both combos slug to `claude` — second must get `combo/claude-<id-prefix>`.
|
||||
const combos: OmniRouteRawCombo[] = [
|
||||
{
|
||||
id: "uuid-a",
|
||||
name: "Claude",
|
||||
models: [{ id: "s", kind: "model", model: "claude-primary", weight: 1 }],
|
||||
},
|
||||
{
|
||||
id: "uuid-b",
|
||||
name: "Claude",
|
||||
models: [{ id: "s", kind: "model", model: "claude-secondary", weight: 1 }],
|
||||
},
|
||||
];
|
||||
const hook = createOmniRouteProviderHook(
|
||||
{ baseURL: "https://or.example.com/v1" },
|
||||
{
|
||||
fetcher: stubModelsFetcher([MODEL_PRIMARY, MODEL_SECONDARY]),
|
||||
combosFetcher: stubCombosFetcher(combos),
|
||||
}
|
||||
);
|
||||
|
||||
const out = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
|
||||
// First combo gets the bare slug; second gets disambiguated.
|
||||
assert.ok(out["combo/claude"], "first combo at bare slug");
|
||||
assert.ok(out["combo/claude-uuid"], "second combo disambiguated by id prefix");
|
||||
});
|
||||
|
||||
test("models(): combos fetch fails → falls back to models-only, warn emitted, no throw", async () => {
|
||||
const modelsFetcher = stubModelsFetcher([MODEL_PRIMARY, MODEL_SECONDARY]);
|
||||
const combosFetcher = failingCombosFetcher(new Error("ECONNRESET"));
|
||||
const hook = createOmniRouteProviderHook(
|
||||
{ baseURL: "https://or.example.com/v1" },
|
||||
{ fetcher: modelsFetcher, combosFetcher }
|
||||
);
|
||||
|
||||
const { result: out, warnings } = await withWarnCapture(async () => {
|
||||
return hook.models!({} as never, { auth: apiAuth("sk-z") as never });
|
||||
});
|
||||
|
||||
// Catalog includes the models but NOT any combo entries.
|
||||
assert.equal(Object.keys(out).length, 2);
|
||||
assert.ok(out["claude-primary"]);
|
||||
assert.ok(out["claude-secondary"]);
|
||||
|
||||
// Soft-fail warning surfaced.
|
||||
const softFail = warnings.find((w) => {
|
||||
const msg = w.args[0];
|
||||
return typeof msg === "string" && msg.includes("combos fetch failed");
|
||||
});
|
||||
assert.ok(softFail, "soft-fail warning must be emitted on combos fetch error");
|
||||
assert.equal(combosFetcher.callCount(), 1);
|
||||
});
|
||||
|
||||
test("models(): combos cached + reused within TTL (one combo fetch per TTL window)", async () => {
|
||||
const modelsFetcher = stubModelsFetcher([MODEL_PRIMARY, MODEL_SECONDARY]);
|
||||
const combosFetcher = stubCombosFetcher([COMBO_CLAUDE_TIER]);
|
||||
let nowMs = 1_000_000;
|
||||
const hook = createOmniRouteProviderHook(
|
||||
{ baseURL: "https://or.example.com/v1", modelCacheTtl: 60_000 },
|
||||
{ fetcher: modelsFetcher, combosFetcher, now: () => nowMs }
|
||||
);
|
||||
|
||||
await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
|
||||
nowMs += 30_000; // half the TTL
|
||||
const second = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
|
||||
assert.equal(combosFetcher.callCount(), 1, "combos fetched only once within TTL");
|
||||
assert.equal(modelsFetcher.callCount(), 1, "models fetched only once within TTL");
|
||||
assert.ok(second["combo/claude-tier"]);
|
||||
});
|
||||
|
||||
test("models(): combos refetched after TTL expiry (same key as models)", async () => {
|
||||
const modelsFetcher = stubModelsFetcher([MODEL_PRIMARY]);
|
||||
const combosFetcher = stubCombosFetcher([COMBO_CLAUDE_TIER]);
|
||||
let nowMs = 1_000_000;
|
||||
const hook = createOmniRouteProviderHook(
|
||||
{ baseURL: "https://or.example.com/v1", modelCacheTtl: 60_000 },
|
||||
{ fetcher: modelsFetcher, combosFetcher, now: () => nowMs }
|
||||
);
|
||||
|
||||
await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
|
||||
nowMs += 60_001;
|
||||
await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
|
||||
assert.equal(combosFetcher.callCount(), 2, "combos must refetch past TTL");
|
||||
assert.equal(modelsFetcher.callCount(), 2, "models must refetch past TTL");
|
||||
});
|
||||
|
||||
test("models(): combos fetcher receives the resolved baseURL + apiKey", async () => {
|
||||
const modelsFetcher = stubModelsFetcher([MODEL_PRIMARY]);
|
||||
const combosFetcher = stubCombosFetcher([COMBO_CLAUDE_TIER]);
|
||||
const hook = createOmniRouteProviderHook(
|
||||
{ baseURL: "https://or.example.com/v1" },
|
||||
{ fetcher: modelsFetcher, combosFetcher }
|
||||
);
|
||||
await hook.models!({} as never, { auth: apiAuth("sk-spy") as never });
|
||||
assert.deepEqual(combosFetcher.callsBy()[0], ["https://or.example.com/v1", "sk-spy"]);
|
||||
});
|
||||
1364
@omniroute/opencode-plugin/tests/config-shim.test.ts
Normal file
1364
@omniroute/opencode-plugin/tests/config-shim.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
66
@omniroute/opencode-plugin/tests/disk-snapshot-perms.test.ts
Normal file
66
@omniroute/opencode-plugin/tests/disk-snapshot-perms.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Regression test for the disk-snapshot file permissions (release/v3.8.2
|
||||
* review finding C2). The snapshot embeds provider topology + connection
|
||||
* records and lives alongside auth.json (0o600), so it must NOT be readable by
|
||||
* group/other. Before the fix it was written with the default (typically
|
||||
* world-readable 0o644) mode.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import {
|
||||
defaultDiskSnapshotWriter,
|
||||
diskSnapshotPath,
|
||||
type OmniRouteFetchCacheEntry,
|
||||
} from "../src/index.js";
|
||||
|
||||
function makeEntry(): Omit<OmniRouteFetchCacheEntry, "expiresAt"> {
|
||||
return {
|
||||
rawModels: [],
|
||||
rawCombos: [],
|
||||
rawEnrichment: new Map(),
|
||||
rawCompressionCombos: [],
|
||||
rawConnections: [],
|
||||
};
|
||||
}
|
||||
|
||||
test("defaultDiskSnapshotWriter writes an owner-only (no group/other) snapshot", async (t) => {
|
||||
// POSIX-only assertion; Windows does not honor numeric file modes.
|
||||
if (process.platform === "win32") {
|
||||
t.skip("file mode semantics are POSIX-only");
|
||||
return;
|
||||
}
|
||||
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-disk-perms-"));
|
||||
const prevDataDir = process.env.OPENCODE_DATA_DIR;
|
||||
process.env.OPENCODE_DATA_DIR = tmp;
|
||||
|
||||
try {
|
||||
await defaultDiskSnapshotWriter("perm-test", makeEntry());
|
||||
|
||||
const file = diskSnapshotPath("perm-test");
|
||||
assert.ok(fs.existsSync(file), "snapshot file should be written");
|
||||
|
||||
const fileMode = fs.statSync(file).mode & 0o777;
|
||||
assert.equal(
|
||||
fileMode & 0o077,
|
||||
0,
|
||||
`snapshot must not be group/other accessible (got ${fileMode.toString(8)})`
|
||||
);
|
||||
|
||||
const dirMode = fs.statSync(path.dirname(file)).mode & 0o777;
|
||||
assert.equal(
|
||||
dirMode & 0o077,
|
||||
0,
|
||||
`plugins dir must not be group/other accessible (got ${dirMode.toString(8)})`
|
||||
);
|
||||
} finally {
|
||||
if (prevDataDir === undefined) delete process.env.OPENCODE_DATA_DIR;
|
||||
else process.env.OPENCODE_DATA_DIR = prevDataDir;
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
1064
@omniroute/opencode-plugin/tests/features.test.ts
Normal file
1064
@omniroute/opencode-plugin/tests/features.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
269
@omniroute/opencode-plugin/tests/fetch-interceptor.test.ts
Normal file
269
@omniroute/opencode-plugin/tests/fetch-interceptor.test.ts
Normal file
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* T-04 fetch-interceptor contract tests.
|
||||
*
|
||||
* Covers `createOmniRouteFetchInterceptor` (URL-prefix gating, header merge,
|
||||
* Content-Type defaulting, input-shape polymorphism) plus the loader
|
||||
* integration that wires it into the AuthHook return shape.
|
||||
*
|
||||
* Strategy: replace `globalThis.fetch` with a closure-based recorder for the
|
||||
* duration of each test (saved-and-restored in try/finally — node:test has
|
||||
* no built-in spy/restore lifecycle). The recorder captures `(input, init)`
|
||||
* as observed by the wrapped global call so we can assert on what was
|
||||
* forwarded after header injection.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createOmniRouteAuthHook, createOmniRouteFetchInterceptor } from "../src/index.js";
|
||||
|
||||
type FetchCall = { input: Parameters<typeof fetch>[0]; init?: RequestInit };
|
||||
|
||||
function installFetchRecorder(response: Response = new Response("ok")) {
|
||||
const calls: FetchCall[] = [];
|
||||
const original = globalThis.fetch;
|
||||
globalThis.fetch = (async (input: any, init?: any) => {
|
||||
calls.push({ input, init });
|
||||
return response;
|
||||
}) as typeof fetch;
|
||||
const restore = () => {
|
||||
globalThis.fetch = original;
|
||||
};
|
||||
return { calls, restore };
|
||||
}
|
||||
|
||||
const BASE = "https://or.example.com/v1";
|
||||
const KEY = "sk-test-fetch";
|
||||
|
||||
test("createOmniRouteFetchInterceptor: targets baseURL → Authorization header injected", async () => {
|
||||
const { calls, restore } = installFetchRecorder();
|
||||
try {
|
||||
const f = createOmniRouteFetchInterceptor({ apiKey: KEY, baseURL: BASE });
|
||||
await f(`${BASE}/chat/completions`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ x: 1 }),
|
||||
});
|
||||
assert.equal(calls.length, 1);
|
||||
const sent = calls[0]!;
|
||||
const sentHeaders = new Headers((sent.init as RequestInit).headers);
|
||||
assert.equal(sentHeaders.get("Authorization"), `Bearer ${KEY}`);
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("createOmniRouteFetchInterceptor: targets baseURL → Authorization OVERRIDES caller-supplied Bearer", async () => {
|
||||
const { calls, restore } = installFetchRecorder();
|
||||
try {
|
||||
const f = createOmniRouteFetchInterceptor({ apiKey: KEY, baseURL: BASE });
|
||||
await f(`${BASE}/chat/completions`, {
|
||||
method: "POST",
|
||||
body: "{}",
|
||||
headers: { Authorization: "Bearer attacker-key" },
|
||||
});
|
||||
const sent = calls[0]!;
|
||||
const sentHeaders = new Headers((sent.init as RequestInit).headers);
|
||||
// We own the apiKey for this provider — caller-supplied Bearer must lose.
|
||||
assert.equal(sentHeaders.get("Authorization"), `Bearer ${KEY}`);
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("createOmniRouteFetchInterceptor: targets baseURL + body → Content-Type defaults to application/json", async () => {
|
||||
const { calls, restore } = installFetchRecorder();
|
||||
try {
|
||||
const f = createOmniRouteFetchInterceptor({ apiKey: KEY, baseURL: BASE });
|
||||
await f(`${BASE}/chat/completions`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ m: "x" }),
|
||||
});
|
||||
const sent = calls[0]!;
|
||||
const sentHeaders = new Headers((sent.init as RequestInit).headers);
|
||||
assert.equal(sentHeaders.get("Content-Type"), "application/json");
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("createOmniRouteFetchInterceptor: caller-set Content-Type is NOT overwritten", async () => {
|
||||
const { calls, restore } = installFetchRecorder();
|
||||
try {
|
||||
const f = createOmniRouteFetchInterceptor({ apiKey: KEY, baseURL: BASE });
|
||||
await f(`${BASE}/v2/whatever`, {
|
||||
method: "POST",
|
||||
body: "raw",
|
||||
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
||||
});
|
||||
const sent = calls[0]!;
|
||||
const sentHeaders = new Headers((sent.init as RequestInit).headers);
|
||||
assert.equal(sentHeaders.get("Content-Type"), "text/plain; charset=utf-8");
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("createOmniRouteFetchInterceptor: non-baseURL host → passthrough, no Authorization injected", async () => {
|
||||
const { calls, restore } = installFetchRecorder();
|
||||
try {
|
||||
const f = createOmniRouteFetchInterceptor({ apiKey: KEY, baseURL: BASE });
|
||||
await f("https://third-party.example.org/v1/chat", {
|
||||
method: "POST",
|
||||
body: "{}",
|
||||
headers: { "X-Caller": "yes" },
|
||||
});
|
||||
const sent = calls[0]!;
|
||||
// Init forwarded verbatim — no header injection.
|
||||
const sentHeaders = new Headers((sent.init as RequestInit | undefined)?.headers);
|
||||
assert.equal(sentHeaders.get("Authorization"), null, "MUST NOT leak apiKey");
|
||||
assert.equal(sentHeaders.get("X-Caller"), "yes");
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("createOmniRouteFetchInterceptor: refuses suffix-spoof — `${base}-attacker.evil` does NOT match baseURL", async () => {
|
||||
const { calls, restore } = installFetchRecorder();
|
||||
try {
|
||||
const f = createOmniRouteFetchInterceptor({ apiKey: KEY, baseURL: BASE });
|
||||
// baseURL is `https://or.example.com/v1`. A spoofed
|
||||
// `https://or.example.com/v1-attacker.evil/chat` shares the literal prefix
|
||||
// but is NOT under our origin path — must be treated as passthrough.
|
||||
await f("https://or.example.com/v1-attacker.evil/chat", {
|
||||
method: "POST",
|
||||
body: "{}",
|
||||
});
|
||||
const sent = calls[0]!;
|
||||
const sentHeaders = new Headers((sent.init as RequestInit | undefined)?.headers);
|
||||
assert.equal(sentHeaders.get("Authorization"), null);
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("createOmniRouteFetchInterceptor: URL object input is handled", async () => {
|
||||
const { calls, restore } = installFetchRecorder();
|
||||
try {
|
||||
const f = createOmniRouteFetchInterceptor({ apiKey: KEY, baseURL: BASE });
|
||||
await f(new URL(`${BASE}/models`), {});
|
||||
const sent = calls[0]!;
|
||||
const sentHeaders = new Headers((sent.init as RequestInit).headers);
|
||||
assert.equal(sentHeaders.get("Authorization"), `Bearer ${KEY}`);
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("createOmniRouteFetchInterceptor: Request input is handled (reads .url)", async () => {
|
||||
const { calls, restore } = installFetchRecorder();
|
||||
try {
|
||||
const f = createOmniRouteFetchInterceptor({ apiKey: KEY, baseURL: BASE });
|
||||
const req = new Request(`${BASE}/chat/completions`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ a: 1 }),
|
||||
headers: { "X-Caller": "preserved" },
|
||||
});
|
||||
await f(req);
|
||||
const sent = calls[0]!;
|
||||
// The interceptor forwards the original Request as `input` but layers our
|
||||
// headers into the `init`. We assert against the init view since fetch()
|
||||
// resolves headers from init first when both are present.
|
||||
const sentHeaders = new Headers((sent.init as RequestInit).headers);
|
||||
assert.equal(sentHeaders.get("Authorization"), `Bearer ${KEY}`);
|
||||
assert.equal(
|
||||
sentHeaders.get("X-Caller"),
|
||||
"preserved",
|
||||
"Request-attached headers must survive the merge"
|
||||
);
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("createOmniRouteFetchInterceptor: trailing slash in baseURL is normalized", async () => {
|
||||
const { calls, restore } = installFetchRecorder();
|
||||
try {
|
||||
const f = createOmniRouteFetchInterceptor({
|
||||
apiKey: KEY,
|
||||
baseURL: `${BASE}////`,
|
||||
});
|
||||
await f(`${BASE}/models`, {});
|
||||
const sent = calls[0]!;
|
||||
const sentHeaders = new Headers((sent.init as RequestInit).headers);
|
||||
assert.equal(sentHeaders.get("Authorization"), `Bearer ${KEY}`);
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("createOmniRouteFetchInterceptor: GET without body does NOT set Content-Type", async () => {
|
||||
const { calls, restore } = installFetchRecorder();
|
||||
try {
|
||||
const f = createOmniRouteFetchInterceptor({ apiKey: KEY, baseURL: BASE });
|
||||
await f(`${BASE}/models`); // no init at all
|
||||
const sent = calls[0]!;
|
||||
const sentHeaders = new Headers((sent.init as RequestInit).headers);
|
||||
assert.equal(sentHeaders.get("Authorization"), `Bearer ${KEY}`);
|
||||
assert.equal(
|
||||
sentHeaders.get("Content-Type"),
|
||||
null,
|
||||
"Content-Type should only default when a body exists"
|
||||
);
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// loader integration
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
test("loader: returns fetch fn when apiKey + baseURL both present (via opts)", async () => {
|
||||
const hook = createOmniRouteAuthHook({ baseURL: BASE });
|
||||
const result = await hook.loader!(async () => ({ type: "api", key: KEY }) as never, {} as never);
|
||||
assert.equal((result as { apiKey: string }).apiKey, KEY);
|
||||
assert.equal((result as { baseURL: string }).baseURL, BASE);
|
||||
assert.equal(
|
||||
typeof (result as { fetch?: unknown }).fetch,
|
||||
"function",
|
||||
"loader must wire fetch interceptor when baseURL resolves"
|
||||
);
|
||||
});
|
||||
|
||||
test("loader: returns fetch fn when baseURL is stashed on the auth credential", async () => {
|
||||
// Some auth backends attach baseURL alongside the key (post-/connect flow).
|
||||
// The loader should pick it up even when plugin opts.baseURL is unset.
|
||||
const hook = createOmniRouteAuthHook();
|
||||
const result = await hook.loader!(
|
||||
async () => ({ type: "api", key: KEY, baseURL: BASE }) as never,
|
||||
{} as never
|
||||
);
|
||||
assert.equal((result as { baseURL?: string }).baseURL, BASE);
|
||||
assert.equal(typeof (result as { fetch?: unknown }).fetch, "function");
|
||||
});
|
||||
|
||||
test("loader: omits fetch fn when baseURL missing (apiKey-only return)", async () => {
|
||||
const hook = createOmniRouteAuthHook(); // no baseURL opt
|
||||
const result = await hook.loader!(async () => ({ type: "api", key: KEY }) as never, {} as never);
|
||||
// Interceptor needs a baseURL to gate-keep; without one, fall back to
|
||||
// apiKey-only and let the SDK use its default fetch.
|
||||
assert.deepEqual(result, { apiKey: KEY });
|
||||
});
|
||||
|
||||
test("loader integration: wired interceptor actually injects Bearer when invoked", async () => {
|
||||
// End-to-end: pull the fetch fn out of the loader return and exercise it,
|
||||
// proving the wiring matches the standalone interceptor's contract.
|
||||
const { calls, restore } = installFetchRecorder();
|
||||
try {
|
||||
const hook = createOmniRouteAuthHook({ baseURL: BASE });
|
||||
const result = await hook.loader!(
|
||||
async () => ({ type: "api", key: KEY }) as never,
|
||||
{} as never
|
||||
);
|
||||
const wiredFetch = (result as { fetch: typeof fetch }).fetch;
|
||||
await wiredFetch(`${BASE}/v1/models`, {});
|
||||
assert.equal(calls.length, 1);
|
||||
const sentHeaders = new Headers((calls[0]!.init as RequestInit).headers);
|
||||
assert.equal(sentHeaders.get("Authorization"), `Bearer ${KEY}`);
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
410
@omniroute/opencode-plugin/tests/gemini-sanitize.test.ts
Normal file
410
@omniroute/opencode-plugin/tests/gemini-sanitize.test.ts
Normal file
@@ -0,0 +1,410 @@
|
||||
/**
|
||||
* T-06 Gemini tool-schema sanitisation contract tests.
|
||||
*
|
||||
* Three layers under test:
|
||||
* 1. `sanitizeGeminiToolSchemas` — pure function; key stripping + clone
|
||||
* semantics on chat-completion + Responses-API shapes.
|
||||
* 2. `shouldSanitizeForGemini` — model-string detection (liberal).
|
||||
* 3. `createGeminiSanitizingFetch` — wrapper composition; URL gating,
|
||||
* body-shape polymorphism, streaming-body bypass, fail-open behaviour,
|
||||
* composition with the T-04 Bearer interceptor.
|
||||
*
|
||||
* Strategy: same posture as fetch-interceptor.test.ts — install a
|
||||
* closure-based fetch recorder; assert on the `(input, init)` observed by
|
||||
* the inner fetch after the sanitising wrapper has had its say.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
__resetGeminiStreamingWarning,
|
||||
createGeminiSanitizingFetch,
|
||||
createOmniRouteFetchInterceptor,
|
||||
sanitizeGeminiToolSchemas,
|
||||
shouldSanitizeForGemini,
|
||||
} from "../src/index.js";
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Helpers
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
type FetchCall = { input: Parameters<typeof fetch>[0]; init?: RequestInit };
|
||||
|
||||
function recorder(response: Response = new Response("ok")): {
|
||||
fn: typeof fetch;
|
||||
calls: FetchCall[];
|
||||
} {
|
||||
const calls: FetchCall[] = [];
|
||||
const fn = (async (input: any, init?: any) => {
|
||||
calls.push({ input, init });
|
||||
return response;
|
||||
}) as typeof fetch;
|
||||
return { fn, calls };
|
||||
}
|
||||
|
||||
function bodyAsRecord(init: RequestInit | undefined): Record<string, unknown> {
|
||||
const b = init?.body;
|
||||
if (typeof b !== "string") {
|
||||
throw new Error(`expected string body, got ${typeof b}`);
|
||||
}
|
||||
return JSON.parse(b) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
// Sample tool payloads — small enough to inline, big enough to cover
|
||||
// chat-completion + Responses-API + nested properties.
|
||||
|
||||
function chatCompletionsWithDollarSchema(): Record<string, unknown> {
|
||||
return {
|
||||
model: "gemini-2.5-pro",
|
||||
tools: [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "search",
|
||||
parameters: {
|
||||
$schema: "http://json-schema.org/draft-07/schema#",
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
q: { type: "string" },
|
||||
},
|
||||
required: ["q"],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function responsesApiWithRef(): Record<string, unknown> {
|
||||
return {
|
||||
model: "gemini-2.5-flash",
|
||||
tools: [
|
||||
{
|
||||
type: "function",
|
||||
name: "lookup",
|
||||
input_schema: {
|
||||
type: "object",
|
||||
$ref: "#/definitions/Lookup",
|
||||
properties: {
|
||||
id: { type: "string", ref: "Id" },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function nestedPropertiesPayload(): Record<string, unknown> {
|
||||
return {
|
||||
model: "gemini-pro",
|
||||
tools: [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "deep",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
outer: {
|
||||
type: "object",
|
||||
$schema: "http://json-schema.org/draft-07/schema#",
|
||||
properties: {
|
||||
inner: {
|
||||
type: "object",
|
||||
additionalProperties: true,
|
||||
$ref: "#/inner",
|
||||
properties: {
|
||||
leaf: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// sanitizeGeminiToolSchemas — pure function
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("sanitizeGeminiToolSchemas: strips $schema from top-level", () => {
|
||||
const input = {
|
||||
model: "gemini-2.5-pro",
|
||||
$schema: "http://json-schema.org/draft-07/schema#",
|
||||
tools: [],
|
||||
};
|
||||
const out = sanitizeGeminiToolSchemas(input) as Record<string, unknown>;
|
||||
assert.equal(out.$schema, undefined);
|
||||
assert.equal(out.model, "gemini-2.5-pro");
|
||||
});
|
||||
|
||||
test("sanitizeGeminiToolSchemas: strips $ref + additionalProperties from tools[].function.parameters", () => {
|
||||
const input = chatCompletionsWithDollarSchema();
|
||||
const out = sanitizeGeminiToolSchemas(input) as Record<string, unknown>;
|
||||
const params = (out.tools as Array<{ function: { parameters: Record<string, unknown> } }>)[0]!
|
||||
.function.parameters;
|
||||
assert.equal(params.$schema, undefined);
|
||||
assert.equal(params.additionalProperties, undefined);
|
||||
// Untouched keys survive.
|
||||
assert.equal(params.type, "object");
|
||||
assert.deepEqual(params.required, ["q"]);
|
||||
});
|
||||
|
||||
test("sanitizeGeminiToolSchemas: strips nested $schema from properties.x.properties.y", () => {
|
||||
const input = nestedPropertiesPayload();
|
||||
const out = sanitizeGeminiToolSchemas(input) as Record<string, unknown>;
|
||||
const params = (out.tools as Array<{ function: { parameters: Record<string, unknown> } }>)[0]!
|
||||
.function.parameters;
|
||||
const outer = (params.properties as Record<string, Record<string, unknown>>).outer!;
|
||||
const inner = (outer.properties as Record<string, Record<string, unknown>>).inner!;
|
||||
assert.equal(outer.$schema, undefined);
|
||||
assert.equal(inner.$ref, undefined);
|
||||
assert.equal(inner.additionalProperties, undefined);
|
||||
// Leaf still intact.
|
||||
assert.deepEqual(inner.properties, { leaf: { type: "string" } });
|
||||
});
|
||||
|
||||
test("sanitizeGeminiToolSchemas: handles Responses-API tools[].input_schema shape", () => {
|
||||
const input = responsesApiWithRef();
|
||||
const out = sanitizeGeminiToolSchemas(input) as Record<string, unknown>;
|
||||
const inputSchema = (out.tools as Array<{ input_schema: Record<string, unknown> }>)[0]!
|
||||
.input_schema;
|
||||
assert.equal(inputSchema.$ref, undefined);
|
||||
// Nested `ref` (lowercase) also stripped.
|
||||
const props = inputSchema.properties as Record<string, Record<string, unknown>>;
|
||||
assert.equal(props.id!.ref, undefined);
|
||||
assert.equal(props.id!.type, "string");
|
||||
});
|
||||
|
||||
test("sanitizeGeminiToolSchemas: leaves payload without tools untouched", () => {
|
||||
const input = { model: "gemini-2.5-pro", messages: [{ role: "user", content: "hi" }] };
|
||||
const out = sanitizeGeminiToolSchemas(input) as Record<string, unknown>;
|
||||
assert.deepEqual(out, input);
|
||||
});
|
||||
|
||||
test("sanitizeGeminiToolSchemas: does not mutate input (returned object is distinct)", () => {
|
||||
const input = chatCompletionsWithDollarSchema();
|
||||
const beforeJson = JSON.stringify(input);
|
||||
const out = sanitizeGeminiToolSchemas(input);
|
||||
// Input bit-identical to its pre-sanitise serialisation.
|
||||
assert.equal(JSON.stringify(input), beforeJson);
|
||||
// Output is a different reference.
|
||||
assert.notEqual(out, input);
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// shouldSanitizeForGemini — detection
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("shouldSanitizeForGemini: gemini-2.5-pro → true", () => {
|
||||
assert.equal(shouldSanitizeForGemini({ model: "gemini-2.5-pro" }), true);
|
||||
});
|
||||
|
||||
test("shouldSanitizeForGemini: models/gemini-pro → true", () => {
|
||||
assert.equal(shouldSanitizeForGemini({ model: "models/gemini-pro" }), true);
|
||||
});
|
||||
|
||||
test("shouldSanitizeForGemini: google-vertex/gemini-1.5-flash → true", () => {
|
||||
assert.equal(shouldSanitizeForGemini({ model: "google-vertex/gemini-1.5-flash" }), true);
|
||||
});
|
||||
|
||||
test("shouldSanitizeForGemini: gemini-cli/gemini-2.5-pro → true (real OmniRoute alias)", () => {
|
||||
assert.equal(shouldSanitizeForGemini({ model: "gemini-cli/gemini-2.5-pro" }), true);
|
||||
});
|
||||
|
||||
test("shouldSanitizeForGemini: claude-sonnet-4 → false", () => {
|
||||
assert.equal(shouldSanitizeForGemini({ model: "claude-sonnet-4" }), false);
|
||||
});
|
||||
|
||||
test("shouldSanitizeForGemini: payload.model missing → false", () => {
|
||||
assert.equal(shouldSanitizeForGemini({ messages: [] }), false);
|
||||
});
|
||||
|
||||
test("shouldSanitizeForGemini: payload is null → false", () => {
|
||||
assert.equal(shouldSanitizeForGemini(null), false);
|
||||
});
|
||||
|
||||
test("shouldSanitizeForGemini: payload.model is non-string → false", () => {
|
||||
assert.equal(shouldSanitizeForGemini({ model: 42 }), false);
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// createGeminiSanitizingFetch — wrapper
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const URL_CHAT = "https://or.example.com/v1/chat/completions";
|
||||
const URL_RESPONSES = "https://or.example.com/v1/responses";
|
||||
const URL_MODELS = "https://or.example.com/v1/models";
|
||||
|
||||
test("createGeminiSanitizingFetch: gemini model + chat/completions → tool schemas stripped before forward", async () => {
|
||||
const rec = recorder();
|
||||
const wrapped = createGeminiSanitizingFetch(rec.fn);
|
||||
await wrapped(URL_CHAT, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(chatCompletionsWithDollarSchema()),
|
||||
});
|
||||
assert.equal(rec.calls.length, 1);
|
||||
const forwarded = bodyAsRecord(rec.calls[0]!.init);
|
||||
const params = (
|
||||
forwarded.tools as Array<{ function: { parameters: Record<string, unknown> } }>
|
||||
)[0]!.function.parameters;
|
||||
assert.equal(params.$schema, undefined);
|
||||
assert.equal(params.additionalProperties, undefined);
|
||||
});
|
||||
|
||||
test("createGeminiSanitizingFetch: non-gemini model + chat/completions → body passed through unchanged", async () => {
|
||||
const rec = recorder();
|
||||
const wrapped = createGeminiSanitizingFetch(rec.fn);
|
||||
const originalBody = JSON.stringify({
|
||||
model: "claude-sonnet-4",
|
||||
tools: [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "x",
|
||||
parameters: { $schema: "keep-me", type: "object" },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
await wrapped(URL_CHAT, { method: "POST", body: originalBody });
|
||||
// Identity check on body — wrapper must NOT mutate non-Gemini payloads.
|
||||
assert.equal(rec.calls[0]!.init!.body, originalBody);
|
||||
});
|
||||
|
||||
test("createGeminiSanitizingFetch: gemini model + /v1/models (non-completion endpoint) → body passed through unchanged", async () => {
|
||||
const rec = recorder();
|
||||
const wrapped = createGeminiSanitizingFetch(rec.fn);
|
||||
// GET /v1/models has no body in production; assert that even if a caller
|
||||
// attached a Gemini-shaped body to a non-completion URL, the wrapper
|
||||
// doesn't touch it.
|
||||
const body = JSON.stringify(chatCompletionsWithDollarSchema());
|
||||
await wrapped(URL_MODELS, { method: "POST", body });
|
||||
assert.equal(rec.calls[0]!.init!.body, body);
|
||||
});
|
||||
|
||||
test("createGeminiSanitizingFetch: gemini model + /responses endpoint → input_schema stripped", async () => {
|
||||
const rec = recorder();
|
||||
const wrapped = createGeminiSanitizingFetch(rec.fn);
|
||||
await wrapped(URL_RESPONSES, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(responsesApiWithRef()),
|
||||
});
|
||||
const forwarded = bodyAsRecord(rec.calls[0]!.init);
|
||||
const schema = (forwarded.tools as Array<{ input_schema: Record<string, unknown> }>)[0]!
|
||||
.input_schema;
|
||||
assert.equal(schema.$ref, undefined);
|
||||
});
|
||||
|
||||
test("createGeminiSanitizingFetch: gemini model + Request input with body → tool schemas stripped", async () => {
|
||||
const rec = recorder();
|
||||
const wrapped = createGeminiSanitizingFetch(rec.fn);
|
||||
const req = new Request(URL_CHAT, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(chatCompletionsWithDollarSchema()),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
await wrapped(req);
|
||||
const forwarded = bodyAsRecord(rec.calls[0]!.init);
|
||||
const params = (
|
||||
forwarded.tools as Array<{ function: { parameters: Record<string, unknown> } }>
|
||||
)[0]!.function.parameters;
|
||||
assert.equal(params.$schema, undefined);
|
||||
});
|
||||
|
||||
test("createGeminiSanitizingFetch: gemini model + ReadableStream body → skipped + warn emitted once", async () => {
|
||||
__resetGeminiStreamingWarning();
|
||||
const rec = recorder();
|
||||
const wrapped = createGeminiSanitizingFetch(rec.fn);
|
||||
|
||||
// Capture console.warn for the duration of this test.
|
||||
const warnings: string[] = [];
|
||||
const originalWarn = console.warn;
|
||||
console.warn = (...args: unknown[]) => {
|
||||
warnings.push(args.map(String).join(" "));
|
||||
};
|
||||
|
||||
try {
|
||||
const stream1 = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(new TextEncoder().encode("{}"));
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
const stream2 = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(new TextEncoder().encode("{}"));
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
// Two streaming calls — only one warn expected.
|
||||
await wrapped(URL_CHAT, { method: "POST", body: stream1 });
|
||||
await wrapped(URL_CHAT, { method: "POST", body: stream2 });
|
||||
} finally {
|
||||
console.warn = originalWarn;
|
||||
}
|
||||
|
||||
// Both calls forwarded to inner fetch with their streams intact.
|
||||
assert.equal(rec.calls.length, 2);
|
||||
// ONE warning total — one-shot latch held.
|
||||
assert.equal(warnings.length, 1);
|
||||
assert.match(warnings[0]!, /streaming Request body, skipping schema strip/);
|
||||
});
|
||||
|
||||
test("createGeminiSanitizingFetch: invalid JSON body → pass through, no throw", async () => {
|
||||
const rec = recorder();
|
||||
const wrapped = createGeminiSanitizingFetch(rec.fn);
|
||||
// Garbage body must not crash the wrapper.
|
||||
await wrapped(URL_CHAT, { method: "POST", body: "this is not json{{" });
|
||||
assert.equal(rec.calls.length, 1);
|
||||
assert.equal(rec.calls[0]!.init!.body, "this is not json{{");
|
||||
});
|
||||
|
||||
test("createGeminiSanitizingFetch: empty body → pass through unchanged", async () => {
|
||||
const rec = recorder();
|
||||
const wrapped = createGeminiSanitizingFetch(rec.fn);
|
||||
await wrapped(URL_CHAT, { method: "POST" });
|
||||
assert.equal(rec.calls.length, 1);
|
||||
});
|
||||
|
||||
test("createGeminiSanitizingFetch: composes correctly with createOmniRouteFetchInterceptor (Bearer + sanitization)", async () => {
|
||||
// Save and replace globalThis.fetch — the Bearer interceptor calls global
|
||||
// fetch when the URL targets its baseURL.
|
||||
const originalFetch = globalThis.fetch;
|
||||
const observed: FetchCall[] = [];
|
||||
globalThis.fetch = (async (input: any, init?: any) => {
|
||||
observed.push({ input, init });
|
||||
return new Response("ok");
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const composed = createGeminiSanitizingFetch(
|
||||
createOmniRouteFetchInterceptor({
|
||||
apiKey: "sk-test",
|
||||
baseURL: "https://or.example.com/v1",
|
||||
})
|
||||
);
|
||||
await composed(URL_CHAT, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(chatCompletionsWithDollarSchema()),
|
||||
});
|
||||
|
||||
assert.equal(observed.length, 1);
|
||||
// Bearer injected (header concern).
|
||||
const sentHeaders = new Headers((observed[0]!.init as RequestInit).headers);
|
||||
assert.equal(sentHeaders.get("Authorization"), "Bearer sk-test");
|
||||
// Schema sanitised (body concern).
|
||||
const forwarded = bodyAsRecord(observed[0]!.init);
|
||||
const params = (
|
||||
forwarded.tools as Array<{ function: { parameters: Record<string, unknown> } }>
|
||||
)[0]!.function.parameters;
|
||||
assert.equal(params.$schema, undefined);
|
||||
assert.equal(params.additionalProperties, undefined);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
136
@omniroute/opencode-plugin/tests/multi-instance.test.ts
Normal file
136
@omniroute/opencode-plugin/tests/multi-instance.test.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* T-08 multi-instance smoke.
|
||||
*
|
||||
* Validates that two `OmniRoutePlugin(input, opts)` invocations with
|
||||
* different `providerId` values coexist without sharing mutable state.
|
||||
* This is the contract that lets opencode.json declare prod + preprod
|
||||
* side by side:
|
||||
*
|
||||
* "plugin": [
|
||||
* ["@omniroute/opencode-plugin", {"providerId": "omniroute-prod", "baseURL": "https://or.example/v1"}],
|
||||
* ["@omniroute/opencode-plugin", {"providerId": "omniroute-preprod", "baseURL": "https://or-preprod.example/v1"}]
|
||||
* ]
|
||||
*
|
||||
* Assertions:
|
||||
* - Each invocation returns its own hooks object (no identity reuse).
|
||||
* - Each `auth` hook carries its own `provider` matching opts.providerId.
|
||||
* - Each `auth.methods` array is its own array (not the same reference).
|
||||
* - Calling the factory twice with IDENTICAL opts still yields two
|
||||
* independent objects (no instance reuse / no shared closure cache).
|
||||
* - Mutating one instance's auth hook does NOT bleed into the other.
|
||||
* - Each instance's loader closure captures its OWN baseURL — no
|
||||
* last-write-wins module-scope state.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { OmniRoutePlugin } from "../src/index.js";
|
||||
|
||||
const fakeInput = {} as Parameters<typeof OmniRoutePlugin>[0];
|
||||
|
||||
test("multi-instance: two plugin invocations bind to their own providerId", async () => {
|
||||
const a = await OmniRoutePlugin(fakeInput, {
|
||||
providerId: "omniroute-prod",
|
||||
baseURL: "https://a.example/v1",
|
||||
});
|
||||
const b = await OmniRoutePlugin(fakeInput, {
|
||||
providerId: "omniroute-preprod",
|
||||
baseURL: "https://b.example/v1",
|
||||
});
|
||||
|
||||
assert.equal(a.auth?.provider, "omniroute-prod");
|
||||
assert.equal(b.auth?.provider, "omniroute-preprod");
|
||||
});
|
||||
|
||||
test("multi-instance: hook objects + nested arrays are independent references", async () => {
|
||||
const a = await OmniRoutePlugin(fakeInput, {
|
||||
providerId: "alpha",
|
||||
baseURL: "https://a.example/v1",
|
||||
});
|
||||
const b = await OmniRoutePlugin(fakeInput, {
|
||||
providerId: "bravo",
|
||||
baseURL: "https://b.example/v1",
|
||||
});
|
||||
|
||||
assert.notEqual(a, b, "top-level hooks objects must not be the same reference");
|
||||
assert.notEqual(a.auth, b.auth, "auth hooks must not be the same reference");
|
||||
assert.notEqual(
|
||||
a.auth?.methods,
|
||||
b.auth?.methods,
|
||||
"methods arrays must not be the same reference"
|
||||
);
|
||||
});
|
||||
|
||||
test("multi-instance: identical opts twice still yield independent objects", async () => {
|
||||
const opts = { providerId: "twin", baseURL: "https://twin.example/v1" };
|
||||
const first = await OmniRoutePlugin(fakeInput, { ...opts });
|
||||
const second = await OmniRoutePlugin(fakeInput, { ...opts });
|
||||
|
||||
assert.notEqual(first, second);
|
||||
assert.notEqual(first.auth, second.auth);
|
||||
assert.notEqual(first.auth?.methods, second.auth?.methods);
|
||||
// Same provider id is fine — what matters is no shared mutable state.
|
||||
assert.equal(first.auth?.provider, "twin");
|
||||
assert.equal(second.auth?.provider, "twin");
|
||||
});
|
||||
|
||||
test("multi-instance: mutating instance A's auth.methods does not affect instance B", async () => {
|
||||
const a = await OmniRoutePlugin(fakeInput, {
|
||||
providerId: "iso-a",
|
||||
baseURL: "https://a.example/v1",
|
||||
});
|
||||
const b = await OmniRoutePlugin(fakeInput, {
|
||||
providerId: "iso-b",
|
||||
baseURL: "https://b.example/v1",
|
||||
});
|
||||
|
||||
const beforeLen = b.auth?.methods?.length ?? 0;
|
||||
// Mutate a's methods array — extend it; b's must be untouched.
|
||||
// We don't know the concrete method shape so push a sentinel cast.
|
||||
a.auth?.methods?.push({ type: "api", label: "sentinel" } as never);
|
||||
assert.equal(b.auth?.methods?.length, beforeLen, "instance B leaked from instance A mutation");
|
||||
});
|
||||
|
||||
test("multi-instance: loader closures see their own opts (not last-write-wins)", async () => {
|
||||
// Each plugin's loader builds its loader payload from the providerId/baseURL
|
||||
// captured at invocation time. If the factory accidentally shared a closure
|
||||
// (e.g. a module-scope let that the last invocation overwrites), both
|
||||
// loaders would emit the same baseURL. Verify they don't.
|
||||
const a = await OmniRoutePlugin(fakeInput, {
|
||||
providerId: "omniroute-prod",
|
||||
baseURL: "https://prod.example/v1",
|
||||
});
|
||||
const b = await OmniRoutePlugin(fakeInput, {
|
||||
providerId: "omniroute-preprod",
|
||||
baseURL: "https://preprod.example/v1",
|
||||
});
|
||||
|
||||
assert.ok(a.auth?.loader, "instance A must have a loader");
|
||||
assert.ok(b.auth?.loader, "instance B must have a loader");
|
||||
|
||||
const getAuthA = async () => ({ type: "api", key: "sk-prod" }) as never;
|
||||
const getAuthB = async () => ({ type: "api", key: "sk-preprod" }) as never;
|
||||
|
||||
const rA = (await a.auth!.loader!(getAuthA, {} as never)) as Record<string, unknown>;
|
||||
const rB = (await b.auth!.loader!(getAuthB, {} as never)) as Record<string, unknown>;
|
||||
|
||||
assert.equal(rA.apiKey, "sk-prod");
|
||||
assert.equal(rA.baseURL, "https://prod.example/v1");
|
||||
assert.equal(rB.apiKey, "sk-preprod");
|
||||
assert.equal(rB.baseURL, "https://preprod.example/v1");
|
||||
});
|
||||
|
||||
test("multi-instance: invalid opts on one instance does not poison the other", async () => {
|
||||
// Sequencing: bad opts → good opts. The bad call must throw cleanly; the
|
||||
// good call must still produce a working hooks object. Confirms no
|
||||
// half-built module-level state survives a failed parse.
|
||||
await assert.rejects(
|
||||
() => OmniRoutePlugin(fakeInput, { providerId: "bad id!" } as never),
|
||||
/providerId/
|
||||
);
|
||||
const ok = await OmniRoutePlugin(fakeInput, {
|
||||
providerId: "recovered",
|
||||
baseURL: "https://ok.example/v1",
|
||||
});
|
||||
assert.equal(ok.auth?.provider, "recovered");
|
||||
});
|
||||
104
@omniroute/opencode-plugin/tests/options-schema.test.ts
Normal file
104
@omniroute/opencode-plugin/tests/options-schema.test.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* T-08 options-schema tests.
|
||||
*
|
||||
* Covers `parseOmniRoutePluginOptions(opts)` — the strict Zod gate that
|
||||
* validates the second-arg `PluginOptions` bag from opencode.json before
|
||||
* any hook is wired. Anti-pattern checklist mirrored here:
|
||||
*
|
||||
* - `null` / `undefined` must collapse to `{}` (defaults apply downstream).
|
||||
* - Unknown keys must THROW (`.strict()` catches opencode.json typos).
|
||||
* - Validation runs at parse time, not import time (module loads cleanly).
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { parseOmniRoutePluginOptions } from "../src/index.js";
|
||||
|
||||
test("parseOmniRoutePluginOptions: undefined → {}", () => {
|
||||
assert.deepEqual(parseOmniRoutePluginOptions(undefined), {});
|
||||
});
|
||||
|
||||
test("parseOmniRoutePluginOptions: null → {}", () => {
|
||||
assert.deepEqual(parseOmniRoutePluginOptions(null), {});
|
||||
});
|
||||
|
||||
test("parseOmniRoutePluginOptions: empty object → {}", () => {
|
||||
assert.deepEqual(parseOmniRoutePluginOptions({}), {});
|
||||
});
|
||||
|
||||
test("parseOmniRoutePluginOptions: valid providerId → returns it", () => {
|
||||
const r = parseOmniRoutePluginOptions({ providerId: "omniroute-preprod" });
|
||||
assert.equal(r.providerId, "omniroute-preprod");
|
||||
});
|
||||
|
||||
test("parseOmniRoutePluginOptions: invalid providerId (special chars) → throws", () => {
|
||||
assert.throws(
|
||||
() => parseOmniRoutePluginOptions({ providerId: "omniroute prod!" }),
|
||||
/providerId.*slug/i
|
||||
);
|
||||
});
|
||||
|
||||
test("parseOmniRoutePluginOptions: empty providerId → throws", () => {
|
||||
assert.throws(() => parseOmniRoutePluginOptions({ providerId: "" }), /providerId/i);
|
||||
});
|
||||
|
||||
test("parseOmniRoutePluginOptions: valid modelCacheTtl → returns it", () => {
|
||||
const r = parseOmniRoutePluginOptions({ modelCacheTtl: 60_000 });
|
||||
assert.equal(r.modelCacheTtl, 60_000);
|
||||
});
|
||||
|
||||
test("parseOmniRoutePluginOptions: negative modelCacheTtl → throws", () => {
|
||||
assert.throws(() => parseOmniRoutePluginOptions({ modelCacheTtl: -1 }), /modelCacheTtl/i);
|
||||
});
|
||||
|
||||
test("parseOmniRoutePluginOptions: zero modelCacheTtl → throws (positive required)", () => {
|
||||
assert.throws(() => parseOmniRoutePluginOptions({ modelCacheTtl: 0 }), /modelCacheTtl/i);
|
||||
});
|
||||
|
||||
test("parseOmniRoutePluginOptions: invalid baseURL (not a URL) → throws", () => {
|
||||
assert.throws(() => parseOmniRoutePluginOptions({ baseURL: "not-a-url" }), /baseURL/i);
|
||||
});
|
||||
|
||||
test("parseOmniRoutePluginOptions: unknown key → throws (strict mode catches typos)", () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
parseOmniRoutePluginOptions({
|
||||
providerId: "omniroute",
|
||||
provider_id: "typo-here",
|
||||
}),
|
||||
/provider_id|unrecognized/i
|
||||
);
|
||||
});
|
||||
|
||||
test("parseOmniRoutePluginOptions: all four fields populated correctly → returns them", () => {
|
||||
const opts = {
|
||||
providerId: "omniroute-prod",
|
||||
displayName: "OmniRoute Production",
|
||||
modelCacheTtl: 120_000,
|
||||
baseURL: "https://or.example.com/v1",
|
||||
};
|
||||
const r = parseOmniRoutePluginOptions(opts);
|
||||
assert.deepEqual(r, opts);
|
||||
});
|
||||
|
||||
test("parseOmniRoutePluginOptions: error message lists every issue path", () => {
|
||||
// Two bad fields at once → error string should mention BOTH.
|
||||
try {
|
||||
parseOmniRoutePluginOptions({
|
||||
providerId: "",
|
||||
baseURL: "garbage",
|
||||
});
|
||||
assert.fail("expected throw");
|
||||
} catch (err) {
|
||||
const msg = (err as Error).message;
|
||||
assert.match(msg, /providerId/);
|
||||
assert.match(msg, /baseURL/);
|
||||
}
|
||||
});
|
||||
|
||||
test("parseOmniRoutePluginOptions: module import alone does NOT throw", async () => {
|
||||
// Re-importing the entry must not trigger validation; validation only fires
|
||||
// on explicit parseOmniRoutePluginOptions / OmniRoutePlugin invocation.
|
||||
const mod = await import("../src/index.js");
|
||||
assert.equal(typeof mod.parseOmniRoutePluginOptions, "function");
|
||||
});
|
||||
269
@omniroute/opencode-plugin/tests/provider.test.ts
Normal file
269
@omniroute/opencode-plugin/tests/provider.test.ts
Normal file
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* T-03 provider-hook contract tests.
|
||||
*
|
||||
* Covers `createOmniRouteProviderHook(opts, deps)`:
|
||||
* - hook.id binds to resolved providerId (single + multi-instance)
|
||||
* - models() narrows ctx.auth, fetches via injected fetcher, caches per
|
||||
* (baseURL, apiKey) tuple, refetches after TTL
|
||||
* - mapRawModelToModelV2 emits a v2 Model shape matching the
|
||||
* @opencode-ai/sdk/v2 type
|
||||
*
|
||||
* Mocking strategy: the fetcher is dependency-injected at hook construction
|
||||
* (`deps.fetcher`). No global fetch monkey-patch needed. `deps.now` lets us
|
||||
* fast-forward time deterministically for TTL assertions.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
createOmniRouteProviderHook,
|
||||
mapRawModelToModelV2,
|
||||
type OmniRouteRawModelEntry,
|
||||
type OmniRouteModelsFetcher,
|
||||
} from "../src/index.js";
|
||||
|
||||
const FIXTURE: OmniRouteRawModelEntry[] = [
|
||||
{
|
||||
id: "claude-primary",
|
||||
object: "model",
|
||||
owned_by: "combo",
|
||||
capabilities: { tool_calling: true, reasoning: true, vision: true, thinking: true },
|
||||
context_length: 200000,
|
||||
max_output_tokens: 64000,
|
||||
input_modalities: ["text", "image"],
|
||||
output_modalities: ["text"],
|
||||
},
|
||||
{
|
||||
id: "claude-low",
|
||||
object: "model",
|
||||
owned_by: "combo",
|
||||
capabilities: { tool_calling: true, reasoning: true, vision: true, thinking: false },
|
||||
context_length: 200000,
|
||||
max_output_tokens: 64000,
|
||||
input_modalities: ["text", "image"],
|
||||
output_modalities: ["text"],
|
||||
},
|
||||
{
|
||||
id: "gemini-3-flash",
|
||||
object: "model",
|
||||
owned_by: "google",
|
||||
capabilities: { tool_calling: true, reasoning: false, vision: true, thinking: false },
|
||||
context_length: 1000000,
|
||||
max_output_tokens: 8192,
|
||||
input_modalities: ["text", "image"],
|
||||
output_modalities: ["text"],
|
||||
},
|
||||
];
|
||||
|
||||
function stubFetcher(payload: OmniRouteRawModelEntry[]): OmniRouteModelsFetcher & {
|
||||
callCount: () => number;
|
||||
callsBy: () => Array<[string, string]>;
|
||||
} {
|
||||
let calls: Array<[string, string]> = [];
|
||||
const f: OmniRouteModelsFetcher = async (baseURL, apiKey) => {
|
||||
calls.push([baseURL, apiKey]);
|
||||
return payload;
|
||||
};
|
||||
return Object.assign(f, {
|
||||
callCount: () => calls.length,
|
||||
callsBy: () => calls,
|
||||
});
|
||||
}
|
||||
|
||||
const apiAuth = (key: string, baseURL?: string): unknown =>
|
||||
baseURL ? { type: "api", key, baseURL } : { type: "api", key };
|
||||
|
||||
test("createOmniRouteProviderHook: default providerId is 'omniroute'", () => {
|
||||
const hook = createOmniRouteProviderHook(undefined, { combosFetcher: async () => [] });
|
||||
assert.equal(hook.id, "omniroute");
|
||||
});
|
||||
|
||||
test("createOmniRouteProviderHook: custom providerId binds to hook.id (multi-instance)", () => {
|
||||
const a = createOmniRouteProviderHook(
|
||||
{ providerId: "omniroute-preprod" },
|
||||
{ combosFetcher: async () => [] }
|
||||
);
|
||||
const b = createOmniRouteProviderHook(
|
||||
{ providerId: "omniroute-local" },
|
||||
{ combosFetcher: async () => [] }
|
||||
);
|
||||
assert.equal(a.id, "omniroute-preprod");
|
||||
assert.equal(b.id, "omniroute-local");
|
||||
});
|
||||
|
||||
test("models: extracts apiKey from ctx.auth (type=api) and calls fetcher with it", async () => {
|
||||
const fetcher = stubFetcher(FIXTURE);
|
||||
const hook = createOmniRouteProviderHook(
|
||||
{ baseURL: "https://or.example.com/v1" },
|
||||
{ fetcher, combosFetcher: async () => [] }
|
||||
);
|
||||
const out = await hook.models!({} as never, { auth: apiAuth("sk-abc") as never });
|
||||
assert.equal(fetcher.callCount(), 1);
|
||||
assert.deepEqual(fetcher.callsBy()[0], ["https://or.example.com/v1", "sk-abc"]);
|
||||
assert.equal(Object.keys(out).length, 3);
|
||||
assert.ok(out["claude-primary"]);
|
||||
});
|
||||
|
||||
test("models: returns {} when ctx.auth is null/undefined/wrong-type/empty-key", async () => {
|
||||
const fetcher = stubFetcher(FIXTURE);
|
||||
const hook = createOmniRouteProviderHook(
|
||||
{ baseURL: "https://or.example.com/v1" },
|
||||
{ fetcher, combosFetcher: async () => [] }
|
||||
);
|
||||
|
||||
assert.deepEqual(await hook.models!({} as never, {} as never), {});
|
||||
assert.deepEqual(await hook.models!({} as never, { auth: undefined } as never), {});
|
||||
assert.deepEqual(
|
||||
await hook.models!({} as never, {
|
||||
auth: { type: "oauth", refresh: "r", access: "a", expires: 0 } as never,
|
||||
}),
|
||||
{}
|
||||
);
|
||||
assert.deepEqual(
|
||||
await hook.models!({} as never, { auth: { type: "api", key: "" } as never }),
|
||||
{}
|
||||
);
|
||||
assert.equal(fetcher.callCount(), 0, "fetcher must not be called on auth rejection");
|
||||
});
|
||||
|
||||
test("models: returns {} when no baseURL resolvable (no opts.baseURL and no auth.baseURL)", async () => {
|
||||
const fetcher = stubFetcher(FIXTURE);
|
||||
const hook = createOmniRouteProviderHook({}, { fetcher, combosFetcher: async () => [] });
|
||||
// valid api auth but neither opts nor auth carries a baseURL
|
||||
assert.deepEqual(await hook.models!({} as never, { auth: apiAuth("sk-x") as never }), {});
|
||||
assert.equal(fetcher.callCount(), 0);
|
||||
});
|
||||
|
||||
test("models: baseURL falls back to auth.baseURL when opts.baseURL absent", async () => {
|
||||
const fetcher = stubFetcher(FIXTURE);
|
||||
const hook = createOmniRouteProviderHook({}, { fetcher, combosFetcher: async () => [] });
|
||||
const out = await hook.models!({} as never, {
|
||||
auth: apiAuth("sk-y", "https://or.creds-attached.example/v1") as never,
|
||||
});
|
||||
assert.equal(fetcher.callCount(), 1);
|
||||
assert.equal(fetcher.callsBy()[0][0], "https://or.creds-attached.example/v1");
|
||||
assert.equal(Object.keys(out).length, 3);
|
||||
});
|
||||
|
||||
test("models: maps a sample /v1/models entry to ModelV2 (sanity)", async () => {
|
||||
const fetcher = stubFetcher(FIXTURE);
|
||||
const hook = createOmniRouteProviderHook(
|
||||
{ providerId: "omniroute", baseURL: "https://or.example.com/v1" },
|
||||
{ fetcher, combosFetcher: async () => [] }
|
||||
);
|
||||
const out = await hook.models!({} as never, { auth: apiAuth("sk-abc") as never });
|
||||
const claude = out["claude-primary"];
|
||||
assert.ok(claude, "claude-primary present");
|
||||
assert.equal(claude.id, "claude-primary");
|
||||
assert.equal(claude.name, "claude-primary");
|
||||
assert.equal(claude.providerID, "omniroute");
|
||||
assert.equal(claude.api.id, "openai-compatible");
|
||||
assert.equal(claude.api.url, "https://or.example.com/v1");
|
||||
assert.equal(claude.api.npm, "@ai-sdk/openai-compatible");
|
||||
// capabilities: toolcall (one word), reasoning OR thinking, attachment = vision
|
||||
assert.equal(claude.capabilities.toolcall, true);
|
||||
assert.equal(claude.capabilities.reasoning, true);
|
||||
assert.equal(claude.capabilities.attachment, true);
|
||||
assert.equal(claude.capabilities.temperature, true);
|
||||
// modalities mapped from arrays
|
||||
assert.equal(claude.capabilities.input.text, true);
|
||||
assert.equal(claude.capabilities.input.image, true);
|
||||
assert.equal(claude.capabilities.input.audio, false);
|
||||
assert.equal(claude.capabilities.output.text, true);
|
||||
assert.equal(claude.capabilities.output.image, false);
|
||||
// cost is zeroed (OmniRoute /v1/models has no pricing)
|
||||
assert.deepEqual(claude.cost, { input: 0, output: 0, cache: { read: 0, write: 0 } });
|
||||
// limits
|
||||
assert.equal(claude.limit.context, 200000);
|
||||
assert.equal(claude.limit.output, 64000);
|
||||
assert.equal(claude.status, "active");
|
||||
});
|
||||
|
||||
test("mapRawModelToModelV2: thinking-only model still surfaces reasoning=true", () => {
|
||||
const m = mapRawModelToModelV2(
|
||||
{
|
||||
id: "thinking-only",
|
||||
capabilities: { thinking: true, reasoning: false },
|
||||
context_length: 100000,
|
||||
max_output_tokens: 8192,
|
||||
},
|
||||
{ providerId: "omniroute", baseURL: "https://or.example.com/v1" }
|
||||
);
|
||||
assert.equal(m.capabilities.reasoning, true);
|
||||
});
|
||||
|
||||
test("mapRawModelToModelV2: missing capabilities defaults to all-false (except temperature)", () => {
|
||||
const m = mapRawModelToModelV2(
|
||||
{ id: "minimal" },
|
||||
{ providerId: "omniroute", baseURL: "https://or.example.com/v1" }
|
||||
);
|
||||
assert.equal(m.capabilities.temperature, true);
|
||||
assert.equal(m.capabilities.reasoning, false);
|
||||
assert.equal(m.capabilities.attachment, false);
|
||||
assert.equal(m.capabilities.toolcall, false);
|
||||
// default modalities = text only
|
||||
assert.equal(m.capabilities.input.text, true);
|
||||
assert.equal(m.capabilities.output.text, true);
|
||||
// missing context / output tokens → 0 fallback (ModelV2.limit.{context,output} required)
|
||||
assert.equal(m.limit.context, 0);
|
||||
assert.equal(m.limit.output, 0);
|
||||
});
|
||||
|
||||
test("models: caches result for second call within TTL (fetcher called once)", async () => {
|
||||
const fetcher = stubFetcher(FIXTURE);
|
||||
let nowMs = 1_000_000;
|
||||
const hook = createOmniRouteProviderHook(
|
||||
{ baseURL: "https://or.example.com/v1", modelCacheTtl: 60_000 },
|
||||
{ fetcher, now: () => nowMs, combosFetcher: async () => [] }
|
||||
);
|
||||
|
||||
const a = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
|
||||
nowMs += 30_000; // half the TTL
|
||||
const b = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
|
||||
assert.equal(fetcher.callCount(), 1, "second call within TTL must hit the cache");
|
||||
assert.equal(Object.keys(a).length, 3);
|
||||
assert.equal(Object.keys(b).length, 3);
|
||||
});
|
||||
|
||||
test("models: refetches after TTL expires", async () => {
|
||||
const fetcher = stubFetcher(FIXTURE);
|
||||
let nowMs = 1_000_000;
|
||||
const hook = createOmniRouteProviderHook(
|
||||
{ baseURL: "https://or.example.com/v1", modelCacheTtl: 60_000 },
|
||||
{ fetcher, now: () => nowMs, combosFetcher: async () => [] }
|
||||
);
|
||||
|
||||
await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
|
||||
nowMs += 60_001; // just past the TTL
|
||||
await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
|
||||
assert.equal(fetcher.callCount(), 2, "call past TTL must refetch");
|
||||
});
|
||||
|
||||
test("models: caches per (baseURL, apiKey) tuple (different keys → independent fetches)", async () => {
|
||||
const fetcher = stubFetcher(FIXTURE);
|
||||
const hook = createOmniRouteProviderHook(
|
||||
{ baseURL: "https://or.example.com/v1", modelCacheTtl: 300_000 },
|
||||
{ fetcher, combosFetcher: async () => [] }
|
||||
);
|
||||
|
||||
await hook.models!({} as never, { auth: apiAuth("sk-A") as never });
|
||||
await hook.models!({} as never, { auth: apiAuth("sk-B") as never });
|
||||
await hook.models!({} as never, { auth: apiAuth("sk-A") as never }); // cached
|
||||
await hook.models!({} as never, { auth: apiAuth("sk-B") as never }); // cached
|
||||
assert.equal(fetcher.callCount(), 2, "one fetch per distinct apiKey, then cache hits");
|
||||
});
|
||||
|
||||
test("models: caches per (baseURL, apiKey) tuple (different baseURL → independent fetches)", async () => {
|
||||
const fetcher = stubFetcher(FIXTURE);
|
||||
const hook = createOmniRouteProviderHook(
|
||||
{ modelCacheTtl: 300_000 }, // no opts.baseURL → falls back to auth.baseURL
|
||||
{ fetcher, combosFetcher: async () => [] }
|
||||
);
|
||||
|
||||
await hook.models!({} as never, { auth: apiAuth("sk-same", "https://prod.example/v1") as never });
|
||||
await hook.models!({} as never, {
|
||||
auth: apiAuth("sk-same", "https://preprod.example/v1") as never,
|
||||
});
|
||||
await hook.models!({} as never, { auth: apiAuth("sk-same", "https://prod.example/v1") as never }); // cached
|
||||
assert.equal(fetcher.callCount(), 2, "distinct baseURLs share apiKey but not cache");
|
||||
});
|
||||
73
@omniroute/opencode-plugin/tests/scaffold.test.ts
Normal file
73
@omniroute/opencode-plugin/tests/scaffold.test.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createRequire } from "node:module";
|
||||
import {
|
||||
OmniRoutePlugin,
|
||||
OMNIROUTE_PROVIDER_KEY,
|
||||
DEFAULT_MODEL_CACHE_TTL_MS,
|
||||
resolveOmniRoutePluginOptions,
|
||||
} from "../src/index.js";
|
||||
|
||||
test("scaffold: exports public surface", () => {
|
||||
assert.equal(
|
||||
typeof OmniRoutePlugin,
|
||||
"function",
|
||||
"OmniRoutePlugin must be a function (Plugin factory)"
|
||||
);
|
||||
assert.equal(OMNIROUTE_PROVIDER_KEY, "omniroute");
|
||||
assert.equal(DEFAULT_MODEL_CACHE_TTL_MS, 300_000);
|
||||
});
|
||||
|
||||
test("scaffold: default export is v1 plugin shape { id, server: OmniRoutePlugin }", async () => {
|
||||
const mod = await import("../src/index.js");
|
||||
assert.equal(typeof mod.default, "object");
|
||||
assert.equal(mod.default.id, "@omniroute/opencode-plugin");
|
||||
assert.equal(mod.default.server, mod.OmniRoutePlugin);
|
||||
});
|
||||
|
||||
test("resolveOmniRoutePluginOptions: defaults", () => {
|
||||
const r = resolveOmniRoutePluginOptions();
|
||||
assert.equal(r.providerId, "omniroute");
|
||||
assert.equal(r.displayName, "OmniRoute");
|
||||
assert.equal(r.modelCacheTtl, 300_000);
|
||||
assert.equal(r.baseURL, undefined);
|
||||
});
|
||||
|
||||
test("resolveOmniRoutePluginOptions: custom providerId derives displayName", () => {
|
||||
const r = resolveOmniRoutePluginOptions({ providerId: "omniroute-preprod" });
|
||||
assert.equal(r.providerId, "omniroute-preprod");
|
||||
assert.equal(r.displayName, "OmniRoute (omniroute-preprod)");
|
||||
});
|
||||
|
||||
test("resolveOmniRoutePluginOptions: explicit displayName wins", () => {
|
||||
const r = resolveOmniRoutePluginOptions({
|
||||
providerId: "omniroute-x",
|
||||
displayName: "Custom Label",
|
||||
});
|
||||
assert.equal(r.displayName, "Custom Label");
|
||||
});
|
||||
|
||||
test("resolveOmniRoutePluginOptions: invalid TTL falls back to default", () => {
|
||||
assert.equal(resolveOmniRoutePluginOptions({ modelCacheTtl: 0 }).modelCacheTtl, 300_000);
|
||||
assert.equal(resolveOmniRoutePluginOptions({ modelCacheTtl: -1 }).modelCacheTtl, 300_000);
|
||||
});
|
||||
|
||||
test("resolveOmniRoutePluginOptions: positive TTL respected", () => {
|
||||
assert.equal(resolveOmniRoutePluginOptions({ modelCacheTtl: 60_000 }).modelCacheTtl, 60_000);
|
||||
});
|
||||
|
||||
test("OmniRoutePlugin: returns an empty hooks object (scaffold)", async () => {
|
||||
const fakeCtx = {} as Parameters<typeof OmniRoutePlugin>[0];
|
||||
const hooks = await OmniRoutePlugin(fakeCtx);
|
||||
assert.equal(typeof hooks, "object");
|
||||
assert.notEqual(hooks, null);
|
||||
});
|
||||
|
||||
test("scaffold: CJS default export resolves via require() with v1 shape", () => {
|
||||
const require_ = createRequire(import.meta.url);
|
||||
const cjs = require_("../dist/index.cjs");
|
||||
// after cjsInterop:true, default export is on cjs.default
|
||||
assert.strictEqual(typeof cjs.default, "object");
|
||||
assert.strictEqual(cjs.default.id, "@omniroute/opencode-plugin");
|
||||
assert.strictEqual(typeof cjs.default.server, "function");
|
||||
});
|
||||
85
@omniroute/opencode-plugin/tests/usable-combo.test.ts
Normal file
85
@omniroute/opencode-plugin/tests/usable-combo.test.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Regression tests for `isUsableCombo` (release/v3.8.2 code review, finding C1).
|
||||
*
|
||||
* The combo member refs returned by `/api/combos` do NOT carry a separate
|
||||
* `providerId` field — OmniRoute's `normalizeComboRecord` folds the provider
|
||||
* id INTO the full model string (e.g. "cc/claude-opus-4-7"). The previous
|
||||
* implementation read `step.providerId` (always `undefined`), so the
|
||||
* `usableOnly` combo filter silently never dropped anything. These tests pin
|
||||
* the corrected behavior: the verdict is derived from the `step.model` prefix,
|
||||
* mirroring `isUsableRawModelId`'s subtract-filter semantics.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { isUsableCombo, type OmniRouteRawCombo } from "../src/index.js";
|
||||
|
||||
/** Build a `usable` set bundle for the tests. */
|
||||
function buildUsable(opts: { aliases?: string[]; canonicals?: string[]; known?: string[] }): {
|
||||
aliases: Set<string>;
|
||||
canonicals: Set<string>;
|
||||
knownAliases: Set<string>;
|
||||
} {
|
||||
return {
|
||||
aliases: new Set(opts.aliases ?? []),
|
||||
canonicals: new Set(opts.canonicals ?? []),
|
||||
// knownAliases is the union of every prefix the universe is aware of —
|
||||
// usable or not. Default to including the usable aliases too.
|
||||
knownAliases: new Set([...(opts.known ?? []), ...(opts.aliases ?? [])]),
|
||||
};
|
||||
}
|
||||
|
||||
function combo(models: OmniRouteRawCombo["models"]): OmniRouteRawCombo {
|
||||
return { id: "c1", name: "Test Combo", models };
|
||||
}
|
||||
|
||||
test("isUsableCombo: member with a usable alias prefix → keep", () => {
|
||||
const usable = buildUsable({ aliases: ["cc"], known: ["cc", "dead"] });
|
||||
const c = combo([{ kind: "model", model: "cc/claude-opus-4-7" }]);
|
||||
assert.equal(isUsableCombo(c, usable), true);
|
||||
});
|
||||
|
||||
test("isUsableCombo: all members known-but-NOT-usable → drop (the C1 regression)", () => {
|
||||
// Before the fix this returned true unconditionally because step.providerId
|
||||
// was always undefined. Now the known-but-unusable "dead" prefix is dropped.
|
||||
const usable = buildUsable({ aliases: ["cc"], known: ["cc", "dead"] });
|
||||
const c = combo([
|
||||
{ kind: "model", model: "dead/legacy-model" },
|
||||
{ kind: "model", model: "dead/another" },
|
||||
]);
|
||||
assert.equal(isUsableCombo(c, usable), false);
|
||||
});
|
||||
|
||||
test("isUsableCombo: unknown prefix → keep (cannot prove unroutable)", () => {
|
||||
const usable = buildUsable({ aliases: ["cc"], known: ["cc", "dead"] });
|
||||
const c = combo([{ kind: "model", model: "agentrouter/mystery" }]);
|
||||
assert.equal(isUsableCombo(c, usable), true);
|
||||
});
|
||||
|
||||
test("isUsableCombo: mixed non-usable + usable member → keep", () => {
|
||||
const usable = buildUsable({ aliases: ["cc"], known: ["cc", "dead"] });
|
||||
const c = combo([
|
||||
{ kind: "model", model: "dead/legacy" },
|
||||
{ kind: "model", model: "cc/claude-opus-4-7" },
|
||||
]);
|
||||
assert.equal(isUsableCombo(c, usable), true);
|
||||
});
|
||||
|
||||
test("isUsableCombo: zero members → keep", () => {
|
||||
const usable = buildUsable({ aliases: ["cc"], known: ["cc"] });
|
||||
assert.equal(isUsableCombo(combo([]), usable), true);
|
||||
assert.equal(isUsableCombo(combo(undefined), usable), true);
|
||||
});
|
||||
|
||||
test("isUsableCombo: only combo-ref steps (no resolvable model) → keep", () => {
|
||||
const usable = buildUsable({ aliases: ["cc"], known: ["cc", "dead"] });
|
||||
const c = combo([{ kind: "combo-ref", comboName: "nested" }]);
|
||||
assert.equal(isUsableCombo(c, usable), true);
|
||||
});
|
||||
|
||||
test("isUsableCombo: usable canonical prefix → keep", () => {
|
||||
const usable = buildUsable({ canonicals: ["anthropic"], known: ["anthropic", "dead"] });
|
||||
const c = combo([{ kind: "model", model: "anthropic/claude-opus-4-7" }]);
|
||||
assert.equal(isUsableCombo(c, usable), true);
|
||||
});
|
||||
20
@omniroute/opencode-plugin/tsconfig.json
Normal file
20
@omniroute/opencode-plugin/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"lib": ["ES2022"],
|
||||
"types": ["node"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true,
|
||||
"isolatedModules": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noUncheckedIndexedAccess": false,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["dist", "node_modules", "tests"]
|
||||
}
|
||||
20
@omniroute/opencode-plugin/tsup.config.ts
Normal file
20
@omniroute/opencode-plugin/tsup.config.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { defineConfig } from "tsup";
|
||||
|
||||
export default defineConfig({
|
||||
entry: ["src/index.ts"],
|
||||
format: ["esm", "cjs"],
|
||||
dts: true,
|
||||
clean: true,
|
||||
sourcemap: false,
|
||||
splitting: false,
|
||||
treeshake: false,
|
||||
target: "node22",
|
||||
outDir: "dist",
|
||||
minify: false,
|
||||
cjsInterop: true,
|
||||
// Bundle runtime deps so the .tgz / npm install is self-contained.
|
||||
// `zod` is required at runtime by the options schema and would otherwise
|
||||
// need a peer install when the plugin is loaded directly from a file path
|
||||
// in opencode.jsonc.
|
||||
noExternal: ["zod"],
|
||||
});
|
||||
4
@omniroute/opencode-provider/.gitignore
vendored
Normal file
4
@omniroute/opencode-provider/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
dist
|
||||
*.log
|
||||
.DS_Store
|
||||
7
@omniroute/opencode-provider/.npmignore
Normal file
7
@omniroute/opencode-provider/.npmignore
Normal file
@@ -0,0 +1,7 @@
|
||||
src
|
||||
tests
|
||||
tsconfig.json
|
||||
tsup.config.ts
|
||||
node_modules
|
||||
.DS_Store
|
||||
*.log
|
||||
21
@omniroute/opencode-provider/LICENSE
Normal file
21
@omniroute/opencode-provider/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 OmniRoute contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
138
@omniroute/opencode-provider/README.md
Normal file
138
@omniroute/opencode-provider/README.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# @omniroute/opencode-provider
|
||||
|
||||
Helper for connecting [OpenCode](https://opencode.ai) to a running [OmniRoute](https://github.com/diegosouzapw/OmniRoute) AI gateway.
|
||||
|
||||
The package emits a **schema-valid entry** for `opencode.json` (`https://opencode.ai/config.json`) that delegates the actual runtime to [`@ai-sdk/openai-compatible`](https://www.npmjs.com/package/@ai-sdk/openai-compatible). It does not ship any new HTTP client — OmniRoute already exposes an OpenAI-compatible surface, and OpenCode already speaks it through the AI SDK.
|
||||
|
||||
> Pre-1.0. The API may still change. See `CHANGELOG` in the OmniRoute repo for breaking notes.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install --save-dev @omniroute/opencode-provider
|
||||
# or
|
||||
pnpm add -D @omniroute/opencode-provider
|
||||
```
|
||||
|
||||
You also need OpenCode's own runtime dep, but that's a transitive concern — OpenCode itself ships with `@ai-sdk/openai-compatible`. This package only **generates configuration**.
|
||||
|
||||
## Quick start
|
||||
|
||||
### 1. Scaffold a fresh `opencode.json`
|
||||
|
||||
```ts
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { buildOmniRouteOpenCodeConfig } from "@omniroute/opencode-provider";
|
||||
|
||||
const config = buildOmniRouteOpenCodeConfig({
|
||||
baseURL: "http://localhost:20128", // or your OmniRoute deployment URL
|
||||
apiKey: process.env.OMNIROUTE_API_KEY ?? "sk_omniroute",
|
||||
});
|
||||
|
||||
writeFileSync("opencode.json", JSON.stringify(config, null, 2));
|
||||
```
|
||||
|
||||
The resulting `opencode.json`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"provider": {
|
||||
"omniroute": {
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"name": "OmniRoute",
|
||||
"options": {
|
||||
"baseURL": "http://localhost:20128/v1",
|
||||
"apiKey": "sk_omniroute",
|
||||
},
|
||||
"models": {
|
||||
"claude-opus-4-5-thinking": { "name": "claude-opus-4-5-thinking" },
|
||||
"claude-sonnet-4-5-thinking": { "name": "claude-sonnet-4-5-thinking" },
|
||||
"gemini-3.1-pro-high": { "name": "gemini-3.1-pro-high" },
|
||||
"gemini-3-flash": { "name": "gemini-3-flash" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Merge into an existing `opencode.json`
|
||||
|
||||
```ts
|
||||
import { createOmniRouteProvider } from "@omniroute/opencode-provider";
|
||||
|
||||
const provider = createOmniRouteProvider({
|
||||
baseURL: "http://localhost:20128",
|
||||
apiKey: process.env.OMNIROUTE_API_KEY!,
|
||||
});
|
||||
|
||||
// Place `provider` under provider.omniroute in your opencode.json
|
||||
```
|
||||
|
||||
If you already have an `opencode.json` on disk and want a non-destructive merge from the OmniRoute side, use `omniroute config opencode` from the CLI (ships with the main OmniRoute install) — it preserves comments and unrelated keys.
|
||||
|
||||
## API
|
||||
|
||||
### `createOmniRouteProvider(options): OpenCodeProviderEntry`
|
||||
|
||||
Returns the value to place under `provider.omniroute` inside `opencode.json`.
|
||||
|
||||
| Option | Type | Required | Description |
|
||||
| ------------- | ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------ |
|
||||
| `baseURL` | `string` | Yes | OmniRoute base URL. Accepts `http://host:port` **or** `http://host:port/v1`. Trailing slashes are tolerated. |
|
||||
| `apiKey` | `string` | Yes | OmniRoute API key. Use `sk_omniroute` for local installs that have `REQUIRE_API_KEY=false`. |
|
||||
| `displayName` | `string` | No | Custom name shown in the OpenCode UI. Default: `"OmniRoute"`. |
|
||||
| `models` | `string[]` | No | Override the surfaced model catalog. Default: 4 curated models — see `OMNIROUTE_DEFAULT_OPENCODE_MODELS`. |
|
||||
| `modelLabels` | `Record<string,string>` | No | Human-readable labels keyed by model id. |
|
||||
|
||||
Throws on empty/invalid input — `baseURL` must be a real URL, `apiKey` must be a non-empty string.
|
||||
|
||||
### `buildOmniRouteOpenCodeConfig(options): OpenCodeConfigDocument`
|
||||
|
||||
Same options as above, but returns a full document with `$schema` and the `provider.omniroute` wrapper, ready to write to `opencode.json`.
|
||||
|
||||
### `normalizeBaseURL(input): string`
|
||||
|
||||
Exported for completeness. Strips trailing `/`, deduplicates a trailing `/v1`, and re-appends exactly one `/v1`. Throws on empty / non-URL input.
|
||||
|
||||
### Constants
|
||||
|
||||
- `OMNIROUTE_PROVIDER_KEY` — `"omniroute"` (the key used under `provider.*`).
|
||||
- `OMNIROUTE_PROVIDER_NPM` — `"@ai-sdk/openai-compatible"` (the runtime delegate).
|
||||
- `OPENCODE_CONFIG_SCHEMA` — `"https://opencode.ai/config.json"`.
|
||||
- `OMNIROUTE_DEFAULT_OPENCODE_MODELS` — readonly list of 4 default model ids.
|
||||
|
||||
## Custom model catalog
|
||||
|
||||
```ts
|
||||
import { createOmniRouteProvider } from "@omniroute/opencode-provider";
|
||||
|
||||
createOmniRouteProvider({
|
||||
baseURL: "http://localhost:20128",
|
||||
apiKey: "sk_omniroute",
|
||||
models: ["auto", "claude-opus-4-7", "gpt-5.5"],
|
||||
modelLabels: {
|
||||
auto: "Auto-Combo (recommended)",
|
||||
"claude-opus-4-7": "Claude Opus 4.7",
|
||||
"gpt-5.5": "GPT-5.5",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Duplicates and empty strings are dropped automatically, and order is preserved.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Requests 404 with `/v1/v1/...`** — you're on an old version (≤1.0.0). Update to `≥0.1.0` of this re-released package. The new build normalises `baseURL` automatically.
|
||||
- **`401 Invalid API key`** — your OmniRoute instance has `REQUIRE_API_KEY=true` but the key you supplied doesn't exist there. Create one via the dashboard or set `REQUIRE_API_KEY=false` and use `sk_omniroute`.
|
||||
- **OpenCode complains the provider has no models** — supply an explicit `models` list; the default 4 may be hidden by your provider visibility settings.
|
||||
|
||||
## Related
|
||||
|
||||
- [OmniRoute](https://github.com/diegosouzapw/OmniRoute) — the AI gateway this plugin targets.
|
||||
- [OpenCode](https://opencode.ai) — the agentic CLI consumer.
|
||||
- [`@ai-sdk/openai-compatible`](https://www.npmjs.com/package/@ai-sdk/openai-compatible) — the runtime delegate that actually speaks HTTP.
|
||||
|
||||
## License
|
||||
|
||||
MIT — see [`LICENSE`](./LICENSE).
|
||||
1998
@omniroute/opencode-provider/package-lock.json
generated
Normal file
1998
@omniroute/opencode-provider/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
60
@omniroute/opencode-provider/package.json
Normal file
60
@omniroute/opencode-provider/package.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "@omniroute/opencode-provider",
|
||||
"version": "0.1.0",
|
||||
"description": "OpenCode provider helper for the OmniRoute AI Gateway. Generates a schema-valid provider entry for opencode.json that delegates the runtime to @ai-sdk/openai-compatible.",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"clean": "rm -rf dist",
|
||||
"test": "node --import tsx/esm --test tests/index.test.ts",
|
||||
"prepublishOnly": "npm run clean && npm run build && npm test"
|
||||
},
|
||||
"keywords": [
|
||||
"omniroute",
|
||||
"opencode",
|
||||
"opencode-ai",
|
||||
"ai-sdk",
|
||||
"openai-compatible",
|
||||
"provider",
|
||||
"ai-gateway",
|
||||
"llm-router"
|
||||
],
|
||||
"author": "OmniRoute contributors",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/diegosouzapw/OmniRoute.git",
|
||||
"directory": "@omniroute/opencode-provider"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/diegosouzapw/OmniRoute/issues"
|
||||
},
|
||||
"homepage": "https://github.com/diegosouzapw/OmniRoute/tree/main/%40omniroute/opencode-provider#readme",
|
||||
"engines": {
|
||||
"node": ">=22.22.3"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.19.19",
|
||||
"tsup": "^8.5.1",
|
||||
"tsx": "^4.22.3",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
886
@omniroute/opencode-provider/src/index.ts
Normal file
886
@omniroute/opencode-provider/src/index.ts
Normal file
@@ -0,0 +1,886 @@
|
||||
/**
|
||||
* OpenCode provider plugin for OmniRoute AI Gateway.
|
||||
*
|
||||
* Generates an OpenCode-compatible provider object that points to a running
|
||||
* OmniRoute instance. The output follows the OpenCode config schema
|
||||
* (https://opencode.ai/config.json) and delegates the runtime to
|
||||
* `@ai-sdk/openai-compatible` so OpenCode can drive any OmniRoute-exposed
|
||||
* model through its standard OpenAI-compatible client.
|
||||
*
|
||||
* Two ways to consume the helper:
|
||||
*
|
||||
* 1. As code, when you build your own opencode.json programmatically:
|
||||
*
|
||||
* ```ts
|
||||
* import { buildOmniRouteOpenCodeConfig } from "@omniroute/opencode-provider";
|
||||
* const config = buildOmniRouteOpenCodeConfig({
|
||||
* baseURL: "http://localhost:20128",
|
||||
* apiKey: "sk_omniroute",
|
||||
* });
|
||||
* // config -> { $schema, provider: { omniroute: { npm, name, options, models } } }
|
||||
* ```
|
||||
*
|
||||
* 2. As a single-provider entry to merge into an existing opencode.json:
|
||||
*
|
||||
* ```ts
|
||||
* import { createOmniRouteProvider } from "@omniroute/opencode-provider";
|
||||
* const provider = createOmniRouteProvider({ baseURL, apiKey });
|
||||
* // provider -> the value to place under provider.omniroute in opencode.json
|
||||
* ```
|
||||
*
|
||||
* Note: `baseURL` accepts both `http://host:port` and `http://host:port/v1`.
|
||||
* The helper normalises trailing slashes / `/v1` so you never get `/v1/v1`.
|
||||
*/
|
||||
|
||||
export const OMNIROUTE_PROVIDER_KEY = "omniroute" as const;
|
||||
export const OMNIROUTE_PROVIDER_NPM = "@ai-sdk/openai-compatible" as const;
|
||||
export const OPENCODE_CONFIG_SCHEMA = "https://opencode.ai/config.json" as const;
|
||||
|
||||
/**
|
||||
* Default catalog of models surfaced to OpenCode when the caller does not
|
||||
* supply an explicit `models` list.
|
||||
*
|
||||
* Curated set covering the most commonly deployed OmniRoute models. Synced
|
||||
* with the Alph4d0g/opencode-omniroute-auth OMNIROUTE_DEFAULT_MODELS constant
|
||||
* (https://github.com/Alph4d0g/opencode-omniroute-auth, MIT) and extended
|
||||
* with Claude Code passthrough models (`cc/` prefix).
|
||||
*/
|
||||
export const OMNIROUTE_DEFAULT_OPENCODE_MODELS = [
|
||||
"cc/claude-opus-4-7",
|
||||
"cc/claude-sonnet-4-6",
|
||||
"cc/claude-haiku-4-5-20251001",
|
||||
"claude-opus-4-5-thinking",
|
||||
"claude-sonnet-4-5-thinking",
|
||||
"gemini-3.1-pro-high",
|
||||
"gemini-3-flash",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Optional capability flags surfaced to OpenCode's model picker.
|
||||
*
|
||||
* OpenCode reads these per-model keys (snake_case in JSON) to render badges
|
||||
* and to gate features such as image attachments, reasoning mode, temperature
|
||||
* controls and tool-calling. Omitted flags default to OpenCode's heuristics.
|
||||
*
|
||||
* Mirrors the capability shape used by Alph4d0g/opencode-omniroute-auth
|
||||
* (https://github.com/Alph4d0g/opencode-omniroute-auth, MIT).
|
||||
*/
|
||||
export interface ModelCapabilities {
|
||||
/** Display label shown in the model picker. Falls back to the model id. */
|
||||
label?: string;
|
||||
/** Model accepts image / file attachments. */
|
||||
attachment?: boolean;
|
||||
/** Model exposes a "reasoning" / extended-thinking surface. */
|
||||
reasoning?: boolean;
|
||||
/** Model honours the `temperature` parameter. */
|
||||
temperature?: boolean;
|
||||
/** Model supports tool / function calling. */
|
||||
tool_call?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default per-model context window sizes (tokens) for the curated default catalog.
|
||||
* Matches the context lengths used by OmniRoute's provider registry.
|
||||
*/
|
||||
export const OMNIROUTE_DEFAULT_MODEL_CONTEXT_LENGTHS: Record<string, number> = {
|
||||
"cc/claude-opus-4-7": 200_000,
|
||||
"cc/claude-sonnet-4-6": 200_000,
|
||||
"cc/claude-haiku-4-5-20251001": 200_000,
|
||||
"claude-opus-4-5-thinking": 200_000,
|
||||
"claude-sonnet-4-5-thinking": 200_000,
|
||||
"gemini-3.1-pro-high": 1_000_000,
|
||||
"gemini-3-flash": 1_000_000,
|
||||
};
|
||||
|
||||
/**
|
||||
* Default per-model capability hints for the curated default catalog.
|
||||
*
|
||||
* Conservative defaults: every default model accepts attachments, tool calls
|
||||
* and temperature; `reasoning` is opt-in per model id. Callers override per
|
||||
* model via `OmniRouteProviderOptions.modelCapabilities`.
|
||||
*/
|
||||
export const OMNIROUTE_DEFAULT_MODEL_CAPABILITIES: Record<string, ModelCapabilities> = {
|
||||
"cc/claude-opus-4-7": { attachment: true, reasoning: true, temperature: true, tool_call: true },
|
||||
"cc/claude-sonnet-4-6": { attachment: true, reasoning: true, temperature: true, tool_call: true },
|
||||
"cc/claude-haiku-4-5-20251001": { attachment: true, temperature: true, tool_call: true },
|
||||
"claude-opus-4-5-thinking": {
|
||||
attachment: true,
|
||||
reasoning: true,
|
||||
temperature: true,
|
||||
tool_call: true,
|
||||
},
|
||||
"claude-sonnet-4-5-thinking": {
|
||||
attachment: true,
|
||||
reasoning: true,
|
||||
temperature: true,
|
||||
tool_call: true,
|
||||
},
|
||||
"gemini-3.1-pro-high": { attachment: true, reasoning: true, temperature: true, tool_call: true },
|
||||
"gemini-3-flash": { attachment: true, temperature: true, tool_call: true },
|
||||
};
|
||||
|
||||
export interface OmniRouteProviderOptions {
|
||||
/** OmniRoute base URL, with or without trailing `/v1`. Required. */
|
||||
baseURL: string;
|
||||
/** OmniRoute API key. Required. Use `sk_omniroute` for local instances without REQUIRE_API_KEY. */
|
||||
apiKey: string;
|
||||
/** Override the display name shown in OpenCode. Default: `"OmniRoute"`. */
|
||||
displayName?: string;
|
||||
/** Override the model catalog. Defaults to `OMNIROUTE_DEFAULT_OPENCODE_MODELS`. */
|
||||
models?: readonly string[];
|
||||
/** Optional human-readable labels keyed by model id. Overridden by `modelCapabilities[id].label`. */
|
||||
modelLabels?: Record<string, string>;
|
||||
/**
|
||||
* Optional capability overrides keyed by model id. Merged on top of
|
||||
* `OMNIROUTE_DEFAULT_MODEL_CAPABILITIES` for ids in the default catalog;
|
||||
* for custom ids the override is used verbatim.
|
||||
*/
|
||||
modelCapabilities?: Record<string, ModelCapabilities>;
|
||||
/**
|
||||
* Primary model for OpenCode (top-level `model` key).
|
||||
* Emitted as `"omniroute/<id>"`. When omitted the key is not written.
|
||||
*/
|
||||
model?: string;
|
||||
/**
|
||||
* Secondary / cheap model for OpenCode (top-level `small_model` key).
|
||||
* Emitted as `"omniroute/<id>"`. When omitted the key is not written.
|
||||
*/
|
||||
smallModel?: string;
|
||||
}
|
||||
|
||||
/** Per-model entry written under `provider.omniroute.models[id]`. */
|
||||
export interface OpenCodeModelEntry {
|
||||
name: string;
|
||||
attachment?: boolean;
|
||||
reasoning?: boolean;
|
||||
temperature?: boolean;
|
||||
tool_call?: boolean;
|
||||
/**
|
||||
* Context window limit. OpenCode reads this to determine usable context
|
||||
* length for compaction, overflow detection, and router decisions.
|
||||
* Maps to `limit.context` in OpenCode's provider config schema.
|
||||
*/
|
||||
limit?: {
|
||||
/** Maximum context length in tokens (e.g. 200000 for Claude, 1000000 for Gemini). */
|
||||
context: number;
|
||||
/** Optional per-request max input tokens. */
|
||||
input?: number;
|
||||
/** Optional max output tokens. */
|
||||
output?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface OpenCodeProviderEntry {
|
||||
/** Identifier of the OpenCode runtime package that will speak to OmniRoute. */
|
||||
npm: typeof OMNIROUTE_PROVIDER_NPM;
|
||||
/** Display name in the OpenCode UI. */
|
||||
name: string;
|
||||
/** Options forwarded to `@ai-sdk/openai-compatible`. */
|
||||
options: {
|
||||
baseURL: string;
|
||||
apiKey: string;
|
||||
};
|
||||
/** Model catalog surfaced to OpenCode. */
|
||||
models: Record<string, OpenCodeModelEntry>;
|
||||
}
|
||||
|
||||
export interface OpenCodeConfigDocument {
|
||||
$schema: typeof OPENCODE_CONFIG_SCHEMA;
|
||||
/** Primary model for OpenCode, e.g. `"omniroute/claude-sonnet-4-5-thinking"`. */
|
||||
model?: string;
|
||||
/** Secondary / cheap model for OpenCode, e.g. `"omniroute/gemini-3-flash"`. */
|
||||
small_model?: string;
|
||||
provider: {
|
||||
[OMNIROUTE_PROVIDER_KEY]: OpenCodeProviderEntry;
|
||||
};
|
||||
}
|
||||
|
||||
function requireNonEmpty(value: unknown, field: string): string {
|
||||
if (typeof value !== "string") {
|
||||
throw new TypeError(`@omniroute/opencode-provider: ${field} must be a string`);
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
throw new Error(`@omniroute/opencode-provider: ${field} is required and cannot be empty`);
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalise the user-supplied baseURL so the final `options.baseURL` always
|
||||
* ends in exactly one `/v1`. Accepts both `http://host` and `http://host/v1`.
|
||||
*/
|
||||
export function normalizeBaseURL(rawBaseURL: string): string {
|
||||
const trimmed = requireNonEmpty(rawBaseURL, "baseURL");
|
||||
try {
|
||||
new URL(trimmed);
|
||||
} catch {
|
||||
throw new Error(
|
||||
`@omniroute/opencode-provider: baseURL is not a valid URL: ${JSON.stringify(rawBaseURL)}`
|
||||
);
|
||||
}
|
||||
let base = trimmed;
|
||||
let end = base.length;
|
||||
while (end > 0 && base[end - 1] === "/") end--;
|
||||
base = end < base.length ? base.slice(0, end) : base;
|
||||
if (base.endsWith("/v1")) base = base.slice(0, -3);
|
||||
return base + "/v1";
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the `provider.omniroute` entry for an OpenCode config document.
|
||||
* The returned object is JSON-serialisable and safe to embed verbatim.
|
||||
*/
|
||||
export function createOmniRouteProvider(options: OmniRouteProviderOptions): OpenCodeProviderEntry {
|
||||
const baseURL = normalizeBaseURL(options.baseURL);
|
||||
const apiKey = requireNonEmpty(options.apiKey, "apiKey");
|
||||
|
||||
const modelList =
|
||||
options.models && options.models.length > 0
|
||||
? [...options.models]
|
||||
: [...OMNIROUTE_DEFAULT_OPENCODE_MODELS];
|
||||
|
||||
const labels = options.modelLabels ?? {};
|
||||
const overrides = options.modelCapabilities ?? {};
|
||||
const models: Record<string, OpenCodeModelEntry> = {};
|
||||
const seen = new Set<string>();
|
||||
for (const raw of modelList) {
|
||||
const id = typeof raw === "string" ? raw.trim() : "";
|
||||
if (!id || seen.has(id)) continue;
|
||||
seen.add(id);
|
||||
const defaults = OMNIROUTE_DEFAULT_MODEL_CAPABILITIES[id] ?? {};
|
||||
const override = overrides[id] ?? {};
|
||||
const merged: ModelCapabilities = { ...defaults, ...override };
|
||||
const explicitLabel =
|
||||
typeof merged.label === "string" && merged.label.trim()
|
||||
? merged.label.trim()
|
||||
: typeof labels[id] === "string" && labels[id].trim()
|
||||
? labels[id].trim()
|
||||
: id;
|
||||
const entry: OpenCodeModelEntry = { name: explicitLabel };
|
||||
if (typeof merged.attachment === "boolean") entry.attachment = merged.attachment;
|
||||
if (typeof merged.reasoning === "boolean") entry.reasoning = merged.reasoning;
|
||||
if (typeof merged.temperature === "boolean") entry.temperature = merged.temperature;
|
||||
if (typeof merged.tool_call === "boolean") entry.tool_call = merged.tool_call;
|
||||
|
||||
// Include context window limit when known — OpenCode reads this to
|
||||
// determine usable context length for compaction & overflow detection.
|
||||
const contextLength = OMNIROUTE_DEFAULT_MODEL_CONTEXT_LENGTHS[id];
|
||||
if (typeof contextLength === "number" && contextLength > 0) {
|
||||
entry.limit = { context: contextLength };
|
||||
}
|
||||
|
||||
models[id] = entry;
|
||||
}
|
||||
|
||||
return {
|
||||
npm: OMNIROUTE_PROVIDER_NPM,
|
||||
name: options.displayName?.trim() || "OmniRoute",
|
||||
options: { baseURL, apiKey },
|
||||
models,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a full OpenCode config document (with `$schema` + `provider.omniroute`).
|
||||
* Useful when scaffolding a fresh `opencode.json`.
|
||||
*
|
||||
* When `options.model` / `options.smallModel` are supplied they are emitted as
|
||||
* top-level `model` / `small_model` keys prefixed with `"omniroute/"` so
|
||||
* OpenCode resolves them through the configured provider.
|
||||
*/
|
||||
export function buildOmniRouteOpenCodeConfig(
|
||||
options: OmniRouteProviderOptions
|
||||
): OpenCodeConfigDocument {
|
||||
const doc: OpenCodeConfigDocument = {
|
||||
$schema: OPENCODE_CONFIG_SCHEMA,
|
||||
provider: {
|
||||
[OMNIROUTE_PROVIDER_KEY]: createOmniRouteProvider(options),
|
||||
},
|
||||
};
|
||||
|
||||
if (options.model !== undefined) {
|
||||
const id = options.model.trim();
|
||||
if (id) doc.model = `${OMNIROUTE_PROVIDER_KEY}/${id}`;
|
||||
}
|
||||
|
||||
if (options.smallModel !== undefined) {
|
||||
const id = options.smallModel.trim();
|
||||
if (id) doc.small_model = `${OMNIROUTE_PROVIDER_KEY}/${id}`;
|
||||
}
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the OmniRoute provider entry (and optional `model` / `small_model`
|
||||
* keys) into an already-existing OpenCode config object.
|
||||
*
|
||||
* Performs a non-destructive merge: all top-level keys in `existing` are
|
||||
* preserved. The `provider` map is shallow-merged so other providers already
|
||||
* present are not removed. If `existing.provider.omniroute` already exists it
|
||||
* is overwritten by the newly built entry.
|
||||
*
|
||||
* `model` and `small_model` are only written when supplied in `options`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const existing = JSON.parse(readFileSync("opencode.json", "utf8"));
|
||||
* const updated = mergeIntoExistingConfig(existing, {
|
||||
* baseURL: "http://localhost:20128",
|
||||
* apiKey: "sk_omniroute",
|
||||
* model: "claude-sonnet-4-5-thinking",
|
||||
* });
|
||||
* writeFileSync("opencode.json", JSON.stringify(updated, null, 2));
|
||||
* ```
|
||||
*/
|
||||
export function mergeIntoExistingConfig(
|
||||
existing: Record<string, unknown>,
|
||||
options: OmniRouteProviderOptions
|
||||
): Record<string, unknown> {
|
||||
const partial = buildOmniRouteOpenCodeConfig(options);
|
||||
|
||||
const merged: Record<string, unknown> = { ...existing };
|
||||
|
||||
if (partial.model !== undefined) merged.model = partial.model;
|
||||
if (partial.small_model !== undefined) merged.small_model = partial.small_model;
|
||||
|
||||
const existingProvider =
|
||||
typeof existing.provider === "object" && existing.provider !== null
|
||||
? (existing.provider as Record<string, unknown>)
|
||||
: {};
|
||||
|
||||
merged.provider = {
|
||||
...existingProvider,
|
||||
[OMNIROUTE_PROVIDER_KEY]: partial.provider[OMNIROUTE_PROVIDER_KEY],
|
||||
};
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* The 7 read-only MCP scopes that allow inspection without any write access.
|
||||
* Suitable for shared / public environments.
|
||||
*/
|
||||
export const OMNIROUTE_MCP_DEFAULT_SCOPES = [
|
||||
"read:health",
|
||||
"read:combos",
|
||||
"read:quota",
|
||||
"read:usage",
|
||||
"read:models",
|
||||
"read:cache",
|
||||
"read:compression",
|
||||
] as const;
|
||||
|
||||
export type OmniRouteMCPScope = (typeof OMNIROUTE_MCP_DEFAULT_SCOPES)[number] | string;
|
||||
|
||||
export interface OmniRouteMCPOptions {
|
||||
/** Absolute path to the MCP server entry point (TypeScript or compiled JS). */
|
||||
serverPath: string;
|
||||
/** OmniRoute API key forwarded to the MCP server as `OMNIROUTE_API_KEY`. */
|
||||
apiKey: string;
|
||||
/**
|
||||
* Management API key used for management-scoped operations.
|
||||
* When supplied it is forwarded as `OMNIROUTE_MANAGEMENT_API_KEY`.
|
||||
*/
|
||||
managementApiKey?: string;
|
||||
/**
|
||||
* Comma-separated scope list passed as `OMNIROUTE_MCP_SCOPES`.
|
||||
* When omitted `OMNIROUTE_MCP_ENFORCE_SCOPES` is not set and all scopes are
|
||||
* available (development default). Pass an explicit list to restrict access.
|
||||
*/
|
||||
scopes?: OmniRouteMCPScope[];
|
||||
/**
|
||||
* Runtime used to execute the MCP server.
|
||||
*
|
||||
* - `"tsx"` (default) — runs via `npx tsx` for TypeScript source files.
|
||||
* - `"node"` — runs via `node` for compiled JS outputs.
|
||||
*/
|
||||
runtime?: "tsx" | "node";
|
||||
}
|
||||
|
||||
export interface OpenCodeMCPServerEntry {
|
||||
command: string;
|
||||
args: string[];
|
||||
env: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the `mcp.servers.omniroute` entry for an OpenCode config document.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const mcpEntry = createOmniRouteMCPEntry({
|
||||
* serverPath: "/home/user/.local/share/omniroute/open-sse/mcp-server/server.ts",
|
||||
* apiKey: "sk_omniroute",
|
||||
* managementApiKey: "sk_manage_...",
|
||||
* scopes: ["read:health", "read:combos", "execute:completions"],
|
||||
* });
|
||||
* // Place at config.mcp.servers.omniroute
|
||||
* ```
|
||||
*/
|
||||
export function createOmniRouteMCPEntry(options: OmniRouteMCPOptions): OpenCodeMCPServerEntry {
|
||||
const serverPath = requireNonEmpty(options.serverPath, "serverPath");
|
||||
const apiKey = requireNonEmpty(options.apiKey, "apiKey");
|
||||
|
||||
const runtime = options.runtime ?? "tsx";
|
||||
|
||||
const command = runtime === "tsx" ? "npx" : "node";
|
||||
const args = runtime === "tsx" ? ["tsx", serverPath] : [serverPath];
|
||||
|
||||
const env: Record<string, string> = {
|
||||
OMNIROUTE_API_KEY: apiKey,
|
||||
};
|
||||
|
||||
if (options.managementApiKey !== undefined) {
|
||||
const mgmtKey = options.managementApiKey.trim();
|
||||
if (mgmtKey) env.OMNIROUTE_MANAGEMENT_API_KEY = mgmtKey;
|
||||
}
|
||||
|
||||
if (options.scopes !== undefined && options.scopes.length > 0) {
|
||||
env.OMNIROUTE_MCP_ENFORCE_SCOPES = "true";
|
||||
env.OMNIROUTE_MCP_SCOPES = options.scopes.join(",");
|
||||
}
|
||||
|
||||
return { command, args, env };
|
||||
}
|
||||
|
||||
async function fetchJSON<T>(url: string, apiKey: string, timeoutMs: number): Promise<T> {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: { Authorization: `Bearer ${apiKey}` },
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`received HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return (await response.json()) as T;
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
throw new Error(`@omniroute/opencode-provider: request to ${url} failed: ${message}`);
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lightweight model descriptor returned by `fetchLiveModels`.
|
||||
* The shape mirrors the subset of fields that OmniRoute's `/v1/models`
|
||||
* endpoint reliably provides across versions, normalised from both
|
||||
* camelCase and snake_case variants used by different OmniRoute releases.
|
||||
*
|
||||
* Attribution: field-variant normalisation logic adapted from
|
||||
* https://github.com/Alph4d0g/opencode-omniroute-auth (MIT).
|
||||
*/
|
||||
export interface OmniRouteLiveModel {
|
||||
id: string;
|
||||
name: string;
|
||||
/** Context window length in tokens (e.g. 200000 for Claude, 1000000 for Gemini). */
|
||||
contextLength?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the live model catalog from a running OmniRoute instance.
|
||||
*
|
||||
* Returns an array of `{ id, name }` objects from `GET /v1/models`. Handles
|
||||
* both the camelCase (`modelId`, `displayName`) and snake_case (`model_id`,
|
||||
* `display_name`) field variants across OmniRoute versions.
|
||||
*
|
||||
* Useful for dynamically populating the `models` option of
|
||||
* `createOmniRouteProvider` / `buildOmniRouteOpenCodeConfig` instead of
|
||||
* relying on `OMNIROUTE_DEFAULT_OPENCODE_MODELS`.
|
||||
*
|
||||
* @param baseURL - OmniRoute base URL (with or without `/v1`).
|
||||
* @param apiKey - OmniRoute API key.
|
||||
* @param timeoutMs - Request timeout in milliseconds (default 5000).
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const models = await fetchLiveModels("http://localhost:20128", "sk_omniroute");
|
||||
* const config = buildOmniRouteOpenCodeConfig({
|
||||
* baseURL: "http://localhost:20128",
|
||||
* apiKey: "sk_omniroute",
|
||||
* models: models.map((m) => m.id),
|
||||
* modelLabels: Object.fromEntries(models.map((m) => [m.id, m.name])),
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export async function fetchLiveModels(
|
||||
baseURL: string,
|
||||
apiKey: string,
|
||||
timeoutMs = 5_000
|
||||
): Promise<OmniRouteLiveModel[]> {
|
||||
const key = requireNonEmpty(apiKey, "apiKey");
|
||||
const url = `${normalizeBaseURL(baseURL)}/models`;
|
||||
|
||||
const body = await fetchJSON<unknown>(url, key, timeoutMs);
|
||||
|
||||
const rawList: unknown[] = Array.isArray(body)
|
||||
? body
|
||||
: body && typeof body === "object" && Array.isArray((body as { data?: unknown[] }).data)
|
||||
? ((body as { data: unknown[] }).data as unknown[])
|
||||
: [];
|
||||
|
||||
const models: OmniRouteLiveModel[] = [];
|
||||
for (const raw of rawList) {
|
||||
if (typeof raw !== "object" || raw === null) continue;
|
||||
const r = raw as Record<string, unknown>;
|
||||
|
||||
const id =
|
||||
typeof r.id === "string"
|
||||
? r.id.trim()
|
||||
: typeof r.modelId === "string"
|
||||
? r.modelId.trim()
|
||||
: typeof r.model_id === "string"
|
||||
? r.model_id.trim()
|
||||
: "";
|
||||
|
||||
if (!id) continue;
|
||||
|
||||
const name =
|
||||
typeof r.name === "string"
|
||||
? r.name.trim()
|
||||
: typeof r.displayName === "string"
|
||||
? r.displayName.trim()
|
||||
: typeof r.display_name === "string"
|
||||
? r.display_name.trim()
|
||||
: id;
|
||||
|
||||
// Extract context_length from OmniRoute's /v1/models response.
|
||||
// OmniRoute returns context_length in snake_case for both synced
|
||||
// models (with inputTokenLimit) and custom models; the catalog's
|
||||
// getDefaultContextFallback also injects it from registry defaults.
|
||||
const contextLength =
|
||||
typeof r.context_length === "number" && r.context_length > 0
|
||||
? r.context_length
|
||||
: typeof r.max_context_window_tokens === "number" && r.max_context_window_tokens > 0
|
||||
? r.max_context_window_tokens
|
||||
: undefined;
|
||||
|
||||
models.push({ id, name: name || id, ...(contextLength ? { contextLength } : {}) });
|
||||
}
|
||||
|
||||
return models;
|
||||
}
|
||||
|
||||
/**
|
||||
* Valid per-combo compression override values.
|
||||
* An empty string clears any existing override (inherits global setting).
|
||||
*/
|
||||
export type OmniRouteCompressionOverride =
|
||||
| ""
|
||||
| "off"
|
||||
| "lite"
|
||||
| "standard"
|
||||
| "aggressive"
|
||||
| "ultra"
|
||||
| "rtk"
|
||||
| "stacked";
|
||||
|
||||
const VALID_COMPRESSION_OVERRIDES = new Set<string>([
|
||||
"",
|
||||
"off",
|
||||
"lite",
|
||||
"standard",
|
||||
"aggressive",
|
||||
"ultra",
|
||||
"rtk",
|
||||
"stacked",
|
||||
]);
|
||||
|
||||
/** Slim combo descriptor returned by `listCombos`. */
|
||||
export interface OmniRouteCombo {
|
||||
id: string;
|
||||
name: string;
|
||||
strategy: string;
|
||||
active: boolean;
|
||||
compressionOverride: OmniRouteCompressionOverride;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the active routing combo list from a running OmniRoute instance.
|
||||
*
|
||||
* Returns an array of combo descriptors from `GET /api/combos`. The
|
||||
* `compressionOverride` field reflects the per-combo compression strategy
|
||||
* (one of the 8 recognised values; empty string means "inherit global").
|
||||
*
|
||||
* Requires a management-scoped API key (Bearer `manage` scope) when the
|
||||
* instance has `REQUIRE_API_KEY` enabled.
|
||||
*
|
||||
* @param baseURL - OmniRoute base URL (with or without `/v1`).
|
||||
* @param managementApiKey - API key with `manage` scope.
|
||||
* @param timeoutMs - Request timeout in milliseconds (default 5000).
|
||||
*/
|
||||
export async function listCombos(
|
||||
baseURL: string,
|
||||
managementApiKey: string,
|
||||
timeoutMs = 5_000
|
||||
): Promise<OmniRouteCombo[]> {
|
||||
const key = requireNonEmpty(managementApiKey, "managementApiKey");
|
||||
const base = normalizeBaseURL(baseURL).replace(/\/v1$/, "");
|
||||
const url = `${base}/api/combos`;
|
||||
|
||||
const body = await fetchJSON<unknown>(url, key, timeoutMs);
|
||||
const rawList: unknown[] = Array.isArray(body)
|
||||
? body
|
||||
: body && typeof body === "object" && Array.isArray((body as { combos?: unknown[] }).combos)
|
||||
? ((body as { combos: unknown[] }).combos as unknown[])
|
||||
: [];
|
||||
|
||||
const combos: OmniRouteCombo[] = [];
|
||||
for (const raw of rawList) {
|
||||
if (typeof raw !== "object" || raw === null) continue;
|
||||
const r = raw as Record<string, unknown>;
|
||||
|
||||
const id = typeof r.id === "string" ? r.id.trim() : "";
|
||||
if (!id) continue;
|
||||
|
||||
const name = typeof r.name === "string" ? r.name.trim() : id;
|
||||
const strategy = typeof r.strategy === "string" ? r.strategy : "";
|
||||
const active = typeof r.active === "boolean" ? r.active : false;
|
||||
|
||||
const rawOverride = typeof r.compressionOverride === "string" ? r.compressionOverride : "";
|
||||
const compressionOverride = VALID_COMPRESSION_OVERRIDES.has(rawOverride)
|
||||
? (rawOverride as OmniRouteCompressionOverride)
|
||||
: "";
|
||||
|
||||
combos.push({ id, name, strategy, active, compressionOverride });
|
||||
}
|
||||
|
||||
return combos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for `createOmniRouteComboConfig`.
|
||||
* Mirrors the subset of combo fields exposed by the OmniRoute `/api/combos`
|
||||
* PATCH / POST payload that are safe to set programmatically.
|
||||
*/
|
||||
export interface OmniRouteComboConfigOptions {
|
||||
/** Human-readable combo name. */
|
||||
name: string;
|
||||
/** Routing strategy (e.g. `"priority"`, `"weighted"`, `"round-robin"`). */
|
||||
strategy: string;
|
||||
/**
|
||||
* Per-combo compression override.
|
||||
* Empty string removes any override (inherits global setting).
|
||||
*/
|
||||
compressionOverride?: OmniRouteCompressionOverride;
|
||||
/** Whether this combo is active for routing. Default: `true`. */
|
||||
active?: boolean;
|
||||
/**
|
||||
* Ordered list of provider IDs in this combo.
|
||||
* Required for create operations; optional for updates.
|
||||
*/
|
||||
providers?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a typed combo payload suitable for OmniRoute's management API.
|
||||
*
|
||||
* The returned object is JSON-serialisable and safe to pass as the body of a
|
||||
* `POST /api/combos` (create) or `PATCH /api/combos/:id` (update) request.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const payload = createOmniRouteComboConfig({
|
||||
* name: "claude-primary",
|
||||
* strategy: "priority",
|
||||
* compressionOverride: "standard",
|
||||
* providers: ["anthropic-claude-opus", "anthropic-claude-sonnet"],
|
||||
* });
|
||||
* await fetch(`${baseURL}/api/combos`, {
|
||||
* method: "POST",
|
||||
* headers: { Authorization: `Bearer ${mgmtKey}`, "Content-Type": "application/json" },
|
||||
* body: JSON.stringify(payload),
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function createOmniRouteComboConfig(
|
||||
options: OmniRouteComboConfigOptions
|
||||
): Record<string, unknown> {
|
||||
const name = requireNonEmpty(options.name, "name");
|
||||
const strategy = requireNonEmpty(options.strategy, "strategy");
|
||||
|
||||
const payload: Record<string, unknown> = {
|
||||
name,
|
||||
strategy,
|
||||
active: options.active ?? true,
|
||||
};
|
||||
|
||||
if (options.compressionOverride !== undefined) {
|
||||
payload.compressionOverride = options.compressionOverride;
|
||||
}
|
||||
|
||||
if (options.providers !== undefined) {
|
||||
const providers = options.providers.filter((p) => typeof p === "string" && p.trim());
|
||||
if (providers.length > 0) {
|
||||
payload.providers = providers;
|
||||
}
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override fields supported per agent / mode entry. Mirrors the subset of
|
||||
* OpenCode's `AgentConfig` schema that is safe to set declaratively from a
|
||||
* config generator. Only fields present in
|
||||
* https://opencode.ai/config.json#AgentConfig are exposed.
|
||||
*/
|
||||
export interface OmniRouteRoleOverrides {
|
||||
/** Forward to OpenCode's `temperature` field. */
|
||||
temperature?: number;
|
||||
/** Forward to OpenCode's `top_p` field. */
|
||||
top_p?: number;
|
||||
}
|
||||
|
||||
/** Per-role binding used by `createOmniRouteAgentBlock`. */
|
||||
export interface OmniRouteAgentRole extends OmniRouteRoleOverrides {
|
||||
/** OmniRoute model id, e.g. `"claude-sonnet-4-5-thinking"`. */
|
||||
modelId: string;
|
||||
/** Optional tools allow-list; per OpenCode schema, map of tool name → enabled. */
|
||||
tools?: Record<string, boolean>;
|
||||
/** Optional system prompt for this agent role. */
|
||||
prompt?: string;
|
||||
}
|
||||
|
||||
/** Options for `createOmniRouteAgentBlock`. */
|
||||
export interface OmniRouteAgentBlockOptions {
|
||||
/** Per-role bindings. Keys become entries under OpenCode's `agent` block. */
|
||||
roles: Record<string, OmniRouteAgentRole>;
|
||||
}
|
||||
|
||||
/** Single entry inside the emitted OpenCode `agent` block. */
|
||||
export interface OpenCodeAgentEntry extends OmniRouteRoleOverrides {
|
||||
/** Always emitted as `"omniroute/<modelId>"`. */
|
||||
model: string;
|
||||
/** Per OpenCode schema, `Record<string, boolean>`. */
|
||||
tools?: Record<string, boolean>;
|
||||
/** Optional system prompt. */
|
||||
prompt?: string;
|
||||
}
|
||||
|
||||
function buildAgentEntry(role: OmniRouteAgentRole): OpenCodeAgentEntry | undefined {
|
||||
if (!role || typeof role.modelId !== "string") return undefined;
|
||||
const modelId = role.modelId.trim();
|
||||
if (!modelId) return undefined;
|
||||
const entry: OpenCodeAgentEntry = { model: `${OMNIROUTE_PROVIDER_KEY}/${modelId}` };
|
||||
if (typeof role.temperature === "number") entry.temperature = role.temperature;
|
||||
if (typeof role.top_p === "number") entry.top_p = role.top_p;
|
||||
if (role.tools && typeof role.tools === "object" && !Array.isArray(role.tools)) {
|
||||
const tools: Record<string, boolean> = {};
|
||||
for (const [name, enabled] of Object.entries(role.tools)) {
|
||||
if (typeof name !== "string" || !name.trim()) continue;
|
||||
if (typeof enabled !== "boolean") continue;
|
||||
tools[name] = enabled;
|
||||
}
|
||||
if (Object.keys(tools).length > 0) entry.tools = tools;
|
||||
}
|
||||
if (typeof role.prompt === "string" && role.prompt.trim()) {
|
||||
entry.prompt = role.prompt;
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the OpenCode `agent` block, pre-wired so each agent role routes to a
|
||||
* specific OmniRoute model. Useful for `.opencode/agent/*.md` defaults and
|
||||
* scaffolded `opencode.json` files.
|
||||
*
|
||||
* Emitted fields are limited to those declared in OpenCode's `AgentConfig`
|
||||
* schema (`model`, `temperature`, `top_p`, `tools`, `prompt`). The `tools`
|
||||
* field is a `Record<string, boolean>` per the schema, not a string array.
|
||||
*
|
||||
* Roles with empty / missing `modelId` are skipped.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const agentBlock = createOmniRouteAgentBlock({
|
||||
* roles: {
|
||||
* build: { modelId: "claude-sonnet-4-5-thinking", temperature: 0.2 },
|
||||
* plan: { modelId: "claude-opus-4-5-thinking", top_p: 0.95 },
|
||||
* review: { modelId: "gemini-3-flash", tools: { edit: false, bash: false } },
|
||||
* },
|
||||
* });
|
||||
* // -> { build: { model: "omniroute/claude-sonnet-4-5-thinking", temperature: 0.2 }, ... }
|
||||
* ```
|
||||
*/
|
||||
export function createOmniRouteAgentBlock(
|
||||
options: OmniRouteAgentBlockOptions
|
||||
): Record<string, OpenCodeAgentEntry> {
|
||||
const out: Record<string, OpenCodeAgentEntry> = {};
|
||||
const roles = options.roles ?? {};
|
||||
for (const [roleName, role] of Object.entries(roles)) {
|
||||
const entry = buildAgentEntry(role);
|
||||
if (entry) out[roleName] = entry;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-mode binding used by `createOmniRouteModesBlock`.
|
||||
*
|
||||
* @deprecated OpenCode's top-level `mode` block is deprecated in favour of
|
||||
* `agent`. Prefer `OmniRouteAgentRole` + `createOmniRouteAgentBlock`. This
|
||||
* type and the corresponding helper are kept for back-compat with configs
|
||||
* still using `mode:`.
|
||||
*/
|
||||
export interface OmniRouteMode extends OmniRouteAgentRole {}
|
||||
|
||||
/**
|
||||
* Options for `createOmniRouteModesBlock`.
|
||||
*
|
||||
* @deprecated See `OmniRouteMode`.
|
||||
*/
|
||||
export interface OmniRouteModesBlockOptions {
|
||||
/** Per-mode bindings. Keys become entries under OpenCode's deprecated top-level `mode` block. */
|
||||
modes: Record<string, OmniRouteMode>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Single entry inside the emitted OpenCode `mode` block.
|
||||
*
|
||||
* @deprecated See `OmniRouteMode`.
|
||||
*/
|
||||
export interface OpenCodeModeEntry extends OpenCodeAgentEntry {}
|
||||
|
||||
/**
|
||||
* Build the OpenCode top-level `mode` block, pre-wired so each mode routes to
|
||||
* a specific OmniRoute model. Emits the same shape as the `agent` block since
|
||||
* OpenCode's schema treats them identically (both reference `AgentConfig`).
|
||||
*
|
||||
* Modes with empty / missing `modelId` are skipped.
|
||||
*
|
||||
* @deprecated OpenCode's top-level `mode` block is deprecated in favour of
|
||||
* `agent`. Prefer `createOmniRouteAgentBlock`. This helper is kept for
|
||||
* back-compat with configs still using `mode:`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const modesBlock = createOmniRouteModesBlock({
|
||||
* modes: {
|
||||
* build: { modelId: "claude-sonnet-4-5-thinking", tools: { edit: true, bash: true } },
|
||||
* plan: { modelId: "claude-opus-4-5-thinking", prompt: "Plan first, code later." },
|
||||
* review: { modelId: "gemini-3-flash" },
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function createOmniRouteModesBlock(
|
||||
options: OmniRouteModesBlockOptions
|
||||
): Record<string, OpenCodeModeEntry> {
|
||||
const out: Record<string, OpenCodeModeEntry> = {};
|
||||
const modes = options.modes ?? {};
|
||||
for (const [modeName, mode] of Object.entries(modes)) {
|
||||
const entry = buildAgentEntry(mode);
|
||||
if (entry) out[modeName] = entry;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export default createOmniRouteProvider;
|
||||
639
@omniroute/opencode-provider/tests/index.test.ts
Normal file
639
@omniroute/opencode-provider/tests/index.test.ts
Normal file
@@ -0,0 +1,639 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createServer } from "node:http";
|
||||
import type { Server } from "node:http";
|
||||
|
||||
import {
|
||||
buildOmniRouteOpenCodeConfig,
|
||||
createOmniRouteAgentBlock,
|
||||
createOmniRouteComboConfig,
|
||||
createOmniRouteMCPEntry,
|
||||
createOmniRouteModesBlock,
|
||||
createOmniRouteProvider,
|
||||
fetchLiveModels,
|
||||
listCombos,
|
||||
mergeIntoExistingConfig,
|
||||
normalizeBaseURL,
|
||||
OMNIROUTE_DEFAULT_MODEL_CAPABILITIES,
|
||||
OMNIROUTE_DEFAULT_MODEL_CONTEXT_LENGTHS,
|
||||
OMNIROUTE_DEFAULT_OPENCODE_MODELS,
|
||||
OMNIROUTE_MCP_DEFAULT_SCOPES,
|
||||
OMNIROUTE_PROVIDER_NPM,
|
||||
OPENCODE_CONFIG_SCHEMA,
|
||||
} from "../src/index.ts";
|
||||
|
||||
test("normalizeBaseURL preserves a bare host:port", () => {
|
||||
assert.equal(normalizeBaseURL("http://localhost:20128"), "http://localhost:20128/v1");
|
||||
});
|
||||
|
||||
test("normalizeBaseURL strips trailing slashes", () => {
|
||||
assert.equal(normalizeBaseURL("http://localhost:20128////"), "http://localhost:20128/v1");
|
||||
});
|
||||
|
||||
test("normalizeBaseURL deduplicates an existing /v1 suffix", () => {
|
||||
assert.equal(normalizeBaseURL("http://localhost:20128/v1"), "http://localhost:20128/v1");
|
||||
assert.equal(normalizeBaseURL("http://localhost:20128/v1/"), "http://localhost:20128/v1");
|
||||
});
|
||||
|
||||
test("normalizeBaseURL rejects empty input", () => {
|
||||
assert.throws(() => normalizeBaseURL(" "), /baseURL is required/);
|
||||
});
|
||||
|
||||
test("normalizeBaseURL rejects malformed URLs", () => {
|
||||
assert.throws(() => normalizeBaseURL("not a url"), /not a valid URL/);
|
||||
});
|
||||
|
||||
test("createOmniRouteProvider validates required fields", () => {
|
||||
assert.throws(
|
||||
() => createOmniRouteProvider({ baseURL: "", apiKey: "x" } as never),
|
||||
/baseURL is required/
|
||||
);
|
||||
assert.throws(
|
||||
() => createOmniRouteProvider({ baseURL: "http://x", apiKey: "" } as never),
|
||||
/apiKey is required/
|
||||
);
|
||||
});
|
||||
|
||||
test("createOmniRouteProvider produces the OpenCode-compatible shape", () => {
|
||||
const provider = createOmniRouteProvider({
|
||||
baseURL: "http://localhost:20128",
|
||||
apiKey: "sk_omniroute",
|
||||
});
|
||||
|
||||
assert.equal(provider.npm, OMNIROUTE_PROVIDER_NPM);
|
||||
assert.equal(provider.name, "OmniRoute");
|
||||
assert.equal(provider.options.baseURL, "http://localhost:20128/v1");
|
||||
assert.equal(provider.options.apiKey, "sk_omniroute");
|
||||
assert.equal(typeof provider.models, "object");
|
||||
});
|
||||
|
||||
test("createOmniRouteProvider seeds the default model catalog", () => {
|
||||
const provider = createOmniRouteProvider({
|
||||
baseURL: "http://localhost:20128",
|
||||
apiKey: "sk_omniroute",
|
||||
});
|
||||
|
||||
const modelIds = Object.keys(provider.models).sort();
|
||||
const defaultIds = [...OMNIROUTE_DEFAULT_OPENCODE_MODELS].sort();
|
||||
assert.deepEqual(modelIds, defaultIds);
|
||||
for (const id of defaultIds) {
|
||||
assert.equal(provider.models[id]?.name, id);
|
||||
assert.equal(provider.models[id]?.attachment, true);
|
||||
}
|
||||
});
|
||||
|
||||
test("createOmniRouteProvider honours a custom models list and labels", () => {
|
||||
const provider = createOmniRouteProvider({
|
||||
baseURL: "http://localhost:20128",
|
||||
apiKey: "sk_omniroute",
|
||||
models: ["auto", "claude-opus-4-7"],
|
||||
modelLabels: { auto: "Auto-Combo", "claude-opus-4-7": "Opus 4.7" },
|
||||
});
|
||||
|
||||
assert.deepEqual(Object.keys(provider.models), ["auto", "claude-opus-4-7"]);
|
||||
assert.equal(provider.models.auto.name, "Auto-Combo");
|
||||
assert.equal(provider.models["claude-opus-4-7"].name, "Opus 4.7");
|
||||
});
|
||||
|
||||
test("createOmniRouteProvider deduplicates and trims model ids", () => {
|
||||
const provider = createOmniRouteProvider({
|
||||
baseURL: "http://localhost:20128",
|
||||
apiKey: "sk_omniroute",
|
||||
models: [" auto ", "auto", "", "claude-opus-4-7"],
|
||||
});
|
||||
assert.deepEqual(Object.keys(provider.models), ["auto", "claude-opus-4-7"]);
|
||||
});
|
||||
|
||||
test("createOmniRouteProvider honours displayName override", () => {
|
||||
const provider = createOmniRouteProvider({
|
||||
baseURL: "http://localhost:20128",
|
||||
apiKey: "sk_omniroute",
|
||||
displayName: "Local OmniRoute",
|
||||
});
|
||||
assert.equal(provider.name, "Local OmniRoute");
|
||||
});
|
||||
|
||||
test("buildOmniRouteOpenCodeConfig wraps the provider with the OpenCode schema", () => {
|
||||
const doc = buildOmniRouteOpenCodeConfig({
|
||||
baseURL: "http://localhost:20128/v1",
|
||||
apiKey: "sk_omniroute",
|
||||
});
|
||||
|
||||
assert.equal(doc.$schema, OPENCODE_CONFIG_SCHEMA);
|
||||
assert.equal(typeof doc.provider.omniroute, "object");
|
||||
assert.equal(doc.provider.omniroute.options.baseURL, "http://localhost:20128/v1");
|
||||
});
|
||||
|
||||
test("config document is JSON-serialisable", () => {
|
||||
const doc = buildOmniRouteOpenCodeConfig({
|
||||
baseURL: "http://localhost:20128",
|
||||
apiKey: "sk_omniroute",
|
||||
});
|
||||
const round = JSON.parse(JSON.stringify(doc));
|
||||
assert.deepEqual(round, doc);
|
||||
});
|
||||
|
||||
test("buildOmniRouteOpenCodeConfig emits model and small_model prefixed with provider key", () => {
|
||||
const doc = buildOmniRouteOpenCodeConfig({
|
||||
baseURL: "http://localhost:20128",
|
||||
apiKey: "sk_omniroute",
|
||||
model: "claude-sonnet-4-5-thinking",
|
||||
smallModel: "gemini-3-flash",
|
||||
});
|
||||
assert.equal(doc.model, "omniroute/claude-sonnet-4-5-thinking");
|
||||
assert.equal(doc.small_model, "omniroute/gemini-3-flash");
|
||||
});
|
||||
|
||||
test("buildOmniRouteOpenCodeConfig omits model and small_model when not supplied", () => {
|
||||
const doc = buildOmniRouteOpenCodeConfig({
|
||||
baseURL: "http://localhost:20128",
|
||||
apiKey: "sk_omniroute",
|
||||
});
|
||||
assert.equal(doc.model, undefined);
|
||||
assert.equal(doc.small_model, undefined);
|
||||
assert.ok(!("model" in doc));
|
||||
assert.ok(!("small_model" in doc));
|
||||
});
|
||||
|
||||
test("buildOmniRouteOpenCodeConfig ignores blank model strings", () => {
|
||||
const doc = buildOmniRouteOpenCodeConfig({
|
||||
baseURL: "http://localhost:20128",
|
||||
apiKey: "sk_omniroute",
|
||||
model: " ",
|
||||
smallModel: "",
|
||||
});
|
||||
assert.ok(!("model" in doc));
|
||||
assert.ok(!("small_model" in doc));
|
||||
});
|
||||
|
||||
test("mergeIntoExistingConfig preserves existing provider entries", () => {
|
||||
const existing = {
|
||||
$schema: OPENCODE_CONFIG_SCHEMA,
|
||||
provider: {
|
||||
anthropic: { npm: "@ai-sdk/anthropic", name: "Anthropic", options: {}, models: {} },
|
||||
},
|
||||
keybinds: { submit: "enter" },
|
||||
};
|
||||
const result = mergeIntoExistingConfig(existing, {
|
||||
baseURL: "http://localhost:20128",
|
||||
apiKey: "sk_omniroute",
|
||||
});
|
||||
assert.ok("anthropic" in (result.provider as Record<string, unknown>));
|
||||
assert.ok("omniroute" in (result.provider as Record<string, unknown>));
|
||||
assert.deepEqual((result as Record<string, unknown>).keybinds, { submit: "enter" });
|
||||
});
|
||||
|
||||
test("mergeIntoExistingConfig overwrites existing omniroute entry", () => {
|
||||
const existing = {
|
||||
provider: {
|
||||
omniroute: {
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
name: "OLD",
|
||||
options: { baseURL: "http://old/v1", apiKey: "old" },
|
||||
models: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
const result = mergeIntoExistingConfig(existing, {
|
||||
baseURL: "http://new",
|
||||
apiKey: "new-key",
|
||||
displayName: "NEW",
|
||||
});
|
||||
const omniroute = (result.provider as Record<string, unknown>).omniroute as { name: string };
|
||||
assert.equal(omniroute.name, "NEW");
|
||||
});
|
||||
|
||||
test("mergeIntoExistingConfig writes model and small_model when supplied", () => {
|
||||
const result = mergeIntoExistingConfig(
|
||||
{},
|
||||
{
|
||||
baseURL: "http://localhost:20128",
|
||||
apiKey: "sk_omniroute",
|
||||
model: "claude-sonnet-4-5-thinking",
|
||||
smallModel: "gemini-3-flash",
|
||||
}
|
||||
);
|
||||
assert.equal(result.model, "omniroute/claude-sonnet-4-5-thinking");
|
||||
assert.equal(result.small_model, "omniroute/gemini-3-flash");
|
||||
});
|
||||
|
||||
test("mergeIntoExistingConfig does not add model keys when not supplied", () => {
|
||||
const result = mergeIntoExistingConfig(
|
||||
{},
|
||||
{ baseURL: "http://localhost:20128", apiKey: "sk_omniroute" }
|
||||
);
|
||||
assert.ok(!("model" in result));
|
||||
assert.ok(!("small_model" in result));
|
||||
});
|
||||
|
||||
test("OMNIROUTE_MCP_DEFAULT_SCOPES contains 7 read-only scopes", () => {
|
||||
assert.equal(OMNIROUTE_MCP_DEFAULT_SCOPES.length, 7);
|
||||
assert.ok(OMNIROUTE_MCP_DEFAULT_SCOPES.every((s) => s.startsWith("read:")));
|
||||
});
|
||||
|
||||
test("createOmniRouteMCPEntry defaults to tsx runtime", () => {
|
||||
const entry = createOmniRouteMCPEntry({
|
||||
serverPath: "/path/to/server.ts",
|
||||
apiKey: "sk_omniroute",
|
||||
});
|
||||
assert.equal(entry.command, "npx");
|
||||
assert.deepEqual(entry.args, ["tsx", "/path/to/server.ts"]);
|
||||
assert.equal(entry.env.OMNIROUTE_API_KEY, "sk_omniroute");
|
||||
assert.ok(!("OMNIROUTE_MCP_ENFORCE_SCOPES" in entry.env));
|
||||
assert.ok(!("OMNIROUTE_MANAGEMENT_API_KEY" in entry.env));
|
||||
});
|
||||
|
||||
test("createOmniRouteMCPEntry uses node runtime when specified", () => {
|
||||
const entry = createOmniRouteMCPEntry({
|
||||
serverPath: "/path/to/server.js",
|
||||
apiKey: "sk_omniroute",
|
||||
runtime: "node",
|
||||
});
|
||||
assert.equal(entry.command, "node");
|
||||
assert.deepEqual(entry.args, ["/path/to/server.js"]);
|
||||
});
|
||||
|
||||
test("createOmniRouteMCPEntry sets management key and scopes when supplied", () => {
|
||||
const entry = createOmniRouteMCPEntry({
|
||||
serverPath: "/path/to/server.ts",
|
||||
apiKey: "sk_omniroute",
|
||||
managementApiKey: "sk_manage",
|
||||
scopes: ["read:health", "read:combos", "execute:completions"],
|
||||
});
|
||||
assert.equal(entry.env.OMNIROUTE_MANAGEMENT_API_KEY, "sk_manage");
|
||||
assert.equal(entry.env.OMNIROUTE_MCP_ENFORCE_SCOPES, "true");
|
||||
assert.equal(entry.env.OMNIROUTE_MCP_SCOPES, "read:health,read:combos,execute:completions");
|
||||
});
|
||||
|
||||
test("createOmniRouteMCPEntry rejects missing required fields", () => {
|
||||
assert.throws(
|
||||
() => createOmniRouteMCPEntry({ serverPath: "", apiKey: "x" }),
|
||||
/serverPath is required/
|
||||
);
|
||||
assert.throws(
|
||||
() => createOmniRouteMCPEntry({ serverPath: "/p", apiKey: "" }),
|
||||
/apiKey is required/
|
||||
);
|
||||
});
|
||||
|
||||
function startMockServer(
|
||||
handler: (path: string) => unknown
|
||||
): Promise<{ url: string; close: () => void }> {
|
||||
return new Promise((resolve) => {
|
||||
const server: Server = createServer((req, res) => {
|
||||
const body = JSON.stringify(handler(req.url ?? ""));
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(body);
|
||||
});
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const addr = server.address() as { port: number };
|
||||
resolve({ url: `http://127.0.0.1:${addr.port}`, close: () => server.close() });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test("fetchLiveModels handles array envelope", async () => {
|
||||
const { url, close } = await startMockServer(() => [
|
||||
{ id: "claude-sonnet", name: "Claude Sonnet" },
|
||||
{ id: "gemini-flash", displayName: "Gemini Flash" },
|
||||
]);
|
||||
try {
|
||||
const models = await fetchLiveModels(url, "sk_test");
|
||||
assert.equal(models.length, 2);
|
||||
assert.equal(models[0].id, "claude-sonnet");
|
||||
assert.equal(models[0].name, "Claude Sonnet");
|
||||
assert.equal(models[1].id, "gemini-flash");
|
||||
assert.equal(models[1].name, "Gemini Flash");
|
||||
} finally {
|
||||
close();
|
||||
}
|
||||
});
|
||||
|
||||
test("fetchLiveModels handles data-envelope and snake_case fields", async () => {
|
||||
const { url, close } = await startMockServer(() => ({
|
||||
data: [{ model_id: "gpt-4o", display_name: "GPT-4o" }],
|
||||
}));
|
||||
try {
|
||||
const models = await fetchLiveModels(url, "sk_test");
|
||||
assert.equal(models.length, 1);
|
||||
assert.equal(models[0].id, "gpt-4o");
|
||||
assert.equal(models[0].name, "GPT-4o");
|
||||
} finally {
|
||||
close();
|
||||
}
|
||||
});
|
||||
|
||||
test("fetchLiveModels falls back to id as name when no name field", async () => {
|
||||
const { url, close } = await startMockServer(() => [{ id: "auto" }]);
|
||||
try {
|
||||
const models = await fetchLiveModels(url, "sk_test");
|
||||
assert.equal(models[0].name, "auto");
|
||||
} finally {
|
||||
close();
|
||||
}
|
||||
});
|
||||
|
||||
test("listCombos normalises compressionOverride", async () => {
|
||||
const { url, close } = await startMockServer(() => ({
|
||||
combos: [
|
||||
{
|
||||
id: "c1",
|
||||
name: "Primary",
|
||||
strategy: "priority",
|
||||
active: true,
|
||||
compressionOverride: "standard",
|
||||
},
|
||||
{
|
||||
id: "c2",
|
||||
name: "Cheap",
|
||||
strategy: "weighted",
|
||||
active: false,
|
||||
compressionOverride: "unknown-value",
|
||||
},
|
||||
{ id: "c3", name: "Off", strategy: "round-robin", active: true, compressionOverride: "" },
|
||||
],
|
||||
}));
|
||||
try {
|
||||
const combos = await listCombos(url, "sk_manage");
|
||||
assert.equal(combos.length, 3);
|
||||
assert.equal(combos[0].compressionOverride, "standard");
|
||||
assert.equal(combos[1].compressionOverride, "");
|
||||
assert.equal(combos[2].compressionOverride, "");
|
||||
} finally {
|
||||
close();
|
||||
}
|
||||
});
|
||||
|
||||
test("createOmniRouteComboConfig builds minimal payload", () => {
|
||||
const payload = createOmniRouteComboConfig({ name: "my-combo", strategy: "priority" });
|
||||
assert.equal(payload.name, "my-combo");
|
||||
assert.equal(payload.strategy, "priority");
|
||||
assert.equal(payload.active, true);
|
||||
assert.ok(!("compressionOverride" in payload));
|
||||
assert.ok(!("providers" in payload));
|
||||
});
|
||||
|
||||
test("createOmniRouteComboConfig includes optional fields when supplied", () => {
|
||||
const payload = createOmniRouteComboConfig({
|
||||
name: "full",
|
||||
strategy: "weighted",
|
||||
compressionOverride: "aggressive",
|
||||
active: false,
|
||||
providers: ["provider-a", "provider-b"],
|
||||
});
|
||||
assert.equal(payload.compressionOverride, "aggressive");
|
||||
assert.equal(payload.active, false);
|
||||
assert.deepEqual(payload.providers, ["provider-a", "provider-b"]);
|
||||
});
|
||||
|
||||
test("OMNIROUTE_DEFAULT_OPENCODE_MODELS includes cc/ prefixed models", () => {
|
||||
const defaults = [...OMNIROUTE_DEFAULT_OPENCODE_MODELS];
|
||||
assert.ok(
|
||||
defaults.some((m) => m.startsWith("cc/")),
|
||||
"should have cc/ prefixed models"
|
||||
);
|
||||
assert.ok(defaults.length >= 7, "should have at least 7 models");
|
||||
});
|
||||
|
||||
test("OMNIROUTE_DEFAULT_MODEL_CONTEXT_LENGTHS covers every default model id", () => {
|
||||
for (const id of OMNIROUTE_DEFAULT_OPENCODE_MODELS) {
|
||||
const ctx = OMNIROUTE_DEFAULT_MODEL_CONTEXT_LENGTHS[id];
|
||||
assert.ok(
|
||||
typeof ctx === "number" && ctx > 0,
|
||||
`default context_length for ${id} missing — should be a positive number`
|
||||
);
|
||||
// Sanity: context should be at least 8K, at most 2M tokens
|
||||
assert.ok(ctx >= 8_000, `${id} context_length ${ctx} seems too low`);
|
||||
assert.ok(ctx <= 2_000_000, `${id} context_length ${ctx} seems too high`);
|
||||
}
|
||||
});
|
||||
|
||||
test("createOmniRouteProvider emits limit.context on default model entries", () => {
|
||||
const provider = createOmniRouteProvider({
|
||||
baseURL: "http://localhost:20128",
|
||||
apiKey: "sk_omniroute",
|
||||
});
|
||||
const entry = provider.models["cc/claude-opus-4-7"];
|
||||
assert.ok(entry.limit, "model entry should have a limit field");
|
||||
assert.equal(entry.limit!.context, 200_000);
|
||||
});
|
||||
|
||||
test("createOmniRouteProvider omits limit.context for unknown model ids", () => {
|
||||
const provider = createOmniRouteProvider({
|
||||
baseURL: "http://localhost:20128",
|
||||
apiKey: "sk_omniroute",
|
||||
models: ["completely-unknown-model"],
|
||||
});
|
||||
const entry = provider.models["completely-unknown-model"];
|
||||
assert.equal(entry.limit, undefined);
|
||||
});
|
||||
|
||||
test("createOmniRouteProvider serialises limit.context to JSON", () => {
|
||||
const provider = createOmniRouteProvider({
|
||||
baseURL: "http://localhost:20128",
|
||||
apiKey: "sk_omniroute",
|
||||
});
|
||||
const round = JSON.parse(JSON.stringify(provider));
|
||||
for (const id of OMNIROUTE_DEFAULT_OPENCODE_MODELS) {
|
||||
const expectedContext = OMNIROUTE_DEFAULT_MODEL_CONTEXT_LENGTHS[id];
|
||||
assert.equal(
|
||||
round.models[id].limit?.context,
|
||||
expectedContext,
|
||||
`${id} should serialise limit.context=${expectedContext}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("fetchLiveModels extracts context_length from snake_case field", async () => {
|
||||
const { url, close } = await startMockServer(() => ({
|
||||
data: [
|
||||
{ id: "cc/claude-opus-4-7", name: "Claude Opus 4.7", context_length: 200_000 },
|
||||
{ id: "gemini-3.1-pro-high", name: "Gemini 3.1 Pro", context_length: 1_000_000 },
|
||||
{ id: "no-context", name: "No Context" },
|
||||
],
|
||||
}));
|
||||
try {
|
||||
const models = await fetchLiveModels(url, "sk_test");
|
||||
const claude = models.find((m) => m.id === "cc/claude-opus-4-7");
|
||||
assert.ok(claude, "claude model should be present");
|
||||
assert.equal(claude!.contextLength, 200_000);
|
||||
const gemini = models.find((m) => m.id === "gemini-3.1-pro-high");
|
||||
assert.equal(gemini!.contextLength, 1_000_000);
|
||||
const noCtx = models.find((m) => m.id === "no-context");
|
||||
assert.equal(noCtx!.contextLength, undefined);
|
||||
} finally {
|
||||
close();
|
||||
}
|
||||
});
|
||||
|
||||
test("OMNIROUTE_DEFAULT_MODEL_CAPABILITIES covers every default model id", () => {
|
||||
for (const id of OMNIROUTE_DEFAULT_OPENCODE_MODELS) {
|
||||
const caps = OMNIROUTE_DEFAULT_MODEL_CAPABILITIES[id];
|
||||
assert.ok(caps, `default capabilities for ${id} missing`);
|
||||
assert.equal(caps.attachment, true, `${id} should default to attachment=true`);
|
||||
assert.equal(caps.tool_call, true, `${id} should default to tool_call=true`);
|
||||
}
|
||||
});
|
||||
|
||||
test("createOmniRouteProvider emits default capability flags inline with the model entry", () => {
|
||||
const provider = createOmniRouteProvider({
|
||||
baseURL: "http://localhost:20128",
|
||||
apiKey: "sk_omniroute",
|
||||
});
|
||||
const entry = provider.models["cc/claude-opus-4-7"];
|
||||
assert.equal(entry.name, "cc/claude-opus-4-7");
|
||||
assert.equal(entry.attachment, true);
|
||||
assert.equal(entry.reasoning, true);
|
||||
assert.equal(entry.temperature, true);
|
||||
assert.equal(entry.tool_call, true);
|
||||
});
|
||||
|
||||
test("createOmniRouteProvider modelCapabilities overrides defaults and merges per id", () => {
|
||||
const provider = createOmniRouteProvider({
|
||||
baseURL: "http://localhost:20128",
|
||||
apiKey: "sk_omniroute",
|
||||
modelCapabilities: {
|
||||
"cc/claude-opus-4-7": { reasoning: false, label: "Opus (no thinking)" },
|
||||
},
|
||||
});
|
||||
const entry = provider.models["cc/claude-opus-4-7"];
|
||||
assert.equal(entry.name, "Opus (no thinking)");
|
||||
assert.equal(entry.reasoning, false);
|
||||
assert.equal(entry.attachment, true);
|
||||
assert.equal(entry.tool_call, true);
|
||||
});
|
||||
|
||||
test("createOmniRouteProvider applies capability overrides to non-default model ids", () => {
|
||||
const provider = createOmniRouteProvider({
|
||||
baseURL: "http://localhost:20128",
|
||||
apiKey: "sk_omniroute",
|
||||
models: ["custom-model"],
|
||||
modelCapabilities: {
|
||||
"custom-model": { attachment: false, tool_call: true, label: "Custom" },
|
||||
},
|
||||
});
|
||||
const entry = provider.models["custom-model"];
|
||||
assert.equal(entry.name, "Custom");
|
||||
assert.equal(entry.attachment, false);
|
||||
assert.equal(entry.tool_call, true);
|
||||
assert.equal(entry.reasoning, undefined);
|
||||
assert.equal(entry.temperature, undefined);
|
||||
});
|
||||
|
||||
test("createOmniRouteProvider modelLabels still works when modelCapabilities omits label", () => {
|
||||
const provider = createOmniRouteProvider({
|
||||
baseURL: "http://localhost:20128",
|
||||
apiKey: "sk_omniroute",
|
||||
models: ["claude-opus-4-5-thinking"],
|
||||
modelLabels: { "claude-opus-4-5-thinking": "Opus 4.5 (legacy label)" },
|
||||
});
|
||||
assert.equal(provider.models["claude-opus-4-5-thinking"].name, "Opus 4.5 (legacy label)");
|
||||
});
|
||||
|
||||
test("createOmniRouteAgentBlock builds provider-prefixed entries per role", () => {
|
||||
const block = createOmniRouteAgentBlock({
|
||||
roles: {
|
||||
build: { modelId: "claude-sonnet-4-5-thinking", temperature: 0.2 },
|
||||
plan: { modelId: "claude-opus-4-5-thinking", top_p: 0.95 },
|
||||
review: { modelId: "gemini-3-flash", temperature: 0.0 },
|
||||
},
|
||||
});
|
||||
assert.equal(block.build.model, "omniroute/claude-sonnet-4-5-thinking");
|
||||
assert.equal(block.build.temperature, 0.2);
|
||||
assert.equal(block.plan.model, "omniroute/claude-opus-4-5-thinking");
|
||||
assert.equal(block.plan.top_p, 0.95);
|
||||
assert.equal(block.review.model, "omniroute/gemini-3-flash");
|
||||
assert.equal(block.review.temperature, 0.0);
|
||||
});
|
||||
|
||||
test("createOmniRouteAgentBlock omits optional fields when not supplied", () => {
|
||||
const block = createOmniRouteAgentBlock({
|
||||
roles: { build: { modelId: "claude-sonnet-4-5-thinking" } },
|
||||
});
|
||||
assert.equal(block.build.model, "omniroute/claude-sonnet-4-5-thinking");
|
||||
assert.ok(!("temperature" in block.build));
|
||||
assert.ok(!("top_p" in block.build));
|
||||
assert.ok(!("tools" in block.build));
|
||||
assert.ok(!("prompt" in block.build));
|
||||
});
|
||||
|
||||
test("createOmniRouteAgentBlock skips roles with empty modelId", () => {
|
||||
const block = createOmniRouteAgentBlock({
|
||||
roles: {
|
||||
build: { modelId: "claude-sonnet-4-5-thinking" },
|
||||
plan: { modelId: " " },
|
||||
review: { modelId: "" },
|
||||
},
|
||||
});
|
||||
assert.deepEqual(Object.keys(block), ["build"]);
|
||||
});
|
||||
|
||||
test("createOmniRouteAgentBlock emits tools as Record<string, boolean> per OC schema", () => {
|
||||
const block = createOmniRouteAgentBlock({
|
||||
roles: {
|
||||
build: {
|
||||
modelId: "claude-sonnet-4-5-thinking",
|
||||
tools: { edit: true, bash: true, web: false },
|
||||
prompt: "Edit files carefully.",
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.deepEqual(block.build.tools, { edit: true, bash: true, web: false });
|
||||
assert.equal(block.build.prompt, "Edit files carefully.");
|
||||
});
|
||||
|
||||
test("createOmniRouteAgentBlock filters invalid tool entries and omits empty maps", () => {
|
||||
const block = createOmniRouteAgentBlock({
|
||||
roles: {
|
||||
build: {
|
||||
modelId: "claude-sonnet-4-5-thinking",
|
||||
// @ts-expect-error — exercising runtime guard against bad input
|
||||
tools: { edit: true, bash: "yes", "": true, web: null },
|
||||
},
|
||||
plan: {
|
||||
modelId: "claude-opus-4-5-thinking",
|
||||
tools: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.deepEqual(block.build.tools, { edit: true });
|
||||
assert.ok(!("tools" in block.plan));
|
||||
});
|
||||
|
||||
test("createOmniRouteModesBlock builds provider-prefixed mode entries", () => {
|
||||
const block = createOmniRouteModesBlock({
|
||||
modes: {
|
||||
build: { modelId: "claude-sonnet-4-5-thinking", tools: { edit: true, bash: true } },
|
||||
plan: { modelId: "claude-opus-4-5-thinking", prompt: "Plan first, code later." },
|
||||
review: { modelId: "gemini-3-flash" },
|
||||
},
|
||||
});
|
||||
assert.equal(block.build.model, "omniroute/claude-sonnet-4-5-thinking");
|
||||
assert.deepEqual(block.build.tools, { edit: true, bash: true });
|
||||
assert.equal(block.plan.prompt, "Plan first, code later.");
|
||||
assert.equal(block.review.model, "omniroute/gemini-3-flash");
|
||||
});
|
||||
|
||||
test("createOmniRouteModesBlock skips modes with empty modelId", () => {
|
||||
const block = createOmniRouteModesBlock({
|
||||
modes: {
|
||||
build: { modelId: "claude-sonnet-4-5-thinking" },
|
||||
plan: { modelId: "" },
|
||||
},
|
||||
});
|
||||
assert.deepEqual(Object.keys(block), ["build"]);
|
||||
});
|
||||
|
||||
test("createOmniRouteModesBlock honours numeric overrides limited to OC schema", () => {
|
||||
const block = createOmniRouteModesBlock({
|
||||
modes: {
|
||||
build: {
|
||||
modelId: "claude-sonnet-4-5-thinking",
|
||||
temperature: 0.7,
|
||||
top_p: 0.9,
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.equal(block.build.temperature, 0.7);
|
||||
assert.equal(block.build.top_p, 0.9);
|
||||
});
|
||||
20
@omniroute/opencode-provider/tsconfig.json
Normal file
20
@omniroute/opencode-provider/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"lib": ["ES2022"],
|
||||
"types": ["node"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true,
|
||||
"isolatedModules": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noUncheckedIndexedAccess": false,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["dist", "node_modules", "tests"]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user