Compare commits
723 Commits
65dedef65b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c4fdf2ac9 | ||
|
|
d6f89ff0fa | ||
|
|
bc475f0172 | ||
|
|
191da1feb5 | ||
|
|
8fae55e8e0 | ||
|
|
ebe5730401 | ||
|
|
0499656c59 | ||
|
|
05a57e94a4 | ||
|
|
c27b03794a | ||
|
|
9866a857a7 | ||
|
|
e2dea2684f | ||
|
|
a30c4f45c3 | ||
|
|
95263f4e60 | ||
|
|
6f1ba986b3 | ||
|
|
c741d008dd | ||
|
|
0d60ef6fef | ||
|
|
ce715c4c56 | ||
|
|
0deb8b0da1 | ||
|
|
b8c8130efe | ||
|
|
ea423bbbfd | ||
|
|
980f788731 | ||
|
|
9271fcb3d4 | ||
|
|
b8f740fb14 | ||
|
|
8da20027c4 | ||
|
|
9201e140cb | ||
|
|
ff80646085 | ||
|
|
929a3725d3 | ||
|
|
cde29fef71 | ||
|
|
6d1daf2ba5 | ||
|
|
82419eaad6 | ||
|
|
f0722498a4 | ||
|
|
a4d5c7f673 | ||
|
|
9a3f62cb86 | ||
|
|
1007d71f0c | ||
|
|
9f703a44dc | ||
|
|
85ed6c7fa4 | ||
|
|
ad4dd0422e | ||
|
|
4ba9809f18 | ||
|
|
80d42eb0ba | ||
|
|
2b6cf03b47 | ||
|
|
88ffad1c4f | ||
|
|
875324e7c7 | ||
|
|
2d7428a7f2 | ||
|
|
47596257ea | ||
|
|
ab3045cb48 | ||
|
|
aaddbdae52 | ||
|
|
8d0e7997c8 | ||
|
|
31a7e4f937 | ||
|
|
c5194d8148 | ||
|
|
43c0a7fe1c | ||
|
|
e78ae48e69 | ||
|
|
4c1da23a71 | ||
|
|
3d2fe9284e | ||
|
|
a3b5f1b15c | ||
|
|
d1dc60774b | ||
|
|
d90cac990c | ||
|
|
0dd7033521 | ||
|
|
c80a09fc2f | ||
|
|
f831c48e56 | ||
|
|
c237a82b42 | ||
|
|
94b2fc14f2 | ||
|
|
0daf416908 | ||
|
|
dca8cf958b | ||
|
|
93bf75279f | ||
|
|
fe308a3aa1 | ||
|
|
7be921c434 | ||
|
|
5163833be5 | ||
|
|
d898ad6807 | ||
|
|
677450cd9b | ||
|
|
ac5944cde7 | ||
|
|
3b768a2851 | ||
|
|
2c8af78d20 | ||
|
|
48b0fd8d88 | ||
|
|
40425db0f1 | ||
|
|
6965a2cc9d | ||
|
|
e3d3893d5d | ||
|
|
4216449405 | ||
|
|
5842bcaaf7 | ||
|
|
991cf4d7fe | ||
|
|
578a6e27aa | ||
|
|
0a1f4f666a | ||
|
|
c7aec0660e | ||
|
|
1bf9f237f7 | ||
|
|
134c03a903 | ||
|
|
18b480dd3e | ||
|
|
0b51f0d762 | ||
|
|
7a9deb2400 | ||
|
|
3997316fb0 | ||
|
|
360851366f | ||
|
|
c75275f109 | ||
|
|
50e687d17d | ||
|
|
4a59b7786b | ||
|
|
8a352c8f9d | ||
|
|
b40da2cb7a | ||
|
|
72245855e5 | ||
|
|
7b2a221212 | ||
|
|
ac0c2f260f | ||
|
|
d2aee7da68 | ||
|
|
717129f7f9 | ||
|
|
b1430aaaca | ||
|
|
bccdc95a9b | ||
|
|
328b69be17 | ||
|
|
f16e32b73d | ||
|
|
8abce8a84d | ||
|
|
c448e5da6f | ||
|
|
6c42d34610 | ||
|
|
ee1ec3faba | ||
|
|
a459e237e8 | ||
|
|
47538bca4d | ||
|
|
05b28c147d | ||
|
|
0a48592475 | ||
|
|
3ad7958365 | ||
|
|
34a58b839c | ||
|
|
ec0728b357 | ||
|
|
0c7fa2b0d5 | ||
|
|
5f6e1c19bd | ||
|
|
7e005acd3c | ||
|
|
8ba1387ba2 | ||
|
|
7e32f1ce20 | ||
|
|
2267d58afc | ||
|
|
02842bef91 | ||
|
|
57326f72e6 | ||
|
|
370bbcd89b | ||
|
|
6f4665dda3 | ||
|
|
2d15dd757d | ||
|
|
861725fba1 | ||
|
|
de7b2ba7d5 | ||
|
|
7db839544d | ||
|
|
5958e5693c | ||
|
|
bc88e58fcf | ||
|
|
141f551a4c | ||
|
|
6ff209e932 | ||
|
|
b0befb5f5d | ||
|
|
40e23b05f7 | ||
|
|
313e2f2e85 | ||
|
|
68393bfa36 | ||
|
|
155dfa93e5 | ||
|
|
db31c0ccca | ||
|
|
cefd87f355 | ||
|
|
8577d015b2 | ||
|
|
4a5e9f0a4f | ||
|
|
c18452598a | ||
|
|
3299aeb904 | ||
|
|
8fdc0a2841 | ||
|
|
873182ec2d | ||
|
|
b8004a28cc | ||
|
|
6b7d3c3062 | ||
|
|
d4c560853c | ||
|
|
4e1a7cd60c | ||
|
|
4629054403 | ||
|
|
93b450349f | ||
|
|
2ca78a8aed | ||
|
|
db8e9b37c6 | ||
|
|
ad13c265ba | ||
|
|
7159d3b254 | ||
|
|
d6c088910b | ||
|
|
821520a057 | ||
|
|
f32eeae3bc | ||
|
|
7c951b01ab | ||
|
|
4fc4c5256a | ||
|
|
eb80b9acb3 | ||
|
|
ea237115a9 | ||
|
|
679bb087db | ||
|
|
1473fb19a5 | ||
|
|
01db1dde1a | ||
|
|
a13efbe2b5 | ||
|
|
6ac5dd2c0e | ||
|
|
eef247b7a4 | ||
|
|
9e0030b75f | ||
|
|
ddedb56c01 | ||
|
|
547374220c | ||
|
|
c8f4bca0c4 | ||
|
|
3011b00d39 | ||
|
|
cf95b2f3f4 | ||
|
|
203e3804b3 | ||
|
|
34424ce536 | ||
|
|
675c26b2b0 | ||
|
|
8b8451231c | ||
|
|
460808e0c8 | ||
|
|
f2c5c847bd | ||
|
|
c0b267a03a | ||
|
|
8860d2ed7f | ||
|
|
a4d1af1b11 | ||
|
|
5031b283a5 | ||
|
|
bdb90ea4ee | ||
|
|
d6cde28c8e | ||
|
|
f26cc60872 | ||
|
|
1ee1522daa | ||
|
|
34e78a7054 | ||
|
|
44bbe09bee | ||
|
|
1008c28f5a | ||
|
|
0621d0e9e8 | ||
|
|
3b40227bc6 | ||
|
|
d84eb46467 | ||
|
|
7af00f040a | ||
|
|
4d30f97407 | ||
|
|
ff948a6dd7 | ||
|
|
ad759c9446 | ||
|
|
9ccbd57016 | ||
|
|
52c9d3480f | ||
|
|
8524666454 | ||
|
|
517a8eafe5 | ||
|
|
c8e67ad5d5 | ||
|
|
54ddbc4660 | ||
|
|
7f95cdac78 | ||
|
|
cfdc551346 | ||
|
|
f895c9fba1 | ||
|
|
22927b0834 | ||
|
|
385a7eba33 | ||
|
|
a6fd76efeb | ||
|
|
21f8c3db18 | ||
|
|
392bbddf29 | ||
|
|
0cd47d830f | ||
|
|
90b4e54354 | ||
|
|
4434cae565 | ||
|
|
5e025c4ba3 | ||
|
|
a13ff55bd9 | ||
|
|
96abc1c864 | ||
|
|
38e6da1fe0 | ||
|
|
a42e3cb78a | ||
|
|
bebf323775 | ||
|
|
5d82c82313 | ||
|
|
43590d8287 | ||
|
|
dae0e2b325 | ||
|
|
3e14192730 | ||
|
|
dbaf0a8ae2 | ||
|
|
5bd63b012c | ||
|
|
3fae903863 | ||
|
|
d5f8208c38 | ||
|
|
42c690632d | ||
|
|
1d17630dc6 | ||
|
|
2b1da4f5d8 | ||
|
|
aaeecc8c8d | ||
|
|
eab0a07f71 | ||
|
|
01ce144fa9 | ||
|
|
718dba8cb6 | ||
|
|
9822985ea6 | ||
|
|
fb5280e1b5 | ||
|
|
009abd306a | ||
|
|
8c53dfb74f | ||
|
|
7bf4080608 | ||
|
|
1de05ad068 | ||
|
|
2196456d4a | ||
|
|
6f200ea77f | ||
|
|
5b0851ebd8 | ||
|
|
19ecdce275 | ||
|
|
18652d181b | ||
|
|
f633a8cb22 | ||
|
|
78f8a29071 | ||
|
|
78fd194722 | ||
|
|
b2361292e7 | ||
|
|
57566c5e4d | ||
|
|
da6de49815 | ||
|
|
6341819d74 | ||
|
|
c396877dd9 | ||
|
|
79d00e20db | ||
|
|
f8d2534062 | ||
|
|
6fb8d8850e | ||
|
|
246896d64b | ||
|
|
64df61f697 | ||
|
|
ef4949b936 | ||
|
|
1409943863 | ||
|
|
3f82daefd8 | ||
|
|
ab9f06f4ff | ||
|
|
0bb0dfc9bc | ||
|
|
511c656cbc | ||
|
|
3a03e38378 | ||
|
|
30ac80b96b | ||
|
|
a749db9820 | ||
|
|
fa4b28d7af | ||
|
|
5292367324 | ||
|
|
35eb40a700 | ||
|
|
6fdb136688 | ||
|
|
44d1aa31f3 | ||
|
|
7b3d23b703 | ||
|
|
efc9d0a498 | ||
|
|
089d03453d | ||
|
|
4a5d368926 | ||
|
|
1c6b25ddbb | ||
|
|
9f16de2533 | ||
|
|
d2ff28dda7 | ||
|
|
f04e84f194 | ||
|
|
5af322f710 | ||
|
|
b64c1a56a1 | ||
|
|
41a4f1200b | ||
|
|
202c554d09 | ||
|
|
16349b6e93 | ||
|
|
efb4a34be4 | ||
|
|
e4b084c762 | ||
|
|
3e6c623cfe | ||
|
|
9c4eab69cc | ||
|
|
9c5941ba46 | ||
|
|
41d2993f7b | ||
|
|
d3ba57b7d7 | ||
|
|
6b4b6049b4 | ||
|
|
bbe9cb3022 | ||
|
|
61a7fc5e0e | ||
|
|
e895e85f54 | ||
|
|
e59eb814bd | ||
|
|
df55eeacdb | ||
|
|
fd8f8843bd | ||
|
|
a9bb96ade3 | ||
|
|
f1cbe7db1d | ||
|
|
95cd2210f9 | ||
|
|
539a15e63f | ||
|
|
4df4435c45 | ||
|
|
d41acf99a6 | ||
|
|
efe2a464af | ||
|
|
66d8117d44 | ||
|
|
0223416c61 | ||
|
|
2483f26c23 | ||
|
|
3eb45560d5 | ||
|
|
b2990a003e | ||
|
|
4027b3583e | ||
|
|
a3ec2d0734 | ||
|
|
9f03791aa9 | ||
|
|
f52ca0a712 | ||
|
|
ddccfd3ec1 | ||
|
|
f60eae83fa | ||
|
|
5935c4d23d | ||
|
|
1c4db91593 | ||
|
|
9d2066bd53 | ||
|
|
f57e70912c | ||
|
|
a7f4a53ce8 | ||
|
|
8f3bfbd1c4 | ||
|
|
f8dfd034f5 | ||
|
|
fc40ba8e7e | ||
|
|
1f2f79a7a7 | ||
|
|
6e09c1142e | ||
|
|
27677dd8bd | ||
|
|
be4f7ef361 | ||
|
|
6b83d82e82 | ||
|
|
6fb2d3d7d7 | ||
|
|
425003417d | ||
|
|
a8893094ea | ||
|
|
a03d852d65 | ||
|
|
e3b85b9829 | ||
|
|
3014a91b07 | ||
|
|
981de05181 | ||
|
|
9950440cf6 | ||
|
|
80d8fe7786 | ||
|
|
b37626ce6b | ||
|
|
1ee57cf727 | ||
|
|
4322ca6f4a | ||
|
|
afbb1af6c5 | ||
|
|
600c46b5a4 | ||
|
|
7d5ca1176d | ||
|
|
5915d479dc | ||
|
|
30098b04d7 | ||
|
|
f72214725d | ||
|
|
9bef525944 | ||
|
|
edd6289f26 | ||
|
|
d0b98c75e5 | ||
|
|
e332a717a8 | ||
|
|
23cfcd60df | ||
|
|
465536e811 | ||
|
|
3d1c3b78ec | ||
|
|
1861e76360 | ||
|
|
c248da0317 | ||
|
|
b7f4755020 | ||
|
|
3e82cbd55b | ||
|
|
11a968f5c3 | ||
|
|
5d8c665baf | ||
|
|
9df78b3379 | ||
|
|
20578da204 | ||
|
|
564fe6f089 | ||
|
|
dd8373a424 | ||
|
|
9be3c27bb7 | ||
|
|
e12184661e | ||
|
|
3a57106c1e | ||
|
|
2c30ba400b | ||
|
|
5d3af3bc62 | ||
|
|
e9f182def7 | ||
|
|
1b31e2f345 | ||
|
|
58d5b39c9a | ||
|
|
157d6d2db7 | ||
|
|
d5593d647c | ||
|
|
83715eca49 | ||
|
|
7dfa99a6f7 | ||
|
|
ac2b71f240 | ||
|
|
578bde1e0d | ||
|
|
e2c03845c7 | ||
|
|
1523ef2494 | ||
|
|
cdec53b22b | ||
|
|
a6afcb4c1d | ||
|
|
2a68bcbeb3 | ||
|
|
c8af8e9555 | ||
|
|
e77988f747 | ||
|
|
96ad19a627 | ||
|
|
fe81b1d712 | ||
|
|
d1ecb46076 | ||
|
|
fff59da962 | ||
|
|
9e3ea2687c | ||
|
|
cfd6b21d0e | ||
|
|
118507953b | ||
|
|
befa421a57 | ||
|
|
e6fdac7bfb | ||
|
|
67f90dae54 | ||
|
|
31face5740 | ||
|
|
0da6de6624 | ||
|
|
0eae9f456c | ||
|
|
561a10c491 | ||
|
|
c6b4de520a | ||
|
|
f49297e2c1 | ||
|
|
966228a6a9 | ||
|
|
5fb8f779ca | ||
|
|
88e29c728c | ||
|
|
a63ec41a7b | ||
|
|
64849e81f5 | ||
|
|
d3bb32273e | ||
|
|
7113dc21a9 | ||
|
|
4ab814fd50 | ||
|
|
c83bdb73a4 | ||
|
|
ea9eed14f8 | ||
|
|
91e445c260 | ||
|
|
6cd3bc3a46 | ||
|
|
37eaca719a | ||
|
|
ff6114599e | ||
|
|
532b9653be | ||
|
|
b7aac92ac4 | ||
|
|
1a48bce294 | ||
|
|
17b18971f1 | ||
|
|
9f101d3a9a | ||
|
|
a884955cd6 | ||
|
|
f72ac60b01 | ||
|
|
761188cd1d | ||
|
|
d9cadf9737 | ||
|
|
a4382607d7 | ||
|
|
84e115834f | ||
|
|
78f7e5147b | ||
|
|
7b0a0f3dac | ||
|
|
3711143549 | ||
|
|
777756e1c2 | ||
|
|
1b34446bf5 | ||
|
|
13db0489c8 | ||
|
|
2af977f947 | ||
|
|
7cee8c2345 | ||
|
|
e0aa8457c2 | ||
|
|
822388fe92 | ||
|
|
e18f43ddad | ||
|
|
991ed3ab58 | ||
|
|
5676a6b38d | ||
|
|
2b1f68c928 | ||
|
|
673583a38b | ||
|
|
b4cce3ac7a | ||
|
|
4023b76ed3 | ||
|
|
e9d117d221 | ||
|
|
149dc7c4e7 | ||
|
|
e70984745b | ||
|
|
da9f28d270 | ||
|
|
99b4f2a24e | ||
|
|
f9fae2c439 | ||
|
|
9bd64c8a1f | ||
|
|
c429ccb64f | ||
|
|
57d008a33d | ||
|
|
6b0d6e2540 | ||
|
|
dfef943f0a | ||
|
|
39c682219e | ||
|
|
66307695eb | ||
|
|
d134a8c7f3 | ||
|
|
bb3d7343f4 | ||
|
|
1a05ee941e | ||
|
|
1b3f987c4d | ||
|
|
81c68f582d | ||
|
|
d842b28a15 | ||
|
|
ed4529e246 | ||
|
|
bf08b485bd | ||
|
|
845d97b6a5 | ||
|
|
8b64705e05 | ||
|
|
bcb0ed0866 | ||
|
|
9ae1b732ef | ||
|
|
385e66cbd5 | ||
|
|
2d317ce423 | ||
|
|
284d24209b | ||
|
|
b8174decf3 | ||
|
|
41cc5bcd4f | ||
|
|
f6d98a908a | ||
|
|
d03eca8450 | ||
|
|
be9a2fb134 | ||
|
|
8d2f98fb01 | ||
|
|
d5f6caba3f | ||
|
|
4682c2e3e2 | ||
|
|
34dd7324d9 | ||
|
|
9ef24fd400 | ||
|
|
85cd55e22b | ||
|
|
4e4ed2ea17 | ||
|
|
521b121815 | ||
|
|
e74235fdce | ||
|
|
f37b79cf4f | ||
|
|
889480cef9 | ||
|
|
01449a2f41 | ||
|
|
d46b489e21 | ||
|
|
935a0e5708 | ||
|
|
baa1e95b9d | ||
|
|
87a61c3b88 | ||
|
|
e9a32b83c2 | ||
|
|
5ba4586e58 | ||
|
|
e25f8ed56c | ||
|
|
0bc8a592a6 | ||
|
|
1d7dd5f261 | ||
|
|
19b8416a81 | ||
|
|
5020bfa2a9 | ||
|
|
b9910ab037 | ||
|
|
902f968056 | ||
|
|
bd259eeb23 | ||
|
|
476f367cf1 | ||
|
|
dda8a2b238 | ||
|
|
7ee99af9f8 | ||
|
|
4347d2468c | ||
|
|
cf1d3f7a7c | ||
|
|
0fa55ed2b4 | ||
|
|
63c9fac9fc | ||
|
|
8c7901c984 | ||
|
|
aa2eb48b9c | ||
|
|
7aeabbabd4 | ||
|
|
e58291e070 | ||
|
|
411d5fda58 | ||
|
|
19775abdda | ||
|
|
a87a07ec8a | ||
|
|
0a5821a811 | ||
|
|
b796f6ec01 | ||
|
|
92112a61db | ||
|
|
a2b00495cd | ||
|
|
f8575c401c | ||
|
|
3367b2aa27 | ||
|
|
bcbb447357 | ||
|
|
8eb11bd304 | ||
|
|
6c6f1e9660 | ||
|
|
e550e252a7 | ||
|
|
e4d572192d | ||
|
|
2601f413c3 | ||
|
|
1bdd9e313f | ||
|
|
9d2784cdb9 | ||
|
|
bcde2fca5a | ||
|
|
9b6fffd00a | ||
|
|
99346314f5 | ||
|
|
1968a4b7d2 | ||
|
|
a68e32d95b | ||
|
|
6360809310 | ||
|
|
238200f652 | ||
|
|
d54605bd82 | ||
|
|
92803facf6 | ||
|
|
443ee26af3 | ||
|
|
395810a60b | ||
|
|
6c03fe1a4d | ||
|
|
a863ac9862 | ||
|
|
8f366babe4 | ||
|
|
083ec9325e | ||
|
|
74039fc0f1 | ||
|
|
17287bc8d0 | ||
|
|
964b14d59c | ||
|
|
d3e53eaf27 | ||
|
|
20a603de01 | ||
|
|
6453f5c395 | ||
|
|
3cf35b0710 | ||
|
|
bc5b0c82ac | ||
|
|
63b13c7e2f | ||
|
|
8ff75eaf12 | ||
|
|
76211500e8 | ||
|
|
28a05f9940 | ||
|
|
701d43892f | ||
|
|
3ae049b501 | ||
|
|
e9f70e8585 | ||
|
|
8582ed4d4f | ||
|
|
7fabe03a8b | ||
|
|
0992c5a809 | ||
|
|
5d3c898a94 | ||
|
|
0e0e395b9e | ||
|
|
7a8a39a141 | ||
|
|
b897389b87 | ||
|
|
a1e89afcc1 | ||
|
|
e4f7155369 | ||
|
|
9d9378436b | ||
|
|
141dc1af4b | ||
|
|
c83c19d9cd | ||
|
|
35dc417b18 | ||
|
|
1f3afa38e8 | ||
|
|
633f848481 | ||
|
|
24fbafa9a7 | ||
|
|
ca92597e1f | ||
|
|
c621c80afc | ||
|
|
ba4a55f6d9 | ||
|
|
511b2c91e3 | ||
|
|
b48d72a2b8 | ||
|
|
b4e2e746b3 | ||
|
|
3d5c03ec29 | ||
|
|
dc8a63cb8b | ||
|
|
5c8880ed3f | ||
|
|
01d76e4799 | ||
|
|
a393ae79d2 | ||
|
|
abcca0f9bd | ||
|
|
73c405f74a | ||
|
|
1f6a446f6c | ||
|
|
b2aff036ad | ||
|
|
58f4185925 | ||
|
|
96c9ffdedc | ||
|
|
1aeaf811b0 | ||
|
|
8e2b17e0c5 | ||
|
|
66e33abd7b | ||
|
|
29de43d307 | ||
|
|
a10603f9f0 | ||
|
|
7387bc574f | ||
|
|
bce8c0eb12 | ||
|
|
7a2c4d3cf1 | ||
|
|
9297ea48e5 | ||
|
|
147eba11fd | ||
|
|
f06dd8df06 | ||
|
|
367372f526 | ||
|
|
ad943bd8cf | ||
|
|
baf9505bfd | ||
|
|
e6c38e078a | ||
|
|
abcaa8c7a9 | ||
|
|
1295b67057 | ||
|
|
34e2425b4d | ||
|
|
f1de88c198 | ||
|
|
57ea4e8897 | ||
|
|
b5c2b1880d | ||
|
|
a64d8d2d66 | ||
|
|
37721ebd7c | ||
|
|
35988d77ec | ||
|
|
6750696874 | ||
|
|
9c29853014 | ||
|
|
8a5b139a9f | ||
|
|
b6c8c1e89d | ||
|
|
a6c68e8690 | ||
|
|
76391bba3f | ||
|
|
08886eaaa3 | ||
|
|
bbf2205640 | ||
|
|
582a4e261a | ||
|
|
83e64c1ac9 | ||
|
|
8978d16659 | ||
|
|
7a6c40872d | ||
|
|
75093ebe1c | ||
|
|
fcf08299fa | ||
|
|
1f2fb823a3 | ||
|
|
36b0070b71 | ||
|
|
230ca789e2 | ||
|
|
4f2166c503 | ||
|
|
7d89855c55 | ||
|
|
aa91f6e700 | ||
|
|
59cfff02f6 | ||
|
|
1838ab019b | ||
|
|
0ffc251704 | ||
|
|
76b5208b11 | ||
|
|
a767c584c7 | ||
|
|
8cab78abbc | ||
|
|
dcc2de15a6 | ||
|
|
1287328b6f | ||
|
|
b9b94715fa | ||
|
|
efb93d18cf | ||
|
|
c3a8a5374f | ||
|
|
247fab47ca | ||
|
|
dae00fe184 | ||
|
|
76361ae3ab | ||
|
|
e25fedf932 | ||
|
|
ddc5683c67 | ||
|
|
68ba1afb34 | ||
|
|
4b7406719c | ||
|
|
821ed35be1 | ||
|
|
ed65131c1c | ||
|
|
1766cd4123 | ||
|
|
d4ed79ffd0 | ||
|
|
86d38c2d82 | ||
|
|
88fe4de151 | ||
|
|
ee26b68fe1 | ||
|
|
a42e1c82d9 | ||
|
|
c4feb7a457 | ||
|
|
9e908ad6be | ||
|
|
3282d22dd9 | ||
|
|
952b0f8c48 | ||
|
|
e5eb9610dc | ||
|
|
2957d4306d | ||
|
|
b56e7e66cc | ||
|
|
0fc4d7f52a | ||
|
|
5ceff756e1 | ||
|
|
009b16fab8 | ||
|
|
b7e401b6b6 | ||
|
|
9c4cbaab7b | ||
|
|
15792b153f | ||
|
|
481f696a87 | ||
|
|
7a9ddcd590 | ||
|
|
f99e3ddd6d | ||
|
|
cbc405c9e3 | ||
|
|
51e72d41c2 | ||
|
|
762652279b | ||
|
|
72ea3eedc9 | ||
|
|
a00e0bc189 | ||
|
|
67945e8d62 | ||
|
|
d2a852b982 | ||
|
|
ded95d5c70 | ||
|
|
a441059761 | ||
|
|
84ac889e22 | ||
|
|
e9f0be06eb | ||
|
|
08ed62852a | ||
|
|
85dd070dea | ||
|
|
0b95efff27 | ||
|
|
3c8fa0f913 | ||
|
|
b1d25ed0dd | ||
|
|
48aaf6ce4e | ||
|
|
beafaef92f | ||
|
|
14c77f8295 | ||
|
|
bf15d0a3f5 | ||
|
|
ca47b0d79c | ||
|
|
9b1a6b30d9 | ||
|
|
310eed825e | ||
|
|
a642ca4ea8 | ||
|
|
e849df64dc | ||
|
|
e913de0720 | ||
|
|
c0a6e675a3 | ||
|
|
cc366f4baa | ||
|
|
4b1956ab49 | ||
|
|
9cb5e22861 | ||
|
|
aa6b9e3a20 | ||
|
|
c67df653b6 | ||
|
|
b17e6fdd07 | ||
|
|
b603d9c6ba | ||
|
|
5ca1b96988 | ||
|
|
a007ba7b1b | ||
|
|
ff713a41e0 | ||
|
|
9d28763753 | ||
|
|
3696532f04 | ||
|
|
cbe388ece3 | ||
|
|
034d4513d9 |
@@ -29,10 +29,12 @@ git log --oneline --left-right main...upstream/main | head -20
|
||||
```
|
||||
|
||||
This shows:
|
||||
|
||||
- `<` = your local commits (ahead)
|
||||
- `>` = upstream commits you're missing (behind)
|
||||
|
||||
**Decision point:**
|
||||
|
||||
- Few local commits, many upstream → **Rebase** (cleaner history)
|
||||
- Many local commits or shared branch → **Merge** (preserves history)
|
||||
|
||||
@@ -70,12 +72,12 @@ git rebase --abort
|
||||
|
||||
### Common Conflict Patterns
|
||||
|
||||
| File | Resolution |
|
||||
|------|------------|
|
||||
| `package.json` | Take upstream deps, keep local scripts if needed |
|
||||
| `pnpm-lock.yaml` | Accept upstream, regenerate with `pnpm install` |
|
||||
| `*.patch` files | Usually take upstream version |
|
||||
| Source files | Merge logic carefully, prefer upstream structure |
|
||||
| File | Resolution |
|
||||
| ---------------- | ------------------------------------------------ |
|
||||
| `package.json` | Take upstream deps, keep local scripts if needed |
|
||||
| `pnpm-lock.yaml` | Accept upstream, regenerate with `pnpm install` |
|
||||
| `*.patch` files | Usually take upstream version |
|
||||
| Source files | Merge logic carefully, prefer upstream structure |
|
||||
|
||||
---
|
||||
|
||||
@@ -88,6 +90,7 @@ git merge upstream/main --no-edit
|
||||
```
|
||||
|
||||
Resolve conflicts same as rebase, then:
|
||||
|
||||
```bash
|
||||
git add <resolved-files>
|
||||
git commit
|
||||
@@ -170,6 +173,7 @@ pnpm clawdbot agent --message "Verification: macOS app rebuild successful - agen
|
||||
Upstream updates may introduce Swift 6.2 / macOS 26 SDK incompatibilities. Use analyze-mode for systematic debugging:
|
||||
|
||||
### Analyze-Mode Investigation
|
||||
|
||||
```bash
|
||||
# Gather context with parallel agents
|
||||
morph-mcp_warpgrep_codebase_search search_string="Find deprecated FileManager.default and Thread.isMainThread usages in Swift files" repo_path="/Volumes/Main SSD/Developer/clawdis"
|
||||
@@ -179,6 +183,7 @@ morph-mcp_warpgrep_codebase_search search_string="Locate Peekaboo submodule and
|
||||
### Common Swift 6.2 Fixes
|
||||
|
||||
**FileManager.default Deprecation:**
|
||||
|
||||
```bash
|
||||
# Search for deprecated usage
|
||||
grep -r "FileManager\.default" src/ apps/ --include="*.swift"
|
||||
@@ -189,6 +194,7 @@ grep -r "FileManager\.default" src/ apps/ --include="*.swift"
|
||||
```
|
||||
|
||||
**Thread.isMainThread Deprecation:**
|
||||
|
||||
```bash
|
||||
# Search for deprecated usage
|
||||
grep -r "Thread\.isMainThread" src/ apps/ --include="*.swift"
|
||||
@@ -199,6 +205,7 @@ grep -r "Thread\.isMainThread" src/ apps/ --include="*.swift"
|
||||
```
|
||||
|
||||
### Peekaboo Submodule Fixes
|
||||
|
||||
```bash
|
||||
# Check Peekaboo for concurrency issues
|
||||
cd src/canvas-host/a2ui
|
||||
@@ -210,6 +217,7 @@ pnpm canvas:a2ui:bundle
|
||||
```
|
||||
|
||||
### macOS App Concurrency Fixes
|
||||
|
||||
```bash
|
||||
# Check macOS app for issues
|
||||
grep -r "Thread\.isMainThread\|FileManager\.default" apps/macos/ --include="*.swift"
|
||||
@@ -220,7 +228,9 @@ cd apps/macos && rm -rf .build .swiftpm
|
||||
```
|
||||
|
||||
### Model Configuration Updates
|
||||
|
||||
If upstream introduced new model configurations:
|
||||
|
||||
```bash
|
||||
# Check for OpenRouter API key requirements
|
||||
grep -r "openrouter\|OPENROUTER" src/ --include="*.ts" --include="*.js"
|
||||
@@ -265,6 +275,7 @@ Common issue: `fetch.preconnect` type mismatch. Fix by using `FetchLike` type in
|
||||
### macOS App Crashes on Launch
|
||||
|
||||
Usually resource bundle mismatch. Full rebuild required:
|
||||
|
||||
```bash
|
||||
cd apps/macos && rm -rf .build .swiftpm
|
||||
./scripts/restart-mac.sh
|
||||
@@ -285,12 +296,14 @@ pnpm install 2>&1 | grep -i patch
|
||||
**Symptoms:** Build fails with deprecation warnings about `FileManager.default` or `Thread.isMainThread`
|
||||
|
||||
**Search-Mode Investigation:**
|
||||
|
||||
```bash
|
||||
# Exhaustive search for deprecated APIs
|
||||
morph-mcp_warpgrep_codebase_search search_string="Find all Swift files using deprecated FileManager.default or Thread.isMainThread" repo_path="/Volumes/Main SSD/Developer/clawdis"
|
||||
```
|
||||
|
||||
**Quick Fix Commands:**
|
||||
|
||||
```bash
|
||||
# Find all affected files
|
||||
find . -name "*.swift" -exec grep -l "FileManager\.default\|Thread\.isMainThread" {} \;
|
||||
@@ -303,6 +316,7 @@ grep -rn "Thread\.isMainThread" --include="*.swift" .
|
||||
```
|
||||
|
||||
**Rebuild After Fixes:**
|
||||
|
||||
```bash
|
||||
# Clean all build artifacts
|
||||
rm -rf apps/macos/.build apps/macos/.swiftpm
|
||||
|
||||
126
.agents/skills/PR_WORKFLOW.md
Normal file
126
.agents/skills/PR_WORKFLOW.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# PR Review Instructions
|
||||
|
||||
Please read this in full and do not skip sections.
|
||||
|
||||
## Working rule
|
||||
|
||||
Skills execute workflow, maintainers provide judgment.
|
||||
Always pause between skills to evaluate technical direction, not just command success.
|
||||
|
||||
These three skills must be used in order:
|
||||
|
||||
1. `review-pr`
|
||||
2. `prepare-pr`
|
||||
3. `merge-pr`
|
||||
|
||||
They are necessary, but not sufficient. Maintainers must steer between steps and understand the code before moving forward.
|
||||
|
||||
Treat PRs as reports first, code second.
|
||||
If submitted code is low quality, ignore it and implement the best solution for the problem.
|
||||
|
||||
Do not continue if you cannot verify the problem is real or test the fix.
|
||||
|
||||
## PR quality bar
|
||||
|
||||
- Do not trust PR code by default.
|
||||
- Do not merge changes you cannot validate with a reproducible problem and a tested fix.
|
||||
- Keep types strict. Do not use `any` in implementation code.
|
||||
- Keep external-input boundaries typed and validated, including CLI input, environment variables, network payloads, and tool output.
|
||||
- Keep implementations properly scoped. Fix root causes, not local symptoms.
|
||||
- Identify and reuse canonical sources of truth so behavior does not drift across the codebase.
|
||||
- Harden changes. Always evaluate security impact and abuse paths.
|
||||
- Understand the system before changing it. Never make the codebase messier just to clear a PR queue.
|
||||
|
||||
## Unified workflow
|
||||
|
||||
Entry criteria:
|
||||
|
||||
- PR URL/number is known.
|
||||
- Problem statement is clear enough to attempt reproduction.
|
||||
- A realistic verification path exists (tests, integration checks, or explicit manual validation).
|
||||
|
||||
### 1) `review-pr`
|
||||
|
||||
Purpose:
|
||||
|
||||
- Review only: correctness, value, security risk, tests, docs, and changelog impact.
|
||||
- Produce structured findings and a recommendation.
|
||||
|
||||
Expected output:
|
||||
|
||||
- Recommendation: ready, needs work, needs discussion, or close.
|
||||
- `.local/review.md` with actionable findings.
|
||||
|
||||
Maintainer checkpoint before `prepare-pr`:
|
||||
|
||||
```
|
||||
What problem are they trying to solve?
|
||||
What is the most optimal implementation?
|
||||
Is the code properly scoped?
|
||||
Can we fix up everything?
|
||||
Do we have any questions?
|
||||
```
|
||||
|
||||
Stop and escalate instead of continuing if:
|
||||
|
||||
- The problem cannot be reproduced or confirmed.
|
||||
- The proposed PR scope does not match the stated problem.
|
||||
- The design introduces unresolved security or trust-boundary concerns.
|
||||
|
||||
### 2) `prepare-pr`
|
||||
|
||||
Purpose:
|
||||
|
||||
- Make the PR merge-ready on its head branch.
|
||||
- Rebase onto current `main`, fix blocker/important findings, and run gates.
|
||||
|
||||
Expected output:
|
||||
|
||||
- Updated code and tests on the PR head branch.
|
||||
- `.local/prep.md` with changes, verification, and current HEAD SHA.
|
||||
- Final status: `PR is ready for /mergepr`.
|
||||
|
||||
Maintainer checkpoint before `merge-pr`:
|
||||
|
||||
```
|
||||
Is this the most optimal implementation?
|
||||
Is the code properly scoped?
|
||||
Is the code properly typed?
|
||||
Is the code hardened?
|
||||
Do we have enough tests?
|
||||
Are tests using fake timers where relevant? (e.g., debounce/throttle, retry backoff, timeout branches, delayed callbacks, polling loops)
|
||||
Do not add performative tests, ensure tests are real and there are no regressions.
|
||||
Take your time, fix it properly, refactor if necessary.
|
||||
Do you see any follow-up refactors we should do?
|
||||
Did any changes introduce any potential security vulnerabilities?
|
||||
```
|
||||
|
||||
Stop and escalate instead of continuing if:
|
||||
|
||||
- You cannot verify behavior changes with meaningful tests or validation.
|
||||
- Fixing findings requires broad architecture changes outside safe PR scope.
|
||||
- Security hardening requirements remain unresolved.
|
||||
|
||||
### 3) `merge-pr`
|
||||
|
||||
Purpose:
|
||||
|
||||
- Merge only after review and prep artifacts are present and checks are green.
|
||||
- Use squash merge flow and verify the PR ends in `MERGED` state.
|
||||
|
||||
Go or no-go checklist before merge:
|
||||
|
||||
- All BLOCKER and IMPORTANT findings are resolved.
|
||||
- Verification is meaningful and regression risk is acceptably low.
|
||||
- Docs and changelog are updated when required.
|
||||
- Required CI checks are green and the branch is not behind `main`.
|
||||
|
||||
Expected output:
|
||||
|
||||
- Successful merge commit and recorded merge SHA.
|
||||
- Worktree cleanup after successful merge.
|
||||
|
||||
Maintainer checkpoint after merge:
|
||||
|
||||
- Were any refactors intentionally deferred and now need follow-up issue(s)?
|
||||
- Did this reveal broader architecture or test gaps we should address?
|
||||
185
.agents/skills/merge-pr/SKILL.md
Normal file
185
.agents/skills/merge-pr/SKILL.md
Normal file
@@ -0,0 +1,185 @@
|
||||
---
|
||||
name: merge-pr
|
||||
description: Merge a GitHub PR via squash after /preparepr. Use when asked to merge a ready PR. Do not push to main or modify code. Ensure the PR ends in MERGED state and clean up worktrees after success.
|
||||
---
|
||||
|
||||
# Merge PR
|
||||
|
||||
## Overview
|
||||
|
||||
Merge a prepared PR via `gh pr merge --squash` and clean up the worktree after success.
|
||||
|
||||
## Inputs
|
||||
|
||||
- Ask for PR number or URL.
|
||||
- If missing, auto-detect from conversation.
|
||||
- If ambiguous, ask.
|
||||
|
||||
## Safety
|
||||
|
||||
- Use `gh pr merge --squash` as the only path to `main`.
|
||||
- Do not run `git push` at all during merge.
|
||||
- Do not run gateway stop commands. Do not kill processes. Do not touch port 18792.
|
||||
|
||||
## Execution Rule
|
||||
|
||||
- Execute the workflow. Do not stop after printing the TODO checklist.
|
||||
- If delegating, require the delegate to run commands and capture outputs.
|
||||
|
||||
## Known Footguns
|
||||
|
||||
- If you see "fatal: not a git repository", you are in the wrong directory. Use `~/dev/openclaw` if available; otherwise ask user.
|
||||
- Read `.local/review.md` and `.local/prep.md` in the worktree. Do not skip.
|
||||
- Clean up the real worktree directory `.worktrees/pr-<PR>` only after a successful merge.
|
||||
- Expect cleanup to remove `.local/` artifacts.
|
||||
|
||||
## Completion Criteria
|
||||
|
||||
- Ensure `gh pr merge` succeeds.
|
||||
- Ensure PR state is `MERGED`, never `CLOSED`.
|
||||
- Record the merge SHA.
|
||||
- Run cleanup only after merge success.
|
||||
|
||||
## First: Create a TODO Checklist
|
||||
|
||||
Create a checklist of all merge steps, print it, then continue and execute the commands.
|
||||
|
||||
## Setup: Use a Worktree
|
||||
|
||||
Use an isolated worktree for all merge work.
|
||||
|
||||
```sh
|
||||
cd ~/dev/openclaw
|
||||
# Sanity: confirm you are in the repo
|
||||
git rev-parse --show-toplevel
|
||||
|
||||
WORKTREE_DIR=".worktrees/pr-<PR>"
|
||||
```
|
||||
|
||||
Run all commands inside the worktree directory.
|
||||
|
||||
## Load Local Artifacts (Mandatory)
|
||||
|
||||
Expect these files from earlier steps:
|
||||
|
||||
- `.local/review.md` from `/reviewpr`
|
||||
- `.local/prep.md` from `/preparepr`
|
||||
|
||||
```sh
|
||||
ls -la .local || true
|
||||
|
||||
if [ -f .local/review.md ]; then
|
||||
echo "Found .local/review.md"
|
||||
sed -n '1,120p' .local/review.md
|
||||
else
|
||||
echo "Missing .local/review.md. Stop and run /reviewpr, then /preparepr."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -f .local/prep.md ]; then
|
||||
echo "Found .local/prep.md"
|
||||
sed -n '1,120p' .local/prep.md
|
||||
else
|
||||
echo "Missing .local/prep.md. Stop and run /preparepr first."
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
## Steps
|
||||
|
||||
1. Identify PR meta
|
||||
|
||||
```sh
|
||||
gh pr view <PR> --json number,title,state,isDraft,author,headRefName,baseRefName,headRepository,body --jq '{number,title,state,isDraft,author:.author.login,head:.headRefName,base:.baseRefName,headRepo:.headRepository.nameWithOwner,body}'
|
||||
contrib=$(gh pr view <PR> --json author --jq .author.login)
|
||||
head=$(gh pr view <PR> --json headRefName --jq .headRefName)
|
||||
head_repo_url=$(gh pr view <PR> --json headRepository --jq .headRepository.url)
|
||||
```
|
||||
|
||||
2. Run sanity checks
|
||||
|
||||
Stop if any are true:
|
||||
|
||||
- PR is a draft.
|
||||
- Required checks are failing.
|
||||
- Branch is behind main.
|
||||
|
||||
```sh
|
||||
# Checks
|
||||
gh pr checks <PR>
|
||||
|
||||
# Check behind main
|
||||
git fetch origin main
|
||||
git fetch origin pull/<PR>/head:pr-<PR>
|
||||
git merge-base --is-ancestor origin/main pr-<PR> || echo "PR branch is behind main, run /preparepr"
|
||||
```
|
||||
|
||||
If anything is failing or behind, stop and say to run `/preparepr`.
|
||||
|
||||
3. Merge PR and delete branch
|
||||
|
||||
If checks are still running, use `--auto` to queue the merge.
|
||||
|
||||
```sh
|
||||
# Check status first
|
||||
check_status=$(gh pr checks <PR> 2>&1)
|
||||
if echo "$check_status" | grep -q "pending\|queued"; then
|
||||
echo "Checks still running, using --auto to queue merge"
|
||||
gh pr merge <PR> --squash --delete-branch --auto
|
||||
echo "Merge queued. Monitor with: gh pr checks <PR> --watch"
|
||||
else
|
||||
gh pr merge <PR> --squash --delete-branch
|
||||
fi
|
||||
```
|
||||
|
||||
If merge fails, report the error and stop. Do not retry in a loop.
|
||||
If the PR needs changes beyond what `/preparepr` already did, stop and say to run `/preparepr` again.
|
||||
|
||||
4. Get merge SHA
|
||||
|
||||
```sh
|
||||
merge_sha=$(gh pr view <PR> --json mergeCommit --jq '.mergeCommit.oid')
|
||||
echo "merge_sha=$merge_sha"
|
||||
```
|
||||
|
||||
5. Optional comment
|
||||
|
||||
Use a literal multiline string or heredoc for newlines.
|
||||
|
||||
```sh
|
||||
gh pr comment <PR> -F - <<'EOF'
|
||||
Merged via squash.
|
||||
|
||||
- Merge commit: $merge_sha
|
||||
|
||||
Thanks @$contrib!
|
||||
EOF
|
||||
```
|
||||
|
||||
6. Verify PR state is MERGED
|
||||
|
||||
```sh
|
||||
gh pr view <PR> --json state --jq .state
|
||||
```
|
||||
|
||||
7. Clean up worktree only on success
|
||||
|
||||
Run cleanup only if step 6 returned `MERGED`.
|
||||
|
||||
```sh
|
||||
cd ~/dev/openclaw
|
||||
|
||||
git worktree remove ".worktrees/pr-<PR>" --force
|
||||
|
||||
git branch -D temp/pr-<PR> 2>/dev/null || true
|
||||
git branch -D pr-<PR> 2>/dev/null || true
|
||||
```
|
||||
|
||||
## Guardrails
|
||||
|
||||
- Worktree only.
|
||||
- Do not close PRs.
|
||||
- End in MERGED state.
|
||||
- Clean up only after merge success.
|
||||
- Never push to main. Use `gh pr merge --squash` only.
|
||||
- Do not run `git push` at all in this command.
|
||||
4
.agents/skills/merge-pr/agents/openai.yaml
Normal file
4
.agents/skills/merge-pr/agents/openai.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Merge PR"
|
||||
short_description: "Merge GitHub PRs via squash"
|
||||
default_prompt: "Use $merge-pr to merge a GitHub PR via squash after preparation."
|
||||
248
.agents/skills/prepare-pr/SKILL.md
Normal file
248
.agents/skills/prepare-pr/SKILL.md
Normal file
@@ -0,0 +1,248 @@
|
||||
---
|
||||
name: prepare-pr
|
||||
description: Prepare a GitHub PR for merge by rebasing onto main, fixing review findings, running gates, committing fixes, and pushing to the PR head branch. Use after /reviewpr. Never merge or push to main.
|
||||
---
|
||||
|
||||
# Prepare PR
|
||||
|
||||
## Overview
|
||||
|
||||
Prepare a PR branch for merge with review fixes, green gates, and an updated head branch.
|
||||
|
||||
## Inputs
|
||||
|
||||
- Ask for PR number or URL.
|
||||
- If missing, auto-detect from conversation.
|
||||
- If ambiguous, ask.
|
||||
|
||||
## Safety
|
||||
|
||||
- Never push to `main` or `origin/main`. Push only to the PR head branch.
|
||||
- Never run `git push` without specifying remote and branch explicitly. Do not run bare `git push`.
|
||||
- Do not run gateway stop commands. Do not kill processes. Do not touch port 18792.
|
||||
- Do not run `git clean -fdx`.
|
||||
- Do not run `git add -A` or `git add .`. Stage only specific files changed.
|
||||
|
||||
## Execution Rule
|
||||
|
||||
- Execute the workflow. Do not stop after printing the TODO checklist.
|
||||
- If delegating, require the delegate to run commands and capture outputs.
|
||||
|
||||
## Known Footguns
|
||||
|
||||
- If you see "fatal: not a git repository", you are in the wrong directory. Use `~/dev/openclaw` if available; otherwise ask user.
|
||||
- Do not run `git clean -fdx`.
|
||||
- Do not run `git add -A` or `git add .`.
|
||||
|
||||
## Completion Criteria
|
||||
|
||||
- Rebase PR commits onto `origin/main`.
|
||||
- Fix all BLOCKER and IMPORTANT items from `.local/review.md`.
|
||||
- Run gates and pass.
|
||||
- Commit prep changes.
|
||||
- Push the updated HEAD back to the PR head branch.
|
||||
- Write `.local/prep.md` with a prep summary.
|
||||
- Output exactly: `PR is ready for /mergepr`.
|
||||
|
||||
## First: Create a TODO Checklist
|
||||
|
||||
Create a checklist of all prep steps, print it, then continue and execute the commands.
|
||||
|
||||
## Setup: Use a Worktree
|
||||
|
||||
Use an isolated worktree for all prep work.
|
||||
|
||||
```sh
|
||||
cd ~/openclaw
|
||||
# Sanity: confirm you are in the repo
|
||||
git rev-parse --show-toplevel
|
||||
|
||||
WORKTREE_DIR=".worktrees/pr-<PR>"
|
||||
```
|
||||
|
||||
Run all commands inside the worktree directory.
|
||||
|
||||
## Load Review Findings (Mandatory)
|
||||
|
||||
```sh
|
||||
if [ -f .local/review.md ]; then
|
||||
echo "Found review findings from /reviewpr"
|
||||
else
|
||||
echo "Missing .local/review.md. Run /reviewpr first and save findings."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Read it
|
||||
sed -n '1,200p' .local/review.md
|
||||
```
|
||||
|
||||
## Steps
|
||||
|
||||
1. Identify PR meta (author, head branch, head repo URL)
|
||||
|
||||
```sh
|
||||
gh pr view <PR> --json number,title,author,headRefName,baseRefName,headRepository,body --jq '{number,title,author:.author.login,head:.headRefName,base:.baseRefName,headRepo:.headRepository.nameWithOwner,body}'
|
||||
contrib=$(gh pr view <PR> --json author --jq .author.login)
|
||||
head=$(gh pr view <PR> --json headRefName --jq .headRefName)
|
||||
head_repo_url=$(gh pr view <PR> --json headRepository --jq .headRepository.url)
|
||||
```
|
||||
|
||||
2. Fetch the PR branch tip into a local ref
|
||||
|
||||
```sh
|
||||
git fetch origin pull/<PR>/head:pr-<PR>
|
||||
```
|
||||
|
||||
3. Rebase PR commits onto latest main
|
||||
|
||||
```sh
|
||||
# Move worktree to the PR tip first
|
||||
git reset --hard pr-<PR>
|
||||
|
||||
# Rebase onto current main
|
||||
git fetch origin main
|
||||
git rebase origin/main
|
||||
```
|
||||
|
||||
If conflicts happen:
|
||||
|
||||
- Resolve each conflicted file.
|
||||
- Run `git add <resolved_file>` for each file.
|
||||
- Run `git rebase --continue`.
|
||||
|
||||
If the rebase gets confusing or you resolve conflicts 3 or more times, stop and report.
|
||||
|
||||
4. Fix issues from `.local/review.md`
|
||||
|
||||
- Fix all BLOCKER and IMPORTANT items.
|
||||
- NITs are optional.
|
||||
- Keep scope tight.
|
||||
|
||||
Keep a running log in `.local/prep.md`:
|
||||
|
||||
- List which review items you fixed.
|
||||
- List which files you touched.
|
||||
- Note behavior changes.
|
||||
|
||||
5. Update `CHANGELOG.md` if flagged in review
|
||||
|
||||
Check `.local/review.md` section H for guidance.
|
||||
If flagged and user-facing:
|
||||
|
||||
- Check if `CHANGELOG.md` exists.
|
||||
|
||||
```sh
|
||||
ls CHANGELOG.md 2>/dev/null
|
||||
```
|
||||
|
||||
- Follow existing format.
|
||||
- Add a concise entry with PR number and contributor.
|
||||
|
||||
6. Update docs if flagged in review
|
||||
|
||||
Check `.local/review.md` section G for guidance.
|
||||
If flagged, update only docs related to the PR changes.
|
||||
|
||||
7. Commit prep fixes
|
||||
|
||||
Stage only specific files:
|
||||
|
||||
```sh
|
||||
git add <file1> <file2> ...
|
||||
```
|
||||
|
||||
Preferred commit tool:
|
||||
|
||||
```sh
|
||||
committer "fix: <summary> (#<PR>) (thanks @$contrib)" <changed files>
|
||||
```
|
||||
|
||||
If `committer` is not found:
|
||||
|
||||
```sh
|
||||
git commit -m "fix: <summary> (#<PR>) (thanks @$contrib)"
|
||||
```
|
||||
|
||||
8. Run full gates before pushing
|
||||
|
||||
```sh
|
||||
pnpm install
|
||||
pnpm build
|
||||
pnpm ui:build
|
||||
pnpm check
|
||||
pnpm test
|
||||
```
|
||||
|
||||
Require all to pass. If something fails, fix, commit, and rerun. Allow at most 3 fix and rerun cycles. If gates still fail after 3 attempts, stop and report the failures. Do not loop indefinitely.
|
||||
|
||||
9. Push updates back to the PR head branch
|
||||
|
||||
```sh
|
||||
# Ensure remote for PR head exists
|
||||
git remote add prhead "$head_repo_url.git" 2>/dev/null || git remote set-url prhead "$head_repo_url.git"
|
||||
|
||||
# Use force with lease after rebase
|
||||
# Double check: $head must NOT be "main" or "master"
|
||||
echo "Pushing to branch: $head"
|
||||
if [ "$head" = "main" ] || [ "$head" = "master" ]; then
|
||||
echo "ERROR: head branch is main/master. This is wrong. Stopping."
|
||||
exit 1
|
||||
fi
|
||||
git push --force-with-lease prhead HEAD:$head
|
||||
```
|
||||
|
||||
10. Verify PR is not behind main (Mandatory)
|
||||
|
||||
```sh
|
||||
git fetch origin main
|
||||
git fetch origin pull/<PR>/head:pr-<PR>-verify --force
|
||||
git merge-base --is-ancestor origin/main pr-<PR>-verify && echo "PR is up to date with main" || echo "ERROR: PR is still behind main, rebase again"
|
||||
git branch -D pr-<PR>-verify 2>/dev/null || true
|
||||
```
|
||||
|
||||
If still behind main, repeat steps 2 through 9.
|
||||
|
||||
11. Write prep summary artifacts (Mandatory)
|
||||
|
||||
Update `.local/prep.md` with:
|
||||
|
||||
- Current HEAD sha from `git rev-parse HEAD`.
|
||||
- Short bullet list of changes.
|
||||
- Gate results.
|
||||
- Push confirmation.
|
||||
- Rebase verification result.
|
||||
|
||||
Create or overwrite `.local/prep.md` and verify it exists and is non-empty:
|
||||
|
||||
```sh
|
||||
git rev-parse HEAD
|
||||
ls -la .local/prep.md
|
||||
wc -l .local/prep.md
|
||||
```
|
||||
|
||||
12. Output
|
||||
|
||||
Include a diff stat summary:
|
||||
|
||||
```sh
|
||||
git diff --stat origin/main..HEAD
|
||||
git diff --shortstat origin/main..HEAD
|
||||
```
|
||||
|
||||
Report totals: X files changed, Y insertions(+), Z deletions(-).
|
||||
|
||||
If gates passed and push succeeded, print exactly:
|
||||
|
||||
```
|
||||
PR is ready for /mergepr
|
||||
```
|
||||
|
||||
Otherwise, list remaining failures and stop.
|
||||
|
||||
## Guardrails
|
||||
|
||||
- Worktree only.
|
||||
- Do not delete the worktree on success. `/mergepr` may reuse it.
|
||||
- Do not run `gh pr merge`.
|
||||
- Never push to main. Only push to the PR head branch.
|
||||
- Run and pass all gates before pushing.
|
||||
4
.agents/skills/prepare-pr/agents/openai.yaml
Normal file
4
.agents/skills/prepare-pr/agents/openai.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Prepare PR"
|
||||
short_description: "Prepare GitHub PRs for merge"
|
||||
default_prompt: "Use $prepare-pr to prep a GitHub PR for merge without merging."
|
||||
228
.agents/skills/review-pr/SKILL.md
Normal file
228
.agents/skills/review-pr/SKILL.md
Normal file
@@ -0,0 +1,228 @@
|
||||
---
|
||||
name: review-pr
|
||||
description: Review-only GitHub pull request analysis with the gh CLI. Use when asked to review a PR, provide structured feedback, or assess readiness to land. Do not merge, push, or make code changes you intend to keep.
|
||||
---
|
||||
|
||||
# Review PR
|
||||
|
||||
## Overview
|
||||
|
||||
Perform a thorough review-only PR assessment and return a structured recommendation on readiness for /preparepr.
|
||||
|
||||
## Inputs
|
||||
|
||||
- Ask for PR number or URL.
|
||||
- If missing, always ask. Never auto-detect from conversation.
|
||||
- If ambiguous, ask.
|
||||
|
||||
## Safety
|
||||
|
||||
- Never push to `main` or `origin/main`, not during review, not ever.
|
||||
- Do not run `git push` at all during review. Treat review as read only.
|
||||
- Do not stop or kill the gateway. Do not run gateway stop commands. Do not kill processes on port 18792.
|
||||
|
||||
## Execution Rule
|
||||
|
||||
- Execute the workflow. Do not stop after printing the TODO checklist.
|
||||
- If delegating, require the delegate to run commands and capture outputs, not a plan.
|
||||
|
||||
## Known Failure Modes
|
||||
|
||||
- If you see "fatal: not a git repository", you are in the wrong directory. Use `~/dev/openclaw` if available; otherwise ask user.
|
||||
- Do not stop after printing the checklist. That is not completion.
|
||||
|
||||
## Writing Style for Output
|
||||
|
||||
- Write casual and direct.
|
||||
- Avoid em dashes and en dashes. Use commas or separate sentences.
|
||||
|
||||
## Completion Criteria
|
||||
|
||||
- Run the commands in the worktree and inspect the PR directly.
|
||||
- Produce the structured review sections A through J.
|
||||
- Save the full review to `.local/review.md` inside the worktree.
|
||||
|
||||
## First: Create a TODO Checklist
|
||||
|
||||
Create a checklist of all review steps, print it, then continue and execute the commands.
|
||||
|
||||
## Setup: Use a Worktree
|
||||
|
||||
Use an isolated worktree for all review work.
|
||||
|
||||
```sh
|
||||
cd ~/dev/openclaw
|
||||
# Sanity: confirm you are in the repo
|
||||
git rev-parse --show-toplevel
|
||||
|
||||
WORKTREE_DIR=".worktrees/pr-<PR>"
|
||||
git fetch origin main
|
||||
|
||||
# Reuse existing worktree if it exists, otherwise create new
|
||||
if [ -d "$WORKTREE_DIR" ]; then
|
||||
cd "$WORKTREE_DIR"
|
||||
git checkout temp/pr-<PR> 2>/dev/null || git checkout -b temp/pr-<PR>
|
||||
git fetch origin main
|
||||
git reset --hard origin/main
|
||||
else
|
||||
git worktree add "$WORKTREE_DIR" -b temp/pr-<PR> origin/main
|
||||
cd "$WORKTREE_DIR"
|
||||
fi
|
||||
|
||||
# Create local scratch space that persists across /reviewpr to /preparepr to /mergepr
|
||||
mkdir -p .local
|
||||
```
|
||||
|
||||
Run all commands inside the worktree directory.
|
||||
Start on `origin/main` so you can check for existing implementations before looking at PR code.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Identify PR meta and context
|
||||
|
||||
```sh
|
||||
gh pr view <PR> --json number,title,state,isDraft,author,baseRefName,headRefName,headRepository,url,body,labels,assignees,reviewRequests,files,additions,deletions --jq '{number,title,url,state,isDraft,author:.author.login,base:.baseRefName,head:.headRefName,headRepo:.headRepository.nameWithOwner,additions,deletions,files:.files|length,body}'
|
||||
```
|
||||
|
||||
2. Check if this already exists in main before looking at the PR branch
|
||||
|
||||
- Identify the core feature or fix from the PR title and description.
|
||||
- Search for existing implementations using keywords from the PR title, changed file paths, and function or component names from the diff.
|
||||
|
||||
```sh
|
||||
# Use keywords from the PR title and changed files
|
||||
rg -n "<keyword_from_pr_title>" -S src packages apps ui || true
|
||||
rg -n "<function_or_component_name>" -S src packages apps ui || true
|
||||
|
||||
git log --oneline --all --grep="<keyword_from_pr_title>" | head -20
|
||||
```
|
||||
|
||||
If it already exists, call it out as a BLOCKER or at least IMPORTANT.
|
||||
|
||||
3. Claim the PR
|
||||
|
||||
Assign yourself so others know someone is reviewing. Skip if the PR looks like spam or is a draft you plan to recommend closing.
|
||||
|
||||
```sh
|
||||
gh_user=$(gh api user --jq .login)
|
||||
gh pr edit <PR> --add-assignee "$gh_user"
|
||||
```
|
||||
|
||||
4. Read the PR description carefully
|
||||
|
||||
Use the body from step 1. Summarize goal, scope, and missing context.
|
||||
|
||||
5. Read the diff thoroughly
|
||||
|
||||
Minimum:
|
||||
|
||||
```sh
|
||||
gh pr diff <PR>
|
||||
```
|
||||
|
||||
If you need full code context locally, fetch the PR head to a local ref and diff it. Do not create a merge commit.
|
||||
|
||||
```sh
|
||||
git fetch origin pull/<PR>/head:pr-<PR>
|
||||
# Show changes without modifying the working tree
|
||||
|
||||
git diff --stat origin/main..pr-<PR>
|
||||
git diff origin/main..pr-<PR>
|
||||
```
|
||||
|
||||
If you want to browse the PR version of files directly, temporarily check out `pr-<PR>` in the worktree. Do not commit or push. Return to `temp/pr-<PR>` and reset to `origin/main` afterward.
|
||||
|
||||
```sh
|
||||
# Use only if needed
|
||||
# git checkout pr-<PR>
|
||||
# ...inspect files...
|
||||
|
||||
git checkout temp/pr-<PR>
|
||||
git reset --hard origin/main
|
||||
```
|
||||
|
||||
6. Validate the change is needed and valuable
|
||||
|
||||
Be honest. Call out low value AI slop.
|
||||
|
||||
7. Evaluate implementation quality
|
||||
|
||||
Review correctness, design, performance, and ergonomics.
|
||||
|
||||
8. Perform a security review
|
||||
|
||||
Assume OpenClaw subagents run with full disk access, including git, gh, and shell. Check auth, input validation, secrets, dependencies, tool safety, and privacy.
|
||||
|
||||
9. Review tests and verification
|
||||
|
||||
Identify what exists, what is missing, and what would be a minimal regression test.
|
||||
|
||||
10. Check docs
|
||||
|
||||
Check if the PR touches code with related documentation such as README, docs, inline API docs, or config examples.
|
||||
|
||||
- If docs exist for the changed area and the PR does not update them, flag as IMPORTANT.
|
||||
- If the PR adds a new feature or config option with no docs, flag as IMPORTANT.
|
||||
- If the change is purely internal with no user-facing impact, skip this.
|
||||
|
||||
11. Check changelog
|
||||
|
||||
Check if `CHANGELOG.md` exists and whether the PR warrants an entry.
|
||||
|
||||
- If the project has a changelog and the PR is user-facing, flag missing entry as IMPORTANT.
|
||||
- Leave the change for /preparepr, only flag it here.
|
||||
|
||||
12. Answer the key question
|
||||
|
||||
Decide if /preparepr can fix issues or the contributor must update the PR.
|
||||
|
||||
13. Save findings to the worktree
|
||||
|
||||
Write the full structured review sections A through J to `.local/review.md`.
|
||||
Create or overwrite the file and verify it exists and is non-empty.
|
||||
|
||||
```sh
|
||||
ls -la .local/review.md
|
||||
wc -l .local/review.md
|
||||
```
|
||||
|
||||
14. Output the structured review
|
||||
|
||||
Produce a review that matches what you saved to `.local/review.md`.
|
||||
|
||||
A) TL;DR recommendation
|
||||
|
||||
- One of: READY FOR /preparepr | NEEDS WORK | NEEDS DISCUSSION | NOT USEFUL (CLOSE)
|
||||
- 1 to 3 sentences.
|
||||
|
||||
B) What changed
|
||||
|
||||
C) What is good
|
||||
|
||||
D) Security findings
|
||||
|
||||
E) Concerns or questions (actionable)
|
||||
|
||||
- Numbered list.
|
||||
- Mark each item as BLOCKER, IMPORTANT, or NIT.
|
||||
- For each, point to file or area and propose a concrete fix.
|
||||
|
||||
F) Tests
|
||||
|
||||
G) Docs status
|
||||
|
||||
- State if related docs are up to date, missing, or not applicable.
|
||||
|
||||
H) Changelog
|
||||
|
||||
- State if `CHANGELOG.md` needs an entry and which category.
|
||||
|
||||
I) Follow ups (optional)
|
||||
|
||||
J) Suggested PR comment (optional)
|
||||
|
||||
## Guardrails
|
||||
|
||||
- Worktree only.
|
||||
- Do not delete the worktree after review.
|
||||
- Review only, do not merge, do not push.
|
||||
4
.agents/skills/review-pr/agents/openai.yaml
Normal file
4
.agents/skills/review-pr/agents/openai.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Review PR"
|
||||
short_description: "Review GitHub PRs without merging"
|
||||
default_prompt: "Use $review-pr to perform a thorough, review-only GitHub PR review."
|
||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1 +1 @@
|
||||
custom: ['https://github.com/sponsors/steipete']
|
||||
custom: ["https://github.com/sponsors/steipete"]
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -6,23 +6,29 @@ labels: bug
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
What went wrong?
|
||||
|
||||
## Steps to reproduce
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
## Expected behavior
|
||||
|
||||
What did you expect to happen?
|
||||
|
||||
## Actual behavior
|
||||
|
||||
What actually happened?
|
||||
|
||||
## Environment
|
||||
|
||||
- Clawdbot version:
|
||||
- OS:
|
||||
- Install method (pnpm/npx/docker/etc):
|
||||
|
||||
## Logs or screenshots
|
||||
|
||||
Paste relevant logs or add screenshots (redact secrets).
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -2,7 +2,7 @@ blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Onboarding
|
||||
url: https://discord.gg/clawd
|
||||
about: New to Clawdbot? Join Discord for setup guidance from Krill in #help.
|
||||
about: New to Clawdbot? Join Discord for setup guidance from Krill in \#help.
|
||||
- name: Support
|
||||
url: https://discord.gg/clawd
|
||||
about: Get help from Krill and the community on Discord in #help.
|
||||
about: Get help from Krill and the community on Discord in \#help.
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -6,13 +6,17 @@ labels: enhancement
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Describe the problem you are trying to solve or the opportunity you see.
|
||||
|
||||
## Proposed solution
|
||||
|
||||
What would you like Clawdbot to do?
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
Any other approaches you have considered?
|
||||
|
||||
## Additional context
|
||||
|
||||
Links, screenshots, or related issues.
|
||||
|
||||
2
.github/actionlint.yaml
vendored
2
.github/actionlint.yaml
vendored
@@ -12,6 +12,6 @@ paths:
|
||||
.github/workflows/**/*.yml:
|
||||
ignore:
|
||||
# Ignore shellcheck warnings (we run shellcheck separately)
|
||||
- 'shellcheck reported issue.+'
|
||||
- "shellcheck reported issue.+"
|
||||
# Ignore intentional if: false for disabled jobs
|
||||
- 'constant expression "false" in condition'
|
||||
|
||||
41
.github/actions/detect-docs-only/action.yml
vendored
Normal file
41
.github/actions/detect-docs-only/action.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: Detect docs-only changes
|
||||
description: >
|
||||
Outputs docs_only=true when all changed files are under docs/ or are
|
||||
markdown (.md/.mdx). Fail-safe: if detection fails, outputs false (run
|
||||
everything). Uses git diff — no API calls, no extra permissions needed.
|
||||
|
||||
outputs:
|
||||
docs_only:
|
||||
description: "'true' if all changes are docs/markdown, 'false' otherwise"
|
||||
value: ${{ steps.check.outputs.docs_only }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Detect docs-only changes
|
||||
id: check
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "push" ]; then
|
||||
BASE="${{ github.event.before }}"
|
||||
else
|
||||
# Use the exact base SHA from the event payload — stable regardless
|
||||
# of base branch movement (avoids origin/<ref> drift).
|
||||
BASE="${{ github.event.pull_request.base.sha }}"
|
||||
fi
|
||||
|
||||
# Fail-safe: if we can't diff, assume non-docs (run everything)
|
||||
CHANGED=$(git diff --name-only "$BASE" HEAD 2>/dev/null || echo "UNKNOWN")
|
||||
if [ "$CHANGED" = "UNKNOWN" ] || [ -z "$CHANGED" ]; then
|
||||
echo "docs_only=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if all changed files are docs or markdown
|
||||
NON_DOCS=$(echo "$CHANGED" | grep -vE '^docs/|\.md$|\.mdx$' || true)
|
||||
if [ -z "$NON_DOCS" ]; then
|
||||
echo "docs_only=true" >> "$GITHUB_OUTPUT"
|
||||
echo "Docs-only change detected — skipping heavy jobs"
|
||||
else
|
||||
echo "docs_only=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
6
.github/labeler.yml
vendored
6
.github/labeler.yml
vendored
@@ -9,6 +9,12 @@
|
||||
- "src/discord/**"
|
||||
- "extensions/discord/**"
|
||||
- "docs/channels/discord.md"
|
||||
"channel: feishu":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "src/feishu/**"
|
||||
- "extensions/feishu/**"
|
||||
- "docs/channels/feishu.md"
|
||||
"channel: googlechat":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
|
||||
55
.github/workflows/auto-response.yml
vendored
55
.github/workflows/auto-response.yml
vendored
@@ -2,25 +2,26 @@ name: Auto response
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
types: [opened, edited, labeled]
|
||||
pull_request_target:
|
||||
types: [labeled]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
auto-response:
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/create-github-app-token@v1
|
||||
- uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: "2729701"
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- name: Handle labeled items
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
@@ -30,22 +31,49 @@ jobs:
|
||||
label: "r: skill",
|
||||
close: true,
|
||||
message:
|
||||
"Thanks for the contribution! New skills should be published to Clawdhub for everyone to use. We’re keeping the core lean on skills, so I’m closing this out.",
|
||||
"Thanks for the contribution! New skills should be published to [Clawhub](https://clawhub.ai) for everyone to use. We’re keeping the core lean on skills, so I’m closing this out.",
|
||||
},
|
||||
{
|
||||
label: "r: support",
|
||||
close: true,
|
||||
message:
|
||||
"Please use our support server https://molt.bot/discord and ask in #help or #users-helping-users to resolve this, or follow the stuck FAQ at https://docs.molt.bot/help/faq#im-stuck-whats-the-fastest-way-to-get-unstuck.",
|
||||
"Please use [our support server](https://discord.gg/clawd) and ask in #help or #users-helping-users to resolve this, or follow the stuck FAQ at https://docs.openclaw.ai/help/faq#im-stuck-whats-the-fastest-way-to-get-unstuck.",
|
||||
},
|
||||
{
|
||||
label: "r: third-party-extension",
|
||||
close: true,
|
||||
message:
|
||||
"This would be better made as a third-party extension with our SDK that you maintain yourself. Docs: https://docs.molt.bot/plugin.",
|
||||
"This would be better made as a third-party extension with our SDK that you maintain yourself. Docs: https://docs.openclaw.ai/plugin.",
|
||||
},
|
||||
{
|
||||
label: "r: moltbook",
|
||||
close: true,
|
||||
lock: true,
|
||||
lockReason: "off-topic",
|
||||
message:
|
||||
"OpenClaw is not affiliated with Moltbook, and issues related to Moltbook should not be submitted here.",
|
||||
},
|
||||
];
|
||||
|
||||
const issue = context.payload.issue;
|
||||
if (issue) {
|
||||
const title = issue.title ?? "";
|
||||
const body = issue.body ?? "";
|
||||
const haystack = `${title}\n${body}`.toLowerCase();
|
||||
const hasLabel = (issue.labels ?? []).some((label) =>
|
||||
typeof label === "string" ? label === "r: moltbook" : label?.name === "r: moltbook",
|
||||
);
|
||||
if (haystack.includes("moltbook") && !hasLabel) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
labels: ["r: moltbook"],
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const labelName = context.payload.label?.name;
|
||||
if (!labelName) {
|
||||
return;
|
||||
@@ -76,3 +104,12 @@ jobs:
|
||||
state: "closed",
|
||||
});
|
||||
}
|
||||
|
||||
if (rule.lock) {
|
||||
await github.rest.issues.lock({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
lock_reason: rule.lockReason ?? "resolved",
|
||||
});
|
||||
}
|
||||
|
||||
259
.github/workflows/ci.yml
vendored
259
.github/workflows/ci.yml
vendored
@@ -2,10 +2,34 @@ name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.event.pull_request.number || github.sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# Detect docs-only changes to skip heavy jobs (test, build, Windows, macOS, Android).
|
||||
# Lint and format always run. Fail-safe: if detection fails, run everything.
|
||||
docs-scope:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
docs_only: ${{ steps.check.outputs.docs_only }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: false
|
||||
|
||||
- name: Detect docs-only changes
|
||||
id: check
|
||||
uses: ./.github/actions/detect-docs-only
|
||||
|
||||
install-check:
|
||||
needs: [docs-scope]
|
||||
if: needs.docs-scope.outputs.docs_only != 'true'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -66,32 +90,25 @@ jobs:
|
||||
pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true
|
||||
|
||||
checks:
|
||||
needs: [docs-scope]
|
||||
if: needs.docs-scope.outputs.docs_only != 'true'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runtime: node
|
||||
task: lint
|
||||
command: pnpm lint
|
||||
task: tsgo
|
||||
command: pnpm tsgo
|
||||
- runtime: node
|
||||
task: test
|
||||
command: pnpm canvas:a2ui:bundle && pnpm test
|
||||
- runtime: node
|
||||
task: build
|
||||
command: pnpm build
|
||||
- runtime: node
|
||||
task: protocol
|
||||
command: pnpm protocol:check
|
||||
- runtime: node
|
||||
task: format
|
||||
command: pnpm format
|
||||
- runtime: bun
|
||||
task: test
|
||||
command: pnpm canvas:a2ui:bundle && bunx vitest run
|
||||
- runtime: bun
|
||||
task: build
|
||||
command: bunx tsc -p tsconfig.json
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -159,6 +176,84 @@ jobs:
|
||||
- name: Run ${{ matrix.task }} (${{ matrix.runtime }})
|
||||
run: ${{ matrix.command }}
|
||||
|
||||
# Lint and format always run, even on docs-only changes.
|
||||
checks-lint:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- task: lint
|
||||
command: pnpm build && pnpm lint
|
||||
- task: format
|
||||
command: pnpm format
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: false
|
||||
|
||||
- name: Checkout submodules (retry)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git submodule sync --recursive
|
||||
for attempt in 1 2 3 4 5; do
|
||||
if git -c protocol.version=2 submodule update --init --force --depth=1 --recursive; then
|
||||
exit 0
|
||||
fi
|
||||
echo "Submodule update failed (attempt $attempt/5). Retrying…"
|
||||
sleep $((attempt * 10))
|
||||
done
|
||||
exit 1
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22.x
|
||||
check-latest: true
|
||||
|
||||
- name: Setup pnpm (corepack retry)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
corepack enable
|
||||
for attempt in 1 2 3; do
|
||||
if corepack prepare pnpm@10.23.0 --activate; then
|
||||
pnpm -v
|
||||
exit 0
|
||||
fi
|
||||
echo "corepack prepare failed (attempt $attempt/3). Retrying..."
|
||||
sleep $((attempt * 10))
|
||||
done
|
||||
exit 1
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Runtime versions
|
||||
run: |
|
||||
node -v
|
||||
npm -v
|
||||
bun -v
|
||||
pnpm -v
|
||||
|
||||
- name: Capture node path
|
||||
run: echo "NODE_BIN=$(dirname \"$(node -p \"process.execPath\")\")" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install dependencies
|
||||
env:
|
||||
CI: true
|
||||
run: |
|
||||
export PATH="$NODE_BIN:$PATH"
|
||||
which node
|
||||
node -v
|
||||
pnpm -v
|
||||
pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true || pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true
|
||||
|
||||
- name: Run ${{ matrix.task }}
|
||||
run: ${{ matrix.command }}
|
||||
|
||||
secrets:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
@@ -185,10 +280,14 @@ jobs:
|
||||
fi
|
||||
|
||||
checks-windows:
|
||||
needs: [docs-scope]
|
||||
if: needs.docs-scope.outputs.docs_only != 'true'
|
||||
runs-on: blacksmith-4vcpu-windows-2025
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
CLAWDBOT_TEST_WORKERS: 1
|
||||
# Keep total concurrency predictable on the 4 vCPU runner:
|
||||
# `scripts/test-parallel.mjs` runs some vitest suites in parallel processes.
|
||||
OPENCLAW_TEST_WORKERS: 2
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
@@ -197,14 +296,11 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- runtime: node
|
||||
task: lint
|
||||
command: pnpm lint
|
||||
task: build & lint
|
||||
command: pnpm build && pnpm lint
|
||||
- runtime: node
|
||||
task: test
|
||||
command: pnpm canvas:a2ui:bundle && pnpm test
|
||||
- runtime: node
|
||||
task: build
|
||||
command: pnpm build
|
||||
- runtime: node
|
||||
task: protocol
|
||||
command: pnpm protocol:check
|
||||
@@ -214,6 +310,25 @@ jobs:
|
||||
with:
|
||||
submodules: false
|
||||
|
||||
- name: Try to exclude workspace from Windows Defender (best-effort)
|
||||
shell: pwsh
|
||||
run: |
|
||||
$cmd = Get-Command Add-MpPreference -ErrorAction SilentlyContinue
|
||||
if (-not $cmd) {
|
||||
Write-Host "Add-MpPreference not available, skipping Defender exclusions."
|
||||
exit 0
|
||||
}
|
||||
|
||||
try {
|
||||
# Defender sometimes intercepts process spawning (vitest workers). If this fails
|
||||
# (eg hardened images), keep going and rely on worker limiting above.
|
||||
Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE" -ErrorAction Stop
|
||||
Add-MpPreference -ExclusionProcess "node.exe" -ErrorAction Stop
|
||||
Write-Host "Defender exclusions applied."
|
||||
} catch {
|
||||
Write-Warning "Failed to apply Defender exclusions, continuing. $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
- name: Checkout submodules (retry)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -275,15 +390,14 @@ jobs:
|
||||
- name: Run ${{ matrix.task }} (${{ matrix.runtime }})
|
||||
run: ${{ matrix.command }}
|
||||
|
||||
checks-macos:
|
||||
if: github.event_name == 'pull_request'
|
||||
# Consolidated macOS job: runs TS tests + Swift lint/build/test sequentially
|
||||
# on a single runner. GitHub limits macOS concurrent jobs to 5 per org;
|
||||
# running 4 separate jobs per PR (as before) starved the queue. One job
|
||||
# per PR allows 5 PRs to run macOS checks simultaneously.
|
||||
macos:
|
||||
needs: [docs-scope]
|
||||
if: github.event_name == 'pull_request' && needs.docs-scope.outputs.docs_only != 'true'
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- task: test
|
||||
command: pnpm test
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -303,6 +417,7 @@ jobs:
|
||||
done
|
||||
exit 1
|
||||
|
||||
# --- Node/pnpm setup (for TS tests) ---
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -342,71 +457,20 @@ jobs:
|
||||
pnpm -v
|
||||
pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true || pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true
|
||||
|
||||
- name: Run ${{ matrix.task }}
|
||||
# --- Run all checks sequentially (fast gates first) ---
|
||||
- name: TS tests (macOS)
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
run: ${{ matrix.command }}
|
||||
|
||||
macos-app:
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- task: lint
|
||||
command: |
|
||||
swiftlint --config .swiftlint.yml
|
||||
swiftformat --lint apps/macos/Sources --config .swiftformat
|
||||
- task: build
|
||||
command: |
|
||||
set -euo pipefail
|
||||
for attempt in 1 2 3; do
|
||||
if swift build --package-path apps/macos --configuration release; then
|
||||
exit 0
|
||||
fi
|
||||
echo "swift build failed (attempt $attempt/3). Retrying…"
|
||||
sleep $((attempt * 20))
|
||||
done
|
||||
exit 1
|
||||
- task: test
|
||||
command: |
|
||||
set -euo pipefail
|
||||
for attempt in 1 2 3; do
|
||||
if swift test --package-path apps/macos --parallel --enable-code-coverage --show-codecov-path; then
|
||||
exit 0
|
||||
fi
|
||||
echo "swift test failed (attempt $attempt/3). Retrying…"
|
||||
sleep $((attempt * 20))
|
||||
done
|
||||
exit 1
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: false
|
||||
|
||||
- name: Checkout submodules (retry)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git submodule sync --recursive
|
||||
for attempt in 1 2 3 4 5; do
|
||||
if git -c protocol.version=2 submodule update --init --force --depth=1 --recursive; then
|
||||
exit 0
|
||||
fi
|
||||
echo "Submodule update failed (attempt $attempt/5). Retrying…"
|
||||
sleep $((attempt * 10))
|
||||
done
|
||||
exit 1
|
||||
run: pnpm test
|
||||
|
||||
# --- Xcode/Swift setup ---
|
||||
- name: Select Xcode 26.1
|
||||
run: |
|
||||
sudo xcode-select -s /Applications/Xcode_26.1.app
|
||||
xcodebuild -version
|
||||
|
||||
- name: Install XcodeGen / SwiftLint / SwiftFormat
|
||||
run: |
|
||||
brew install xcodegen swiftlint swiftformat
|
||||
run: brew install xcodegen swiftlint swiftformat
|
||||
|
||||
- name: Show toolchain
|
||||
run: |
|
||||
@@ -414,8 +478,35 @@ jobs:
|
||||
xcodebuild -version
|
||||
swift --version
|
||||
|
||||
- name: Run ${{ matrix.task }}
|
||||
run: ${{ matrix.command }}
|
||||
- name: Swift lint
|
||||
run: |
|
||||
swiftlint --config .swiftlint.yml
|
||||
swiftformat --lint apps/macos/Sources --config .swiftformat
|
||||
|
||||
- name: Swift build (release)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for attempt in 1 2 3; do
|
||||
if swift build --package-path apps/macos --configuration release; then
|
||||
exit 0
|
||||
fi
|
||||
echo "swift build failed (attempt $attempt/3). Retrying…"
|
||||
sleep $((attempt * 20))
|
||||
done
|
||||
exit 1
|
||||
|
||||
- name: Swift test
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for attempt in 1 2 3; do
|
||||
if swift test --package-path apps/macos --parallel --enable-code-coverage --show-codecov-path; then
|
||||
exit 0
|
||||
fi
|
||||
echo "swift test failed (attempt $attempt/3). Retrying…"
|
||||
sleep $((attempt * 20))
|
||||
done
|
||||
exit 1
|
||||
|
||||
ios:
|
||||
if: false # ignore iOS in CI for now
|
||||
runs-on: macos-latest
|
||||
@@ -590,6 +681,8 @@ jobs:
|
||||
PY
|
||||
|
||||
android:
|
||||
needs: [docs-scope]
|
||||
if: needs.docs-scope.outputs.docs_only != 'true'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
138
.github/workflows/formal-conformance.yml
vendored
Normal file
138
.github/workflows/formal-conformance.yml
vendored
Normal file
@@ -0,0 +1,138 @@
|
||||
name: Formal models (informational conformance)
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: formal-conformance-${{ github.event.pull_request.number || github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
formal_conformance:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Checkout openclaw (PR)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: openclaw
|
||||
|
||||
- name: Checkout formal models
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: vignesh07/clawdbot-formal-models
|
||||
ref: main
|
||||
path: clawdbot-formal-models
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- name: Regenerate extracted constants from openclaw
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd clawdbot-formal-models
|
||||
export OPENCLAW_REPO_DIR="${GITHUB_WORKSPACE}/openclaw"
|
||||
node scripts/extract-tool-groups.mjs
|
||||
node scripts/check-tool-group-alias.mjs
|
||||
|
||||
# Drift is about extracted artifacts only; compute it before model checking
|
||||
# to avoid any incidental file touches affecting the result.
|
||||
- name: Compute drift (generated/*)
|
||||
id: drift
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd clawdbot-formal-models
|
||||
|
||||
if git diff --quiet -- generated; then
|
||||
echo "drift=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "drift=true" >> "$GITHUB_OUTPUT"
|
||||
git diff -- generated > "${GITHUB_WORKSPACE}/formal-models-drift.diff"
|
||||
|
||||
- name: Model check (green suite)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd clawdbot-formal-models
|
||||
make \
|
||||
precedence groups elevated nodes-policy \
|
||||
attacker approvals approvals-token nodes-pipeline \
|
||||
gateway-exposure gateway-exposure-v2 gateway-exposure-v2-protected \
|
||||
gateway-auth-conformance gateway-auth-tailscale gateway-auth-proxy \
|
||||
pairing pairing-cap pairing-idempotency pairing-refresh pairing-refresh-race \
|
||||
ingress-gating ingress-idempotency ingress-dedupe-fallback ingress-trace ingress-trace2 \
|
||||
routing-isolation routing-precedence routing-identitylinks routing-identity-transitive routing-identity-symmetry routing-identity-channel-override \
|
||||
routing-thread-parent discord-pluralkit \
|
||||
ingress-retry session-key-stability session-explosion-bound config-normalization \
|
||||
queue-drain delivery-route-stability delivery-pipeline retry-termination retry-eventual-success \
|
||||
no-cross-stream multi-event-eventual-emission \
|
||||
dedupe-collision-fallback crash-restart-dedupe two-worker-dedupe openclaw-session-key-conformance \
|
||||
routing-thread-parent-channel-override routing-trirule gateway-auth-proxy-header-spoof \
|
||||
group-alias-check
|
||||
|
||||
- name: Model check (negative suite, expected violations)
|
||||
continue-on-error: true
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd clawdbot-formal-models
|
||||
make -k \
|
||||
precedence-negative groups-negative elevated-negative nodes-policy-negative \
|
||||
attacker-negative attacker-nodes-negative attacker-nodes-allowlist-negative attacker-nodes-allowlist-negative \
|
||||
approvals-negative approvals-token-negative nodes-pipeline-negative \
|
||||
gateway-exposure-negative gateway-exposure-v2-negative gateway-exposure-v2-protected-negative \
|
||||
gateway-exposure-v2-unsafe-custom gateway-exposure-v2-unsafe-tailnet gateway-exposure-v2-unsafe-auto \
|
||||
gateway-auth-conformance-negative gateway-auth-tailscale-negative gateway-auth-proxy-negative \
|
||||
pairing-negative pairing-cap-negative pairing-idempotency-negative pairing-refresh-negative pairing-refresh-race-negative \
|
||||
ingress-gating-negative ingress-idempotency-negative ingress-dedupe-fallback-negative ingress-trace-negative ingress-trace2-negative \
|
||||
routing-isolation-negative routing-precedence-negative routing-identitylinks-negative routing-identity-transitive-negative routing-identity-symmetry-negative routing-identity-channel-override-negative \
|
||||
routing-thread-parent-negative discord-pluralkit-negative \
|
||||
ingress-retry-negative session-key-stability-negative config-normalization-negative \
|
||||
queue-drain delivery-route-stability-negative delivery-pipeline-negative retry-termination-negative retry-eventual-success-negative \
|
||||
no-cross-stream-negative multi-event-eventual-emission-negative \
|
||||
dedupe-collision-fallback-negative crash-restart-dedupe-negative two-worker-dedupe-negative openclaw-session-key-conformance-negative \
|
||||
routing-thread-parent-channel-override-negative routing-trirule-negative gateway-auth-proxy-header-spoof-negative
|
||||
|
||||
- name: Upload drift diff artifact
|
||||
if: steps.drift.outputs.drift == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: formal-models-conformance-drift
|
||||
path: formal-models-drift.diff
|
||||
|
||||
- name: Comment on PR (informational)
|
||||
if: steps.drift.outputs.drift == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const body = [
|
||||
'⚠️ **Formal models conformance drift detected**',
|
||||
'',
|
||||
'The formal models extracted constants (`generated/*`) do not match this openclaw PR.',
|
||||
'',
|
||||
'This check is **informational** (not blocking merges yet).',
|
||||
'See the `formal-models-conformance-drift` artifact for the diff.',
|
||||
'',
|
||||
'If this change is intentional, follow up by updating the formal models repo or regenerating the extracted artifacts there.',
|
||||
].join('\n');
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.pull_request.number,
|
||||
body,
|
||||
});
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
if [ "${{ steps.drift.outputs.drift }}" = "true" ]; then
|
||||
echo "Formal conformance drift detected (informational)."
|
||||
else
|
||||
echo "Formal conformance: no drift."
|
||||
fi
|
||||
20
.github/workflows/install-smoke.yml
vendored
20
.github/workflows/install-smoke.yml
vendored
@@ -6,8 +6,28 @@ on:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: install-smoke-${{ github.event.pull_request.number || github.sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
docs-scope:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
docs_only: ${{ steps.check.outputs.docs_only }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Detect docs-only changes
|
||||
id: check
|
||||
uses: ./.github/actions/detect-docs-only
|
||||
|
||||
install-smoke:
|
||||
needs: [docs-scope]
|
||||
if: needs.docs-scope.outputs.docs_only != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout CLI
|
||||
|
||||
66
.github/workflows/labeler.yml
vendored
66
.github/workflows/labeler.yml
vendored
@@ -3,22 +3,78 @@ name: Labeler
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
label:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/create-github-app-token@v1
|
||||
- uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: "2729701"
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- uses: actions/labeler@v5
|
||||
- uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5
|
||||
with:
|
||||
configuration-path: .github/labeler.yml
|
||||
repo-token: ${{ steps.app-token.outputs.token }}
|
||||
sync-labels: true
|
||||
- name: Apply maintainer label for org members
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
const association = context.payload.pull_request?.author_association;
|
||||
if (!association) {
|
||||
return;
|
||||
}
|
||||
if (![
|
||||
"MEMBER",
|
||||
"OWNER",
|
||||
].includes(association)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await github.rest.issues.addLabels({
|
||||
...context.repo,
|
||||
issue_number: context.payload.pull_request.number,
|
||||
labels: ["maintainer"],
|
||||
});
|
||||
|
||||
label-issues:
|
||||
permissions:
|
||||
issues: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: "2729701"
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- name: Apply maintainer label for org members
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
const association = context.payload.issue?.author_association;
|
||||
if (!association) {
|
||||
return;
|
||||
}
|
||||
if (![
|
||||
"MEMBER",
|
||||
"OWNER",
|
||||
].includes(association)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await github.rest.issues.addLabels({
|
||||
...context.repo,
|
||||
issue_number: context.payload.issue.number,
|
||||
labels: ["maintainer"],
|
||||
});
|
||||
|
||||
5
.github/workflows/workflow-sanity.yml
vendored
5
.github/workflows/workflow-sanity.yml
vendored
@@ -3,6 +3,11 @@ name: Workflow Sanity
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: workflow-sanity-${{ github.event.pull_request.number || github.sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
no-tabs:
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -64,10 +64,15 @@ apps/ios/*.mobileprovision
|
||||
|
||||
# Local untracked files
|
||||
.local/
|
||||
.vscode/
|
||||
IDENTITY.md
|
||||
USER.md
|
||||
.tgz
|
||||
|
||||
# local tooling
|
||||
.serena/
|
||||
|
||||
# Agent credentials and memory (NEVER COMMIT)
|
||||
memory/
|
||||
.agent/*.json
|
||||
!.agent/workflows/
|
||||
local/
|
||||
|
||||
52
.markdownlint-cli2.jsonc
Normal file
52
.markdownlint-cli2.jsonc
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"globs": ["docs/**/*.md", "docs/**/*.mdx", "README.md"],
|
||||
"ignores": ["docs/zh-CN/**", "docs/.i18n/**", "docs/reference/templates/**"],
|
||||
"config": {
|
||||
"default": true,
|
||||
|
||||
"MD013": false,
|
||||
"MD025": false,
|
||||
"MD029": false,
|
||||
|
||||
"MD033": {
|
||||
"allowed_elements": [
|
||||
"Note",
|
||||
"Info",
|
||||
"Tip",
|
||||
"Warning",
|
||||
"Card",
|
||||
"CardGroup",
|
||||
"Columns",
|
||||
"Steps",
|
||||
"Step",
|
||||
"Tabs",
|
||||
"Tab",
|
||||
"Accordion",
|
||||
"AccordionGroup",
|
||||
"CodeGroup",
|
||||
"Frame",
|
||||
"Callout",
|
||||
"ParamField",
|
||||
"ResponseField",
|
||||
"RequestExample",
|
||||
"ResponseExample",
|
||||
"img",
|
||||
"a",
|
||||
"br",
|
||||
"details",
|
||||
"summary",
|
||||
"p",
|
||||
"strong",
|
||||
"picture",
|
||||
"source",
|
||||
"Tooltip",
|
||||
"Check",
|
||||
],
|
||||
},
|
||||
|
||||
"MD036": false,
|
||||
"MD040": false,
|
||||
"MD041": false,
|
||||
"MD046": false,
|
||||
},
|
||||
}
|
||||
@@ -1,5 +1,20 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||
"indentWidth": 2,
|
||||
"printWidth": 100
|
||||
"experimentalSortImports": {
|
||||
"newlinesBetween": false,
|
||||
},
|
||||
"experimentalSortPackageJson": {
|
||||
"sortScripts": true,
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"apps/",
|
||||
"assets/",
|
||||
"dist/",
|
||||
"docs/_layouts/",
|
||||
"node_modules/",
|
||||
"patches/",
|
||||
"pnpm-lock.yaml/",
|
||||
"Swabble/",
|
||||
"vendor/",
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,12 +1,36 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": [
|
||||
"unicorn",
|
||||
"typescript",
|
||||
"oxc"
|
||||
],
|
||||
"plugins": ["unicorn", "typescript", "oxc"],
|
||||
"categories": {
|
||||
"correctness": "error"
|
||||
"correctness": "error",
|
||||
"perf": "error",
|
||||
"suspicious": "error"
|
||||
},
|
||||
"ignorePatterns": ["src/canvas-host/a2ui/a2ui.bundle.js"]
|
||||
"rules": {
|
||||
"curly": "error",
|
||||
"eslint-plugin-unicorn/prefer-array-find": "off",
|
||||
"eslint/no-await-in-loop": "off",
|
||||
"eslint/no-new": "off",
|
||||
"oxc/no-accumulating-spread": "off",
|
||||
"oxc/no-async-endpoint-handlers": "off",
|
||||
"oxc/no-map-spread": "off",
|
||||
"typescript/no-explicit-any": "error",
|
||||
"typescript/no-extraneous-class": "off",
|
||||
"typescript/no-unsafe-type-assertion": "off",
|
||||
"unicorn/consistent-function-scoping": "off",
|
||||
"unicorn/require-post-message-target-origin": "off"
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"assets/",
|
||||
"dist/",
|
||||
"docs/_layouts/",
|
||||
"extensions/",
|
||||
"node_modules/",
|
||||
"patches/",
|
||||
"pnpm-lock.yaml/",
|
||||
"skills/",
|
||||
"src/canvas-host/a2ui/a2ui.bundle.js",
|
||||
"Swabble/",
|
||||
"vendor/"
|
||||
]
|
||||
}
|
||||
|
||||
195
.pi/extensions/diff.ts
Normal file
195
.pi/extensions/diff.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* Diff Extension
|
||||
*
|
||||
* /diff command shows modified/deleted/new files from git status and opens
|
||||
* the selected file in VS Code's diff view.
|
||||
*/
|
||||
|
||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
||||
import {
|
||||
Container,
|
||||
Key,
|
||||
matchesKey,
|
||||
type SelectItem,
|
||||
SelectList,
|
||||
Text,
|
||||
} from "@mariozechner/pi-tui";
|
||||
|
||||
interface FileInfo {
|
||||
status: string;
|
||||
statusLabel: string;
|
||||
file: string;
|
||||
}
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
pi.registerCommand("diff", {
|
||||
description: "Show git changes and open in VS Code diff view",
|
||||
handler: async (_args, ctx) => {
|
||||
if (!ctx.hasUI) {
|
||||
ctx.ui.notify("No UI available", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get changed files from git status
|
||||
const result = await pi.exec("git", ["status", "--porcelain"], { cwd: ctx.cwd });
|
||||
|
||||
if (result.code !== 0) {
|
||||
ctx.ui.notify(`git status failed: ${result.stderr}`, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.stdout || !result.stdout.trim()) {
|
||||
ctx.ui.notify("No changes in working tree", "info");
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse git status output
|
||||
// Format: XY filename (where XY is two-letter status, then space, then filename)
|
||||
const lines = result.stdout.split("\n");
|
||||
const files: FileInfo[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.length < 4) {
|
||||
continue;
|
||||
} // Need at least "XY f"
|
||||
|
||||
const status = line.slice(0, 2);
|
||||
const file = line.slice(2).trimStart();
|
||||
|
||||
// Translate status codes to short labels
|
||||
let statusLabel: string;
|
||||
if (status.includes("M")) {
|
||||
statusLabel = "M";
|
||||
} else if (status.includes("A")) {
|
||||
statusLabel = "A";
|
||||
} else if (status.includes("D")) {
|
||||
statusLabel = "D";
|
||||
} else if (status.includes("?")) {
|
||||
statusLabel = "?";
|
||||
} else if (status.includes("R")) {
|
||||
statusLabel = "R";
|
||||
} else if (status.includes("C")) {
|
||||
statusLabel = "C";
|
||||
} else {
|
||||
statusLabel = status.trim() || "~";
|
||||
}
|
||||
|
||||
files.push({ status: statusLabel, statusLabel, file });
|
||||
}
|
||||
|
||||
if (files.length === 0) {
|
||||
ctx.ui.notify("No changes found", "info");
|
||||
return;
|
||||
}
|
||||
|
||||
const openSelected = async (fileInfo: FileInfo): Promise<void> => {
|
||||
try {
|
||||
// Open in VS Code diff view.
|
||||
// For untracked files, git difftool won't work, so fall back to just opening the file.
|
||||
if (fileInfo.status === "?") {
|
||||
await pi.exec("code", ["-g", fileInfo.file], { cwd: ctx.cwd });
|
||||
return;
|
||||
}
|
||||
|
||||
const diffResult = await pi.exec(
|
||||
"git",
|
||||
["difftool", "-y", "--tool=vscode", fileInfo.file],
|
||||
{
|
||||
cwd: ctx.cwd,
|
||||
},
|
||||
);
|
||||
if (diffResult.code !== 0) {
|
||||
await pi.exec("code", ["-g", fileInfo.file], { cwd: ctx.cwd });
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
ctx.ui.notify(`Failed to open ${fileInfo.file}: ${message}`, "error");
|
||||
}
|
||||
};
|
||||
|
||||
// Show file picker with SelectList
|
||||
await ctx.ui.custom<void>((tui, theme, _kb, done) => {
|
||||
const container = new Container();
|
||||
|
||||
// Top border
|
||||
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
||||
|
||||
// Title
|
||||
container.addChild(new Text(theme.fg("accent", theme.bold(" Select file to diff")), 0, 0));
|
||||
|
||||
// Build select items with colored status
|
||||
const items: SelectItem[] = files.map((f) => {
|
||||
let statusColor: string;
|
||||
switch (f.status) {
|
||||
case "M":
|
||||
statusColor = theme.fg("warning", f.status);
|
||||
break;
|
||||
case "A":
|
||||
statusColor = theme.fg("success", f.status);
|
||||
break;
|
||||
case "D":
|
||||
statusColor = theme.fg("error", f.status);
|
||||
break;
|
||||
case "?":
|
||||
statusColor = theme.fg("muted", f.status);
|
||||
break;
|
||||
default:
|
||||
statusColor = theme.fg("dim", f.status);
|
||||
}
|
||||
return {
|
||||
value: f,
|
||||
label: `${statusColor} ${f.file}`,
|
||||
};
|
||||
});
|
||||
|
||||
const visibleRows = Math.min(files.length, 15);
|
||||
let currentIndex = 0;
|
||||
|
||||
const selectList = new SelectList(items, visibleRows, {
|
||||
selectedPrefix: (t) => theme.fg("accent", t),
|
||||
selectedText: (t) => t, // Keep existing colors
|
||||
description: (t) => theme.fg("muted", t),
|
||||
scrollInfo: (t) => theme.fg("dim", t),
|
||||
noMatch: (t) => theme.fg("warning", t),
|
||||
});
|
||||
selectList.onSelect = (item) => {
|
||||
void openSelected(item.value as FileInfo);
|
||||
};
|
||||
selectList.onCancel = () => done();
|
||||
selectList.onSelectionChange = (item) => {
|
||||
currentIndex = items.indexOf(item);
|
||||
};
|
||||
container.addChild(selectList);
|
||||
|
||||
// Help text
|
||||
container.addChild(
|
||||
new Text(theme.fg("dim", " ↑↓ navigate • ←→ page • enter open • esc close"), 0, 0),
|
||||
);
|
||||
|
||||
// Bottom border
|
||||
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
||||
|
||||
return {
|
||||
render: (w) => container.render(w),
|
||||
invalidate: () => container.invalidate(),
|
||||
handleInput: (data) => {
|
||||
// Add paging with left/right
|
||||
if (matchesKey(data, Key.left)) {
|
||||
// Page up - clamp to 0
|
||||
currentIndex = Math.max(0, currentIndex - visibleRows);
|
||||
selectList.setSelectedIndex(currentIndex);
|
||||
} else if (matchesKey(data, Key.right)) {
|
||||
// Page down - clamp to last
|
||||
currentIndex = Math.min(items.length - 1, currentIndex + visibleRows);
|
||||
selectList.setSelectedIndex(currentIndex);
|
||||
} else {
|
||||
selectList.handleInput(data);
|
||||
}
|
||||
tui.requestRender();
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
194
.pi/extensions/files.ts
Normal file
194
.pi/extensions/files.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* Files Extension
|
||||
*
|
||||
* /files command lists all files the model has read/written/edited in the active session branch,
|
||||
* coalesced by path and sorted newest first. Selecting a file opens it in VS Code.
|
||||
*/
|
||||
|
||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
||||
import {
|
||||
Container,
|
||||
Key,
|
||||
matchesKey,
|
||||
type SelectItem,
|
||||
SelectList,
|
||||
Text,
|
||||
} from "@mariozechner/pi-tui";
|
||||
|
||||
interface FileEntry {
|
||||
path: string;
|
||||
operations: Set<"read" | "write" | "edit">;
|
||||
lastTimestamp: number;
|
||||
}
|
||||
|
||||
type FileToolName = "read" | "write" | "edit";
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
pi.registerCommand("files", {
|
||||
description: "Show files read/written/edited in this session",
|
||||
handler: async (_args, ctx) => {
|
||||
if (!ctx.hasUI) {
|
||||
ctx.ui.notify("No UI available", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the current branch (path from leaf to root)
|
||||
const branch = ctx.sessionManager.getBranch();
|
||||
|
||||
// First pass: collect tool calls (id -> {path, name}) from assistant messages
|
||||
const toolCalls = new Map<string, { path: string; name: FileToolName; timestamp: number }>();
|
||||
|
||||
for (const entry of branch) {
|
||||
if (entry.type !== "message") {
|
||||
continue;
|
||||
}
|
||||
const msg = entry.message;
|
||||
|
||||
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
||||
for (const block of msg.content) {
|
||||
if (block.type === "toolCall") {
|
||||
const name = block.name;
|
||||
if (name === "read" || name === "write" || name === "edit") {
|
||||
const path = block.arguments?.path;
|
||||
if (path && typeof path === "string") {
|
||||
toolCalls.set(block.id, { path, name, timestamp: msg.timestamp });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: match tool results to get the actual execution timestamp
|
||||
const fileMap = new Map<string, FileEntry>();
|
||||
|
||||
for (const entry of branch) {
|
||||
if (entry.type !== "message") {
|
||||
continue;
|
||||
}
|
||||
const msg = entry.message;
|
||||
|
||||
if (msg.role === "toolResult") {
|
||||
const toolCall = toolCalls.get(msg.toolCallId);
|
||||
if (!toolCall) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const { path, name } = toolCall;
|
||||
const timestamp = msg.timestamp;
|
||||
|
||||
const existing = fileMap.get(path);
|
||||
if (existing) {
|
||||
existing.operations.add(name);
|
||||
if (timestamp > existing.lastTimestamp) {
|
||||
existing.lastTimestamp = timestamp;
|
||||
}
|
||||
} else {
|
||||
fileMap.set(path, {
|
||||
path,
|
||||
operations: new Set([name]),
|
||||
lastTimestamp: timestamp,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fileMap.size === 0) {
|
||||
ctx.ui.notify("No files read/written/edited in this session", "info");
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort by most recent first
|
||||
const files = Array.from(fileMap.values()).toSorted(
|
||||
(a, b) => b.lastTimestamp - a.lastTimestamp,
|
||||
);
|
||||
|
||||
const openSelected = async (file: FileEntry): Promise<void> => {
|
||||
try {
|
||||
await pi.exec("code", ["-g", file.path], { cwd: ctx.cwd });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
ctx.ui.notify(`Failed to open ${file.path}: ${message}`, "error");
|
||||
}
|
||||
};
|
||||
|
||||
// Show file picker with SelectList
|
||||
await ctx.ui.custom<void>((tui, theme, _kb, done) => {
|
||||
const container = new Container();
|
||||
|
||||
// Top border
|
||||
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
||||
|
||||
// Title
|
||||
container.addChild(new Text(theme.fg("accent", theme.bold(" Select file to open")), 0, 0));
|
||||
|
||||
// Build select items with colored operations
|
||||
const items: SelectItem[] = files.map((f) => {
|
||||
const ops: string[] = [];
|
||||
if (f.operations.has("read")) {
|
||||
ops.push(theme.fg("muted", "R"));
|
||||
}
|
||||
if (f.operations.has("write")) {
|
||||
ops.push(theme.fg("success", "W"));
|
||||
}
|
||||
if (f.operations.has("edit")) {
|
||||
ops.push(theme.fg("warning", "E"));
|
||||
}
|
||||
const opsLabel = ops.join("");
|
||||
return {
|
||||
value: f,
|
||||
label: `${opsLabel} ${f.path}`,
|
||||
};
|
||||
});
|
||||
|
||||
const visibleRows = Math.min(files.length, 15);
|
||||
let currentIndex = 0;
|
||||
|
||||
const selectList = new SelectList(items, visibleRows, {
|
||||
selectedPrefix: (t) => theme.fg("accent", t),
|
||||
selectedText: (t) => t, // Keep existing colors
|
||||
description: (t) => theme.fg("muted", t),
|
||||
scrollInfo: (t) => theme.fg("dim", t),
|
||||
noMatch: (t) => theme.fg("warning", t),
|
||||
});
|
||||
selectList.onSelect = (item) => {
|
||||
void openSelected(item.value as FileEntry);
|
||||
};
|
||||
selectList.onCancel = () => done();
|
||||
selectList.onSelectionChange = (item) => {
|
||||
currentIndex = items.indexOf(item);
|
||||
};
|
||||
container.addChild(selectList);
|
||||
|
||||
// Help text
|
||||
container.addChild(
|
||||
new Text(theme.fg("dim", " ↑↓ navigate • ←→ page • enter open • esc close"), 0, 0),
|
||||
);
|
||||
|
||||
// Bottom border
|
||||
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
||||
|
||||
return {
|
||||
render: (w) => container.render(w),
|
||||
invalidate: () => container.invalidate(),
|
||||
handleInput: (data) => {
|
||||
// Add paging with left/right
|
||||
if (matchesKey(data, Key.left)) {
|
||||
// Page up - clamp to 0
|
||||
currentIndex = Math.max(0, currentIndex - visibleRows);
|
||||
selectList.setSelectedIndex(currentIndex);
|
||||
} else if (matchesKey(data, Key.right)) {
|
||||
// Page down - clamp to last
|
||||
currentIndex = Math.min(items.length - 1, currentIndex + visibleRows);
|
||||
selectList.setSelectedIndex(currentIndex);
|
||||
} else {
|
||||
selectList.handleInput(data);
|
||||
}
|
||||
tui.requestRender();
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
193
.pi/extensions/prompt-url-widget.ts
Normal file
193
.pi/extensions/prompt-url-widget.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
import {
|
||||
DynamicBorder,
|
||||
type ExtensionAPI,
|
||||
type ExtensionContext,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
import { Container, Text } from "@mariozechner/pi-tui";
|
||||
|
||||
const PR_PROMPT_PATTERN = /^\s*You are given one or more GitHub PR URLs:\s*(\S+)/im;
|
||||
const ISSUE_PROMPT_PATTERN = /^\s*Analyze GitHub issue\(s\):\s*(\S+)/im;
|
||||
|
||||
type PromptMatch = {
|
||||
kind: "pr" | "issue";
|
||||
url: string;
|
||||
};
|
||||
|
||||
type GhMetadata = {
|
||||
title?: string;
|
||||
author?: {
|
||||
login?: string;
|
||||
name?: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
function extractPromptMatch(prompt: string): PromptMatch | undefined {
|
||||
const prMatch = prompt.match(PR_PROMPT_PATTERN);
|
||||
if (prMatch?.[1]) {
|
||||
return { kind: "pr", url: prMatch[1].trim() };
|
||||
}
|
||||
|
||||
const issueMatch = prompt.match(ISSUE_PROMPT_PATTERN);
|
||||
if (issueMatch?.[1]) {
|
||||
return { kind: "issue", url: issueMatch[1].trim() };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function fetchGhMetadata(
|
||||
pi: ExtensionAPI,
|
||||
kind: PromptMatch["kind"],
|
||||
url: string,
|
||||
): Promise<GhMetadata | undefined> {
|
||||
const args =
|
||||
kind === "pr"
|
||||
? ["pr", "view", url, "--json", "title,author"]
|
||||
: ["issue", "view", url, "--json", "title,author"];
|
||||
|
||||
try {
|
||||
const result = await pi.exec("gh", args);
|
||||
if (result.code !== 0 || !result.stdout) {
|
||||
return undefined;
|
||||
}
|
||||
return JSON.parse(result.stdout) as GhMetadata;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function formatAuthor(author?: GhMetadata["author"]): string | undefined {
|
||||
if (!author) {
|
||||
return undefined;
|
||||
}
|
||||
const name = author.name?.trim();
|
||||
const login = author.login?.trim();
|
||||
if (name && login) {
|
||||
return `${name} (@${login})`;
|
||||
}
|
||||
if (login) {
|
||||
return `@${login}`;
|
||||
}
|
||||
if (name) {
|
||||
return name;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export default function promptUrlWidgetExtension(pi: ExtensionAPI) {
|
||||
const setWidget = (
|
||||
ctx: ExtensionContext,
|
||||
match: PromptMatch,
|
||||
title?: string,
|
||||
authorText?: string,
|
||||
) => {
|
||||
ctx.ui.setWidget("prompt-url", (_tui, thm) => {
|
||||
const titleText = title ? thm.fg("accent", title) : thm.fg("accent", match.url);
|
||||
const authorLine = authorText ? thm.fg("muted", authorText) : undefined;
|
||||
const urlLine = thm.fg("dim", match.url);
|
||||
|
||||
const lines = [titleText];
|
||||
if (authorLine) {
|
||||
lines.push(authorLine);
|
||||
}
|
||||
lines.push(urlLine);
|
||||
|
||||
const container = new Container();
|
||||
container.addChild(new DynamicBorder((s: string) => thm.fg("muted", s)));
|
||||
container.addChild(new Text(lines.join("\n"), 1, 0));
|
||||
return container;
|
||||
});
|
||||
};
|
||||
|
||||
const applySessionName = (ctx: ExtensionContext, match: PromptMatch, title?: string) => {
|
||||
const label = match.kind === "pr" ? "PR" : "Issue";
|
||||
const trimmedTitle = title?.trim();
|
||||
const fallbackName = `${label}: ${match.url}`;
|
||||
const desiredName = trimmedTitle ? `${label}: ${trimmedTitle} (${match.url})` : fallbackName;
|
||||
const currentName = pi.getSessionName()?.trim();
|
||||
if (!currentName) {
|
||||
pi.setSessionName(desiredName);
|
||||
return;
|
||||
}
|
||||
if (currentName === match.url || currentName === fallbackName) {
|
||||
pi.setSessionName(desiredName);
|
||||
}
|
||||
};
|
||||
|
||||
pi.on("before_agent_start", async (event, ctx) => {
|
||||
if (!ctx.hasUI) {
|
||||
return;
|
||||
}
|
||||
const match = extractPromptMatch(event.prompt);
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
|
||||
setWidget(ctx, match);
|
||||
applySessionName(ctx, match);
|
||||
void fetchGhMetadata(pi, match.kind, match.url).then((meta) => {
|
||||
const title = meta?.title?.trim();
|
||||
const authorText = formatAuthor(meta?.author);
|
||||
setWidget(ctx, match, title, authorText);
|
||||
applySessionName(ctx, match, title);
|
||||
});
|
||||
});
|
||||
|
||||
pi.on("session_switch", async (_event, ctx) => {
|
||||
rebuildFromSession(ctx);
|
||||
});
|
||||
|
||||
const getUserText = (content: string | { type: string; text?: string }[] | undefined): string => {
|
||||
if (!content) {
|
||||
return "";
|
||||
}
|
||||
if (typeof content === "string") {
|
||||
return content;
|
||||
}
|
||||
return (
|
||||
content
|
||||
.filter((block): block is { type: "text"; text: string } => block.type === "text")
|
||||
.map((block) => block.text)
|
||||
.join("\n") ?? ""
|
||||
);
|
||||
};
|
||||
|
||||
const rebuildFromSession = (ctx: ExtensionContext) => {
|
||||
if (!ctx.hasUI) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = ctx.sessionManager.getEntries();
|
||||
const lastMatch = [...entries].toReversed().find((entry) => {
|
||||
if (entry.type !== "message" || entry.message.role !== "user") {
|
||||
return false;
|
||||
}
|
||||
const text = getUserText(entry.message.content);
|
||||
return !!extractPromptMatch(text);
|
||||
});
|
||||
|
||||
const content =
|
||||
lastMatch?.type === "message" && lastMatch.message.role === "user"
|
||||
? lastMatch.message.content
|
||||
: undefined;
|
||||
const text = getUserText(content);
|
||||
const match = text ? extractPromptMatch(text) : undefined;
|
||||
if (!match) {
|
||||
ctx.ui.setWidget("prompt-url", undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
setWidget(ctx, match);
|
||||
applySessionName(ctx, match);
|
||||
void fetchGhMetadata(pi, match.kind, match.url).then((meta) => {
|
||||
const title = meta?.title?.trim();
|
||||
const authorText = formatAuthor(meta?.author);
|
||||
setWidget(ctx, match, title, authorText);
|
||||
applySessionName(ctx, match, title);
|
||||
});
|
||||
};
|
||||
|
||||
pi.on("session_start", async (_event, ctx) => {
|
||||
rebuildFromSession(ctx);
|
||||
});
|
||||
}
|
||||
26
.pi/extensions/redraws.ts
Normal file
26
.pi/extensions/redraws.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Redraws Extension
|
||||
*
|
||||
* Exposes /tui to show TUI redraw stats.
|
||||
*/
|
||||
|
||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||
import { Text } from "@mariozechner/pi-tui";
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
pi.registerCommand("tui", {
|
||||
description: "Show TUI stats",
|
||||
handler: async (_args, ctx) => {
|
||||
if (!ctx.hasUI) {
|
||||
return;
|
||||
}
|
||||
let redraws = 0;
|
||||
await ctx.ui.custom<void>((tui, _theme, _keybindings, done) => {
|
||||
redraws = tui.fullRedraws;
|
||||
done(undefined);
|
||||
return new Text("", 0, 0);
|
||||
});
|
||||
ctx.ui.notify(`TUI full redraws: ${redraws}`, "info");
|
||||
},
|
||||
});
|
||||
}
|
||||
2
.pi/git/.gitignore
vendored
Normal file
2
.pi/git/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
58
.pi/prompts/cl.md
Normal file
58
.pi/prompts/cl.md
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
description: Audit changelog entries before release
|
||||
---
|
||||
|
||||
Audit changelog entries for all commits since the last release.
|
||||
|
||||
## Process
|
||||
|
||||
1. **Find the last release tag:**
|
||||
|
||||
```bash
|
||||
git tag --sort=-version:refname | head -1
|
||||
```
|
||||
|
||||
2. **List all commits since that tag:**
|
||||
|
||||
```bash
|
||||
git log <tag>..HEAD --oneline
|
||||
```
|
||||
|
||||
3. **Read each package's [Unreleased] section:**
|
||||
- packages/ai/CHANGELOG.md
|
||||
- packages/tui/CHANGELOG.md
|
||||
- packages/coding-agent/CHANGELOG.md
|
||||
|
||||
4. **For each commit, check:**
|
||||
- Skip: changelog updates, doc-only changes, release housekeeping
|
||||
- Determine which package(s) the commit affects (use `git show <hash> --stat`)
|
||||
- Verify a changelog entry exists in the affected package(s)
|
||||
- For external contributions (PRs), verify format: `Description ([#N](url) by [@user](url))`
|
||||
|
||||
5. **Cross-package duplication rule:**
|
||||
Changes in `ai`, `agent` or `tui` that affect end users should be duplicated to `coding-agent` changelog, since coding-agent is the user-facing package that depends on them.
|
||||
|
||||
6. **Add New Features section after changelog fixes:**
|
||||
- Insert a `### New Features` section at the start of `## [Unreleased]` in `packages/coding-agent/CHANGELOG.md`.
|
||||
- Propose the top new features to the user for confirmation before writing them.
|
||||
- Link to relevant docs and sections whenever possible.
|
||||
|
||||
7. **Report:**
|
||||
- List commits with missing entries
|
||||
- List entries that need cross-package duplication
|
||||
- Add any missing entries directly
|
||||
|
||||
## Changelog Format Reference
|
||||
|
||||
Sections (in order):
|
||||
|
||||
- `### Breaking Changes` - API changes requiring migration
|
||||
- `### Added` - New features
|
||||
- `### Changed` - Changes to existing functionality
|
||||
- `### Fixed` - Bug fixes
|
||||
- `### Removed` - Removed features
|
||||
|
||||
Attribution:
|
||||
|
||||
- Internal: `Fixed foo ([#123](https://github.com/badlogic/pi-mono/issues/123))`
|
||||
- External: `Added bar ([#456](https://github.com/badlogic/pi-mono/pull/456) by [@user](https://github.com/user))`
|
||||
22
.pi/prompts/is.md
Normal file
22
.pi/prompts/is.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
description: Analyze GitHub issues (bugs or feature requests)
|
||||
---
|
||||
|
||||
Analyze GitHub issue(s): $ARGUMENTS
|
||||
|
||||
For each issue:
|
||||
|
||||
1. Read the issue in full, including all comments and linked issues/PRs.
|
||||
|
||||
2. **For bugs**:
|
||||
- Ignore any root cause analysis in the issue (likely wrong)
|
||||
- Read all related code files in full (no truncation)
|
||||
- Trace the code path and identify the actual root cause
|
||||
- Propose a fix
|
||||
|
||||
3. **For feature requests**:
|
||||
- Read all related code files in full (no truncation)
|
||||
- Propose the most concise implementation approach
|
||||
- List affected files and changes needed
|
||||
|
||||
Do NOT implement unless explicitly asked. Analyze and propose only.
|
||||
70
.pi/prompts/landpr.md
Normal file
70
.pi/prompts/landpr.md
Normal file
@@ -0,0 +1,70 @@
|
||||
---
|
||||
description: Land a PR (merge with proper workflow)
|
||||
---
|
||||
|
||||
Input
|
||||
|
||||
- PR: $1 <number|url>
|
||||
- If missing: use the most recent PR mentioned in the conversation.
|
||||
- If ambiguous: ask.
|
||||
|
||||
Do (end-to-end)
|
||||
Goal: PR must end in GitHub state = MERGED (never CLOSED). Use `gh pr merge` with `--rebase` or `--squash`.
|
||||
|
||||
1. Repo clean: `git status`.
|
||||
2. Identify PR meta (author + head branch):
|
||||
|
||||
```sh
|
||||
gh pr view <PR> --json number,title,author,headRefName,baseRefName,headRepository --jq '{number,title,author:.author.login,head:.headRefName,base:.baseRefName,headRepo:.headRepository.nameWithOwner}'
|
||||
contrib=$(gh pr view <PR> --json author --jq .author.login)
|
||||
head=$(gh pr view <PR> --json headRefName --jq .headRefName)
|
||||
head_repo_url=$(gh pr view <PR> --json headRepository --jq .headRepository.url)
|
||||
```
|
||||
|
||||
3. Fast-forward base:
|
||||
- `git checkout main`
|
||||
- `git pull --ff-only`
|
||||
4. Create temp base branch from main:
|
||||
- `git checkout -b temp/landpr-<ts-or-pr>`
|
||||
5. Check out PR branch locally:
|
||||
- `gh pr checkout <PR>`
|
||||
6. Rebase PR branch onto temp base:
|
||||
- `git rebase temp/landpr-<ts-or-pr>`
|
||||
- Fix conflicts; keep history tidy.
|
||||
7. Fix + tests + changelog:
|
||||
- Implement fixes + add/adjust tests
|
||||
- Update `CHANGELOG.md` and mention `#<PR>` + `@$contrib`
|
||||
8. Decide merge strategy:
|
||||
- Rebase if we want to preserve commit history
|
||||
- Squash if we want a single clean commit
|
||||
- If unclear, ask
|
||||
9. Full gate (BEFORE commit):
|
||||
- `pnpm lint && pnpm build && pnpm test`
|
||||
10. Commit via committer (include # + contributor in commit message):
|
||||
- `committer "fix: <summary> (#<PR>) (thanks @$contrib)" CHANGELOG.md <changed files>`
|
||||
- `land_sha=$(git rev-parse HEAD)`
|
||||
11. Push updated PR branch (rebase => usually needs force):
|
||||
|
||||
```sh
|
||||
git remote add prhead "$head_repo_url.git" 2>/dev/null || git remote set-url prhead "$head_repo_url.git"
|
||||
git push --force-with-lease prhead HEAD:$head
|
||||
```
|
||||
|
||||
12. Merge PR (must show MERGED on GitHub):
|
||||
- Rebase: `gh pr merge <PR> --rebase`
|
||||
- Squash: `gh pr merge <PR> --squash`
|
||||
- Never `gh pr close` (closing is wrong)
|
||||
13. Sync main:
|
||||
- `git checkout main`
|
||||
- `git pull --ff-only`
|
||||
14. Comment on PR with what we did + SHAs + thanks:
|
||||
|
||||
```sh
|
||||
merge_sha=$(gh pr view <PR> --json mergeCommit --jq '.mergeCommit.oid')
|
||||
gh pr comment <PR> --body "Landed via temp rebase onto main.\n\n- Gate: pnpm lint && pnpm build && pnpm test\n- Land commit: $land_sha\n- Merge commit: $merge_sha\n\nThanks @$contrib!"
|
||||
```
|
||||
|
||||
15. Verify PR state == MERGED:
|
||||
- `gh pr view <PR> --json state --jq .state`
|
||||
16. Delete temp branch:
|
||||
- `git branch -D temp/landpr-<ts-or-pr>`
|
||||
105
.pi/prompts/reviewpr.md
Normal file
105
.pi/prompts/reviewpr.md
Normal file
@@ -0,0 +1,105 @@
|
||||
---
|
||||
description: Review a PR thoroughly without merging
|
||||
---
|
||||
|
||||
Input
|
||||
|
||||
- PR: $1 <number|url>
|
||||
- If missing: use the most recent PR mentioned in the conversation.
|
||||
- If ambiguous: ask.
|
||||
|
||||
Do (review-only)
|
||||
Goal: produce a thorough review and a clear recommendation (READY for /landpr vs NEEDS WORK). Do NOT merge, do NOT push, do NOT make changes in the repo as part of this command.
|
||||
|
||||
1. Identify PR meta + context
|
||||
|
||||
```sh
|
||||
gh pr view <PR> --json number,title,state,isDraft,author,baseRefName,headRefName,headRepository,url,body,labels,assignees,reviewRequests,files,additions,deletions --jq '{number,title,url,state,isDraft,author:.author.login,base:.baseRefName,head:.headRefName,headRepo:.headRepository.nameWithOwner,additions,deletions,files:.files|length}'
|
||||
```
|
||||
|
||||
2. Read the PR description carefully
|
||||
- Summarize the stated goal, scope, and any "why now?" rationale.
|
||||
- Call out any missing context: motivation, alternatives considered, rollout/compat notes, risk.
|
||||
|
||||
3. Read the diff thoroughly (prefer full diff)
|
||||
|
||||
```sh
|
||||
gh pr diff <PR>
|
||||
# If you need more surrounding context for files:
|
||||
gh pr checkout <PR> # optional; still review-only
|
||||
git show --stat
|
||||
```
|
||||
|
||||
4. Validate the change is needed / valuable
|
||||
- What user/customer/dev pain does this solve?
|
||||
- Is this change the smallest reasonable fix?
|
||||
- Are we introducing complexity for marginal benefit?
|
||||
- Are we changing behavior/contract in a way that needs docs or a release note?
|
||||
|
||||
5. Evaluate implementation quality + optimality
|
||||
- Correctness: edge cases, error handling, null/undefined, concurrency, ordering.
|
||||
- Design: is the abstraction/architecture appropriate or over/under-engineered?
|
||||
- Performance: hot paths, allocations, queries, network, N+1s, caching.
|
||||
- Security/privacy: authz/authn, input validation, secrets, logging PII.
|
||||
- Backwards compatibility: public APIs, config, migrations.
|
||||
- Style consistency: formatting, naming, patterns used elsewhere.
|
||||
|
||||
6. Tests & verification
|
||||
- Identify what's covered by tests (unit/integration/e2e).
|
||||
- Are there regression tests for the bug fixed / scenario added?
|
||||
- Missing tests? Call out exact cases that should be added.
|
||||
- If tests are present, do they actually assert the important behavior (not just snapshots / happy path)?
|
||||
|
||||
7. Follow-up refactors / cleanup suggestions
|
||||
- Any code that should be simplified before merge?
|
||||
- Any TODOs that should be tickets vs addressed now?
|
||||
- Any deprecations, docs, types, or lint rules we should adjust?
|
||||
|
||||
8. Key questions to answer explicitly
|
||||
- Can we fix everything ourselves in a follow-up, or does the contributor need to update this PR?
|
||||
- Any blocking concerns (must-fix before merge)?
|
||||
- Is this PR ready to land, or does it need work?
|
||||
|
||||
9. Output (structured)
|
||||
Produce a review with these sections:
|
||||
|
||||
A) TL;DR recommendation
|
||||
|
||||
- One of: READY FOR /landpr | NEEDS WORK | NEEDS DISCUSSION
|
||||
- 1–3 sentence rationale.
|
||||
|
||||
B) What changed
|
||||
|
||||
- Brief bullet summary of the diff/behavioral changes.
|
||||
|
||||
C) What's good
|
||||
|
||||
- Bullets: correctness, simplicity, tests, docs, ergonomics, etc.
|
||||
|
||||
D) Concerns / questions (actionable)
|
||||
|
||||
- Numbered list.
|
||||
- Mark each item as:
|
||||
- BLOCKER (must fix before merge)
|
||||
- IMPORTANT (should fix before merge)
|
||||
- NIT (optional)
|
||||
- For each: point to the file/area and propose a concrete fix or alternative.
|
||||
|
||||
E) Tests
|
||||
|
||||
- What exists.
|
||||
- What's missing (specific scenarios).
|
||||
|
||||
F) Follow-ups (optional)
|
||||
|
||||
- Non-blocking refactors/tickets to open later.
|
||||
|
||||
G) Suggested PR comment (optional)
|
||||
|
||||
- Offer: "Want me to draft a PR comment to the author?"
|
||||
- If yes, provide a ready-to-paste comment summarizing the above, with clear asks.
|
||||
|
||||
Rules / Guardrails
|
||||
|
||||
- Review only: do not merge (`gh pr merge`), do not push branches, do not edit code.
|
||||
- If you need clarification, ask questions rather than guessing.
|
||||
@@ -51,9 +51,9 @@ repos:
|
||||
rev: v0.11.0
|
||||
hooks:
|
||||
- id: shellcheck
|
||||
args: [--severity=error] # Only fail on errors, not warnings/info
|
||||
args: [--severity=error] # Only fail on errors, not warnings/info
|
||||
# Exclude vendor and scripts with embedded code or known issues
|
||||
exclude: '^(vendor/|scripts/e2e/)'
|
||||
exclude: "^(vendor/|scripts/e2e/)"
|
||||
|
||||
# GitHub Actions linting
|
||||
- repo: https://github.com/rhysd/actionlint
|
||||
@@ -67,7 +67,7 @@ repos:
|
||||
hooks:
|
||||
- id: zizmor
|
||||
args: [--persona=regular, --min-severity=medium, --min-confidence=medium]
|
||||
exclude: '^(vendor/|Swabble/)'
|
||||
exclude: "^(vendor/|Swabble/)"
|
||||
|
||||
# Project checks (same commands as CI)
|
||||
- repo: local
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
src/canvas-host/a2ui/a2ui.bundle.js
|
||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["oxc.oxc-vscode"]
|
||||
}
|
||||
22
.vscode/settings.json
vendored
Normal file
22
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"files.insertFinalNewline": true,
|
||||
"files.trimFinalNewlines": true,
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||
},
|
||||
"typescript.preferences.importModuleSpecifierEnding": "js",
|
||||
"typescript.reportStyleChecksAsWarnings": false,
|
||||
"typescript.updateImportsOnFileMove.enabled": "always",
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"typescript.experimental.useTsgo": true
|
||||
}
|
||||
36
AGENTS.md
36
AGENTS.md
@@ -1,8 +1,10 @@
|
||||
# Repository Guidelines
|
||||
|
||||
- Repo: https://github.com/openclaw/openclaw
|
||||
- GitHub issues/comments/PR comments: use literal multiline strings or `-F - <<'EOF'` (or $'...') for real newlines; never embed "\\n".
|
||||
|
||||
## Project Structure & Module Organization
|
||||
|
||||
- Source code: `src/` (CLI wiring in `src/cli`, commands in `src/commands`, web provider in `src/provider-web.ts`, infra in `src/infra`, media pipeline in `src/media`).
|
||||
- Tests: colocated `*.test.ts`.
|
||||
- Docs: `docs/` (images, queue, Pi config). Built output lives in `dist/`.
|
||||
@@ -16,6 +18,7 @@
|
||||
- When adding channels/extensions/apps/docs, review `.github/labeler.yml` for label coverage.
|
||||
|
||||
## Docs Linking (Mintlify)
|
||||
|
||||
- Docs are hosted on Mintlify (docs.openclaw.ai).
|
||||
- Internal doc links in `docs/**/*.md`: root-relative, no `.md`/`.mdx` (example: `[Config](/configuration)`).
|
||||
- Section cross-references: use anchors on root-relative paths (example: `[Hooks](/configuration#hooks)`).
|
||||
@@ -25,7 +28,16 @@
|
||||
- README (GitHub): keep absolute docs URLs (`https://docs.openclaw.ai/...`) so links work on GitHub.
|
||||
- Docs content must be generic: no personal device names/hostnames/paths; use placeholders like `user@gateway-host` and “gateway host”.
|
||||
|
||||
## Docs i18n (zh-CN)
|
||||
|
||||
- `docs/zh-CN/**` is generated; do not edit unless the user explicitly asks.
|
||||
- Pipeline: update English docs → adjust glossary (`docs/.i18n/glossary.zh-CN.json`) → run `scripts/docs-i18n` → apply targeted fixes only if instructed.
|
||||
- Translation memory: `docs/.i18n/zh-CN.tm.jsonl` (generated).
|
||||
- See `docs/.i18n/README.md`.
|
||||
- The pipeline can be slow/inefficient; if it’s dragging, ping @jospalmbier on Discord instead of hacking around it.
|
||||
|
||||
## exe.dev VM ops (general)
|
||||
|
||||
- Access: stable path is `ssh exe.dev` then `ssh vm-name` (assume SSH key already set).
|
||||
- SSH flaky: use exe.dev web terminal or Shelley (web agent); keep a tmux session for long ops.
|
||||
- Update: `sudo npm i -g openclaw@latest` (global install needs root on `/usr/lib/node_modules`).
|
||||
@@ -36,6 +48,7 @@
|
||||
- Verify: `openclaw channels status --probe`, `ss -ltnp | rg 18789`, `tail -n 120 /tmp/openclaw-gateway.log`.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
|
||||
- Runtime baseline: Node **22+** (keep Node + Bun paths working).
|
||||
- Install deps: `pnpm install`
|
||||
- Pre-commit hooks: `prek install` (runs same checks as CI)
|
||||
@@ -44,24 +57,28 @@
|
||||
- Run CLI in dev: `pnpm openclaw ...` (bun) or `pnpm dev`.
|
||||
- Node remains supported for running built output (`dist/*`) and production installs.
|
||||
- Mac packaging (dev): `scripts/package-mac-app.sh` defaults to current arch. Release checklist: `docs/platforms/mac/release.md`.
|
||||
- Type-check/build: `pnpm build` (tsc)
|
||||
- Lint/format: `pnpm lint` (oxlint), `pnpm format` (oxfmt)
|
||||
- Type-check/build: `pnpm build`
|
||||
- TypeScript checks: `pnpm tsgo`
|
||||
- Lint/format: `pnpm check`
|
||||
- Tests: `pnpm test` (vitest); coverage: `pnpm test:coverage`
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
|
||||
- Language: TypeScript (ESM). Prefer strict typing; avoid `any`.
|
||||
- Formatting/linting via Oxlint and Oxfmt; run `pnpm lint` before commits.
|
||||
- Formatting/linting via Oxlint and Oxfmt; run `pnpm check` before commits.
|
||||
- Add brief code comments for tricky or non-obvious logic.
|
||||
- Keep files concise; extract helpers instead of “V2” copies. Use existing patterns for CLI options and dependency injection via `createDefaultDeps`.
|
||||
- Aim to keep files under ~700 LOC; guideline only (not a hard guardrail). Split/refactor when it improves clarity or testability.
|
||||
- Naming: use **OpenClaw** for product/app/docs headings; use `openclaw` for CLI command, package/binary, paths, and config keys.
|
||||
|
||||
## Release Channels (Naming)
|
||||
|
||||
- stable: tagged releases only (e.g. `vYYYY.M.D`), npm dist-tag `latest`.
|
||||
- beta: prerelease tags `vYYYY.M.D-beta.N`, npm dist-tag `beta` (may ship without macOS app).
|
||||
- dev: moving head on `main` (no tag; git checkout main).
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
- Framework: Vitest with V8 coverage thresholds (70% lines/branches/functions/statements).
|
||||
- Naming: match source names with `*.test.ts`; e2e in `*.e2e.test.ts`.
|
||||
- Run `pnpm test` (or `pnpm test:coverage`) before pushing when you touch logic.
|
||||
@@ -72,11 +89,14 @@
|
||||
- Mobile: before using a simulator, check for connected real devices (iOS + Android) and prefer them when available.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
|
||||
- Create commits with `scripts/committer "<msg>" <file...>`; avoid manual `git add`/`git commit` so staging stays scoped.
|
||||
- Follow concise, action-oriented commit messages (e.g., `CLI: add verbose flag to send`).
|
||||
- Group related changes; avoid bundling unrelated refactors.
|
||||
- Changelog workflow: keep latest released version at top (no `Unreleased`); after publishing, bump version and start a new top section.
|
||||
- PRs should summarize scope, note testing performed, and mention any user-facing changes or new flags.
|
||||
- Read this when submitting a PR: `docs/help/submitting-a-pr.md` ([Submitting a PR](https://docs.openclaw.ai/help/submitting-a-pr))
|
||||
- Read this when submitting an issue: `docs/help/submitting-an-issue.md` ([Submitting an Issue](https://docs.openclaw.ai/help/submitting-an-issue))
|
||||
- PR review flow: when given a PR link, review via `gh pr view`/`gh pr diff` and do **not** change branches.
|
||||
- PR review calls: prefer a single `gh pr view --json ...` to batch metadata/comments; run `gh pr diff` only when needed.
|
||||
- Before starting a review when a GH Issue/PR is pasted: run `git pull`; if there are local changes or unpushed commits, stop and alert the user before reviewing.
|
||||
@@ -90,23 +110,28 @@
|
||||
- After merging a PR: run `bun scripts/update-clawtributors.ts` if the contributor is missing, then commit the regenerated README.
|
||||
|
||||
## Shorthand Commands
|
||||
|
||||
- `sync`: if working tree is dirty, commit all changes (pick a sensible Conventional Commit message), then `git pull --rebase`; if rebase conflicts and cannot resolve, stop; otherwise `git push`.
|
||||
|
||||
### PR Workflow (Review vs Land)
|
||||
|
||||
- **Review mode (PR link only):** read `gh pr view/diff`; **do not** switch branches; **do not** change code.
|
||||
- **Landing mode:** create an integration branch from `main`, bring in PR commits (**prefer rebase** for linear history; **merge allowed** when complexity/conflicts make it safer), apply fixes, add changelog (+ thanks + PR #), run full gate **locally before committing** (`pnpm lint && pnpm build && pnpm test`), commit, merge back to `main`, then `git switch main` (never stay on a topic branch after landing). Important: contributor needs to be in git graph after this!
|
||||
- **Landing mode:** create an integration branch from `main`, bring in PR commits (**prefer rebase** for linear history; **merge allowed** when complexity/conflicts make it safer), apply fixes, add changelog (+ thanks + PR #), run full gate **locally before committing** (`pnpm build && pnpm check && pnpm test`), commit, merge back to `main`, then `git switch main` (never stay on a topic branch after landing). Important: contributor needs to be in git graph after this!
|
||||
|
||||
## Security & Configuration Tips
|
||||
|
||||
- Web provider stores creds at `~/.openclaw/credentials/`; rerun `openclaw login` if logged out.
|
||||
- Pi sessions live under `~/.openclaw/sessions/` by default; the base directory is not configurable.
|
||||
- Environment variables: see `~/.profile`.
|
||||
- Never commit or publish real phone numbers, videos, or live configuration values. Use obviously fake placeholders in docs, tests, and examples.
|
||||
- Release flow: always read `docs/reference/RELEASING.md` and `docs/platforms/mac/release.md` before any release work; do not ask routine questions once those docs answer them.
|
||||
- Release flow: always read `docs/reference/RELEASING.md` and `docs/platforms/mac/release.md` before any release work; do not ask routine questions once those docs answer them.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Rebrand/migration issues or legacy config/service warnings: run `openclaw doctor` (see `docs/gateway/doctor.md`).
|
||||
|
||||
## Agent-Specific Notes
|
||||
|
||||
- Vocabulary: "makeup" = "mac app".
|
||||
- Never edit `node_modules` (global/Homebrew/npm/git installs too). Updates overwrite. Skill notes go in `tools.md` or `AGENTS.md`.
|
||||
- Signal: "update fly" => `fly ssh console -a flawd-bot -C "bash -lc 'cd /data/clawd/openclaw && git pull --rebase origin main'"` then `fly machines restart e825232f34d058 -a flawd-bot`.
|
||||
@@ -155,6 +180,7 @@
|
||||
- Release guardrails: do not change version numbers without operator’s explicit consent; always ask permission before running any npm publish/release step.
|
||||
|
||||
## NPM + 1Password (publish/verify)
|
||||
|
||||
- Use the 1password skill; all `op` commands must run inside a fresh tmux session.
|
||||
- Sign in: `eval "$(op signin --account my.1password.com)"` (app unlocked + integration on).
|
||||
- OTP: `op read 'op://Private/Npmjs/one-time password?attribute=otp'`.
|
||||
|
||||
407
CHANGELOG.md
407
CHANGELOG.md
@@ -2,10 +2,327 @@
|
||||
|
||||
Docs: https://docs.openclaw.ai
|
||||
|
||||
## 2026.1.29
|
||||
Status: stable.
|
||||
## 2026.2.6-4
|
||||
|
||||
### Added
|
||||
|
||||
- Gateway: add `agents.create`, `agents.update`, `agents.delete` RPC methods for web UI agent management. (#11045) Thanks @advaitpaliwal.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Web UI: make chat refresh smoothly scroll to the latest messages and suppress new-messages badge flash during manual refresh.
|
||||
- Cron: route text-only isolated agent announces through the shared subagent announce flow; add exponential backoff for repeated errors; preserve future `nextRunAtMs` on restart; include current-boundary schedule matches; prevent stale threadId reuse across targets; and add per-job execution timeout. (#11641) Thanks @tyler6204.
|
||||
- Subagents: stabilize announce timing, preserve compaction metrics across retries, clamp overflow-prone long timeouts, and cap impossible context usage token totals. (#11551) Thanks @tyler6204.
|
||||
- Agents: recover from context overflow caused by oversized tool results (pre-emptive capping + fallback truncation). (#11579) Thanks @tyler6204.
|
||||
- Gateway/CLI: when `gateway.bind=lan`, use a LAN IP for probe URLs and Control UI links. (#11448) Thanks @AnonO6.
|
||||
- Memory: set Voyage embeddings `input_type` for improved retrieval. (#10818) Thanks @mcinteerj.
|
||||
- Memory/QMD: run boot refresh in background by default, add configurable QMD maintenance timeouts, and retry QMD after fallback failures. (#9690, #9705)
|
||||
- Media understanding: recognize `.caf` audio attachments for transcription. (#10982) Thanks @succ985.
|
||||
- State dir: honor `OPENCLAW_STATE_DIR` for default device identity and canvas storage paths. (#4824) Thanks @kossoy.
|
||||
- Tests: harden flaky hotspots by removing timer sleeps, consolidating onboarding provider-auth coverage, and improving memory test realism. (#11598) Thanks @gumadeiras.
|
||||
|
||||
## 2026.2.6
|
||||
|
||||
### Changes
|
||||
|
||||
- Cron: default `wakeMode` is now `"now"` for new jobs (was `"next-heartbeat"`). (#10776) Thanks @tyler6204.
|
||||
- Cron: `cron run` defaults to force execution; use `--due` to restrict to due-only. (#10776) Thanks @tyler6204.
|
||||
- Models: support Anthropic Opus 4.6 and OpenAI Codex gpt-5.3-codex (forward-compat fallbacks). (#9853, #10720, #9995) Thanks @TinyTb, @calvin-hpnet, @tyler6204.
|
||||
- Providers: add xAI (Grok) support. (#9885) Thanks @grp06.
|
||||
- Providers: add Baidu Qianfan support. (#8868) Thanks @ide-rea.
|
||||
- Web UI: add token usage dashboard. (#10072) Thanks @Takhoffman.
|
||||
- Memory: native Voyage AI support. (#7078) Thanks @mcinteerj.
|
||||
- Sessions: cap sessions_history payloads to reduce context overflow. (#10000) Thanks @gut-puncture.
|
||||
- CLI: sort commands alphabetically in help output. (#8068) Thanks @deepsoumya617.
|
||||
- CI: optimize pipeline throughput (macOS consolidation, Windows perf, workflow concurrency). (#10784) Thanks @mcaxtr.
|
||||
- Agents: bump pi-mono to 0.52.7; add embedded forward-compat fallback for Opus 4.6 model ids.
|
||||
|
||||
### Added
|
||||
|
||||
- Cron: run history deep-links to session chat from the dashboard. (#10776) Thanks @tyler6204.
|
||||
- Cron: per-run session keys in run log entries and default labels for cron sessions. (#10776) Thanks @tyler6204.
|
||||
- Cron: legacy payload field compatibility (`deliver`, `channel`, `to`, `bestEffortDeliver`) in schema. (#10776) Thanks @tyler6204.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Cron: scheduler reliability (timer drift, restart catch-up, lock contention, stale running markers). (#10776) Thanks @tyler6204.
|
||||
- Cron: store migration hardening (legacy field migration, parse error handling, explicit delivery mode persistence). (#10776) Thanks @tyler6204.
|
||||
- Telegram: auto-inject DM topic threadId in message tool + subagent announce. (#7235) Thanks @Lukavyi.
|
||||
- Security: require auth for Gateway canvas host and A2UI assets. (#9518) Thanks @coygeek.
|
||||
- Cron: fix scheduling and reminder delivery regressions; harden next-run recompute + timer re-arming + legacy schedule fields. (#9733, #9823, #9948, #9932) Thanks @tyler6204, @pycckuu, @j2h4u, @fujiwara-tofu-shop.
|
||||
- Update: harden Control UI asset handling in update flow. (#10146) Thanks @gumadeiras.
|
||||
- Security: add skill/plugin code safety scanner; redact credentials from config.get gateway responses. (#9806, #9858) Thanks @abdelsfane.
|
||||
- Exec approvals: coerce bare string allowlist entries to objects. (#9903) Thanks @mcaxtr.
|
||||
- Slack: add mention stripPatterns for /new and /reset. (#9971) Thanks @ironbyte-rgb.
|
||||
- Chrome extension: fix bundled path resolution. (#8914) Thanks @kelvinCB.
|
||||
- Compaction/errors: allow multiple compaction retries on context overflow; show clear billing errors. (#8928, #8391) Thanks @Glucksberg.
|
||||
|
||||
## 2026.2.3
|
||||
|
||||
### Changes
|
||||
|
||||
- Telegram: remove last `@ts-nocheck` from `bot-handlers.ts`, use Grammy types directly, deduplicate `StickerMetadata`. Zero `@ts-nocheck` remaining in `src/telegram/`. (#9206)
|
||||
- Telegram: remove `@ts-nocheck` from `bot-message.ts`, type deps via `Omit<BuildTelegramMessageContextParams>`, widen `allMedia` to `TelegramMediaRef[]`. (#9180)
|
||||
- Telegram: remove `@ts-nocheck` from `bot.ts`, fix duplicate `bot.catch` error handler (Grammy overrides), remove dead reaction `message_thread_id` routing, harden sticker cache guard. (#9077)
|
||||
- Onboarding: add Cloudflare AI Gateway provider setup and docs. (#7914) Thanks @roerohan.
|
||||
- Onboarding: add Moonshot (.cn) auth choice and keep the China base URL when preserving defaults. (#7180) Thanks @waynelwz.
|
||||
- Docs: clarify tmux send-keys for TUI by splitting text and Enter. (#7737) Thanks @Wangnov.
|
||||
- Docs: mirror the landing page revamp for zh-CN (features, quickstart, docs directory, network model, credits). (#8994) Thanks @joshp123.
|
||||
- Messages: add per-channel and per-account responsePrefix overrides across channels. (#9001) Thanks @mudrii.
|
||||
- Cron: add announce delivery mode for isolated jobs (CLI + Control UI) and delivery mode config.
|
||||
- Cron: default isolated jobs to announce delivery; accept ISO 8601 `schedule.at` in tool inputs.
|
||||
- Cron: hard-migrate isolated jobs to announce/none delivery; drop legacy post-to-main/payload delivery fields and `atMs` inputs.
|
||||
- Cron: delete one-shot jobs after success by default; add `--keep-after-run` for CLI.
|
||||
- Cron: suppress messaging tools during announce delivery so summaries post consistently.
|
||||
- Cron: avoid duplicate deliveries when isolated runs send messages directly.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Heartbeat: allow explicit accountId routing for multi-account channels. (#8702) Thanks @lsh411.
|
||||
- TUI/Gateway: handle non-streaming finals, refresh history for non-local chat runs, and avoid event gap warnings for targeted tool streams. (#8432) Thanks @gumadeiras.
|
||||
- Shell completion: auto-detect and migrate slow dynamic patterns to cached files for faster terminal startup; add completion health checks to doctor/update/onboard.
|
||||
- Telegram: honor session model overrides in inline model selection. (#8193) Thanks @gildo.
|
||||
- Web UI: fix agent model selection saves for default/non-default agents and wrap long workspace paths. Thanks @Takhoffman.
|
||||
- Web UI: resolve header logo path when `gateway.controlUi.basePath` is set. (#7178) Thanks @Yeom-JinHo.
|
||||
- Web UI: apply button styling to the new-messages indicator.
|
||||
- Onboarding: infer auth choice from non-interactive API key flags. (#8484) Thanks @f-trycua.
|
||||
- Security: keep untrusted channel metadata out of system prompts (Slack/Discord). Thanks @KonstantinMirin.
|
||||
- Security: enforce sandboxed media paths for message tool attachments. (#9182) Thanks @victormier.
|
||||
- Security: require explicit credentials for gateway URL overrides to prevent credential leakage. (#8113) Thanks @victormier.
|
||||
- Security: gate `whatsapp_login` tool to owner senders and default-deny non-owner contexts. (#8768) Thanks @victormier.
|
||||
- Voice call: harden webhook verification with host allowlists/proxy trust and keep ngrok loopback bypass.
|
||||
- Voice call: add regression coverage for anonymous inbound caller IDs with allowlist policy. (#8104) Thanks @victormier.
|
||||
- Cron: accept epoch timestamps and 0ms durations in CLI `--at` parsing.
|
||||
- Cron: reload store data when the store file is recreated or mtime changes.
|
||||
- Cron: deliver announce runs directly, honor delivery mode, and respect wakeMode for summaries. (#8540) Thanks @tyler6204.
|
||||
- Telegram: include forward_from_chat metadata in forwarded messages and harden cron delivery target checks. (#8392) Thanks @Glucksberg.
|
||||
- macOS: fix cron payload summary rendering and ISO 8601 formatter concurrency safety.
|
||||
|
||||
## 2026.2.2-3
|
||||
|
||||
### Fixes
|
||||
|
||||
- Update: ship legacy daemon-cli shim for pre-tsdown update imports (fixes daemon restart after npm update).
|
||||
|
||||
## 2026.2.2-2
|
||||
|
||||
### Changes
|
||||
|
||||
- Docs: promote BlueBubbles as the recommended iMessage integration; mark imsg channel as legacy. (#8415) Thanks @tyler6204.
|
||||
|
||||
### Fixes
|
||||
|
||||
- CLI status: resolve build-info from bundled dist output (fixes "unknown" commit in npm builds).
|
||||
|
||||
## 2026.2.2-1
|
||||
|
||||
### Fixes
|
||||
|
||||
- CLI status: fall back to build-info for version detection (fixes "unknown" in beta builds). Thanks @gumadeira.
|
||||
|
||||
## 2026.2.2
|
||||
|
||||
### Changes
|
||||
|
||||
- Feishu: add Feishu/Lark plugin support + docs. (#7313) Thanks @jiulingyun (openclaw-cn).
|
||||
- Web UI: add Agents dashboard for managing agent files, tools, skills, models, channels, and cron jobs.
|
||||
- Subagents: discourage direct messaging tool use unless a specific external recipient is requested.
|
||||
- Memory: implement the opt-in QMD backend for workspace memory. (#3160) Thanks @vignesh07.
|
||||
- Security: add healthcheck skill and bootstrap audit guidance. (#7641) Thanks @Takhoffman.
|
||||
- Config: allow setting a default subagent thinking level via `agents.defaults.subagents.thinking` (and per-agent `agents.list[].subagents.thinking`). (#7372) Thanks @tyler6204.
|
||||
- Docs: zh-CN translations seed + polish, pipeline guidance, nav/landing updates, and typo fixes. (#8202, #6995, #6619, #7242, #7303, #7415) Thanks @AaronWander, @taiyi747, @Explorer1092, @rendaoyuan, @joshp123, @lailoo.
|
||||
- Docs: add zh-CN i18n guardrails to avoid editing generated translations. (#8416) Thanks @joshp123.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Docs: finish renaming the QMD memory docs to reference the OpenClaw state dir.
|
||||
- Onboarding: keep TUI flow exclusive (skip completion prompt + background Web UI seed).
|
||||
- Onboarding: drop completion prompt now handled by install/update.
|
||||
- TUI: block onboarding output while TUI is active and restore terminal state on exit.
|
||||
- CLI: cache shell completion scripts in state dir and source cached files in profiles.
|
||||
- Zsh completion: escape option descriptions to avoid invalid option errors.
|
||||
- Agents: repair malformed tool calls and session transcripts. (#7473) Thanks @justinhuangcode.
|
||||
- fix(agents): validate AbortSignal instances before calling AbortSignal.any() (#7277) (thanks @Elarwei001)
|
||||
- fix(webchat): respect user scroll position during streaming and refresh (#7226) (thanks @marcomarandiz)
|
||||
- Telegram: recover from grammY long-poll timed out errors. (#7466) Thanks @macmimi23.
|
||||
- Media understanding: skip binary media from file text extraction. (#7475) Thanks @AlexZhangji.
|
||||
- Security: enforce access-group gating for Slack slash commands when channel type lookup fails.
|
||||
- Security: require validated shared-secret auth before skipping device identity on gateway connect.
|
||||
- Security: guard skill installer downloads with SSRF checks (block private/localhost URLs).
|
||||
- Security: harden Windows exec allowlist; block cmd.exe bypass via single &. Thanks @simecek.
|
||||
- fix(voice-call): harden inbound allowlist; reject anonymous callers; require Telnyx publicKey for allowlist; token-gate Twilio media streams; cap webhook body size (thanks @simecek)
|
||||
- Media understanding: apply SSRF guardrails to provider fetches; allow private baseUrl overrides explicitly.
|
||||
- fix(webchat): respect user scroll position during streaming and refresh (#7226) (thanks @marcomarandiz)
|
||||
- Telegram: recover from grammY long-poll timed out errors. (#7466) Thanks @macmimi23.
|
||||
- Agents: repair malformed tool calls and session transcripts. (#7473) Thanks @justinhuangcode.
|
||||
- fix(agents): validate AbortSignal instances before calling AbortSignal.any() (#7277) (thanks @Elarwei001)
|
||||
- Media understanding: skip binary media from file text extraction. (#7475) Thanks @AlexZhangji.
|
||||
- Onboarding: keep TUI flow exclusive (skip completion prompt + background Web UI seed); completion prompt now handled by install/update.
|
||||
- TUI: block onboarding output while TUI is active and restore terminal state on exit.
|
||||
- CLI/Zsh completion: cache scripts in state dir and escape option descriptions to avoid invalid option errors.
|
||||
- fix(ui): resolve Control UI asset path correctly.
|
||||
- fix(ui): refresh agent files after external edits.
|
||||
- Docs: finish renaming the QMD memory docs to reference the OpenClaw state dir.
|
||||
- Tests: stub SSRF DNS pinning in web auto-reply + Gemini video coverage. (#6619) Thanks @joshp123.
|
||||
|
||||
## 2026.2.1
|
||||
|
||||
### Changes
|
||||
|
||||
- Docs: onboarding/install/i18n/exec-approvals/Control UI/exe.dev/cacheRetention updates + misc nav/typos. (#3050, #3461, #4064, #4675, #4729, #4763, #5003, #5402, #5446, #5474, #5663, #5689, #5694, #5967, #6270, #6300, #6311, #6416, #6487, #6550, #6789)
|
||||
- Telegram: use shared pairing store. (#6127) Thanks @obviyus.
|
||||
- Agents: add OpenRouter app attribution headers. Thanks @alexanderatallah.
|
||||
- Agents: add system prompt safety guardrails. (#5445) Thanks @joshp123.
|
||||
- Agents: update pi-ai to 0.50.9 and rename cacheControlTtl -> cacheRetention (with back-compat mapping).
|
||||
- Agents: extend CreateAgentSessionOptions with systemPrompt/skills/contextFiles.
|
||||
- Agents: add tool policy conformance snapshot (no runtime behavior change). (#6011)
|
||||
- Auth: update MiniMax OAuth hint + portal auth note copy.
|
||||
- Discord: inherit thread parent bindings for routing. (#3892) Thanks @aerolalit.
|
||||
- Gateway: inject timestamps into agent and chat.send messages. (#3705) Thanks @conroywhitney, @CashWilliams.
|
||||
- Gateway: require TLS 1.3 minimum for TLS listeners. (#5970) Thanks @loganaden.
|
||||
- Web UI: refine chat layout + extend session active duration.
|
||||
- CI: add formal conformance + alias consistency checks. (#5723, #5807)
|
||||
|
||||
### Fixes
|
||||
|
||||
- Security: guard remote media fetches with SSRF protections (block private/localhost, DNS pinning).
|
||||
- Updates: clean stale global install rename dirs and extend gateway update timeouts to avoid npm ENOTEMPTY failures.
|
||||
- Plugins: validate plugin/hook install paths and reject traversal-like names.
|
||||
- Telegram: add download timeouts for file fetches. (#6914) Thanks @hclsys.
|
||||
- Telegram: enforce thread specs for DM vs forum sends. (#6833) Thanks @obviyus.
|
||||
- Streaming: flush block streaming on paragraph boundaries for newline chunking. (#7014)
|
||||
- Streaming: stabilize partial streaming filters.
|
||||
- Auto-reply: avoid referencing workspace files in /new greeting prompt. (#5706) Thanks @bravostation.
|
||||
- Tools: align tool execute adapters/signatures (legacy + parameter order + arg normalization).
|
||||
- Tools: treat "\*" tool allowlist entries as valid to avoid spurious unknown-entry warnings.
|
||||
- Skills: update session-logs paths from .clawdbot to .openclaw. (#4502)
|
||||
- Slack: harden media fetch limits and Slack file URL validation. (#6639) Thanks @davidiach.
|
||||
- Lint: satisfy curly rule after import sorting. (#6310)
|
||||
- Process: resolve Windows `spawn()` failures for npm-family CLIs by appending `.cmd` when needed. (#5815) Thanks @thejhinvirtuoso.
|
||||
- Discord: resolve PluralKit proxied senders for allowlists and labels. (#5838) Thanks @thewilloftheshadow.
|
||||
- Tlon: add timeout to SSE client fetch calls (CWE-400). (#5926)
|
||||
- Memory search: L2-normalize local embedding vectors to fix semantic search. (#5332)
|
||||
- Agents: align embedded runner + typings with pi-coding-agent API updates (pi 0.51.0).
|
||||
- Agents: ensure OpenRouter attribution headers apply in the embedded runner.
|
||||
- Agents: cap context window resolution for compaction safeguard. (#6187) Thanks @iamEvanYT.
|
||||
- System prompt: resolve overrides and hint using session_status for current date/time. (#1897, #1928, #2108, #3677)
|
||||
- Agents: fix Pi prompt template argument syntax. (#6543)
|
||||
- Subagents: fix announce failover race (always emit lifecycle end; timeout=0 means no-timeout). (#6621)
|
||||
- Teams: gate media auth retries.
|
||||
- Telegram: restore draft streaming partials. (#5543) Thanks @obviyus.
|
||||
- Onboarding: friendlier Windows onboarding message. (#6242) Thanks @shanselman.
|
||||
- TUI: prevent crash when searching with digits in the model selector.
|
||||
- Agents: wire before_tool_call plugin hook into tool execution. (#6570, #6660) Thanks @ryancnelson.
|
||||
- Browser: secure Chrome extension relay CDP sessions.
|
||||
- Docker: use container port for gateway command instead of host port. (#5110) Thanks @mise42.
|
||||
- Docker: start gateway CMD by default for container deployments. (#6635) Thanks @kaizen403.
|
||||
- fix(lobster): block arbitrary exec via lobsterPath/cwd injection (GHSA-4mhr-g7xj-cg8j). (#5335) Thanks @vignesh07.
|
||||
- Security: sanitize WhatsApp accountId to prevent path traversal. (#4610)
|
||||
- Security: restrict MEDIA path extraction to prevent LFI. (#4930)
|
||||
- Security: validate message-tool filePath/path against sandbox root. (#6398)
|
||||
- Security: block LD*/DYLD* env overrides for host exec. (#4896) Thanks @HassanFleyah.
|
||||
- Security: harden web tool content wrapping + file parsing safeguards. (#4058) Thanks @VACInc.
|
||||
- Security: enforce Twitch `allowFrom` allowlist gating (deny non-allowlisted senders). Thanks @MegaManSec.
|
||||
|
||||
## 2026.1.31
|
||||
|
||||
### Changes
|
||||
|
||||
- Docs: onboarding/install/i18n/exec-approvals/Control UI/exe.dev/cacheRetention updates + misc nav/typos. (#3050, #3461, #4064, #4675, #4729, #4763, #5003, #5402, #5446, #5474, #5663, #5689, #5694, #5967, #6270, #6300, #6311, #6416, #6487, #6550, #6789)
|
||||
- Telegram: use shared pairing store. (#6127) Thanks @obviyus.
|
||||
- Agents: add OpenRouter app attribution headers. Thanks @alexanderatallah.
|
||||
- Agents: add system prompt safety guardrails. (#5445) Thanks @joshp123.
|
||||
- Agents: update pi-ai to 0.50.9 and rename cacheControlTtl -> cacheRetention (with back-compat mapping).
|
||||
- Agents: extend CreateAgentSessionOptions with systemPrompt/skills/contextFiles.
|
||||
- Agents: add tool policy conformance snapshot (no runtime behavior change). (#6011)
|
||||
- Auth: update MiniMax OAuth hint + portal auth note copy.
|
||||
- Discord: inherit thread parent bindings for routing. (#3892) Thanks @aerolalit.
|
||||
- Gateway: inject timestamps into agent and chat.send messages. (#3705) Thanks @conroywhitney, @CashWilliams.
|
||||
- Gateway: require TLS 1.3 minimum for TLS listeners. (#5970) Thanks @loganaden.
|
||||
- Web UI: refine chat layout + extend session active duration.
|
||||
- CI: add formal conformance + alias consistency checks. (#5723, #5807)
|
||||
|
||||
### Fixes
|
||||
|
||||
- Security: guard remote media fetches with SSRF protections (block private/localhost, DNS pinning).
|
||||
- Updates: clean stale global install rename dirs and extend gateway update timeouts to avoid npm ENOTEMPTY failures.
|
||||
- Plugins: validate plugin/hook install paths and reject traversal-like names.
|
||||
- Telegram: add download timeouts for file fetches. (#6914) Thanks @hclsys.
|
||||
- Telegram: enforce thread specs for DM vs forum sends. (#6833) Thanks @obviyus.
|
||||
- Streaming: flush block streaming on paragraph boundaries for newline chunking. (#7014)
|
||||
- Streaming: stabilize partial streaming filters.
|
||||
- Auto-reply: avoid referencing workspace files in /new greeting prompt. (#5706) Thanks @bravostation.
|
||||
- Tools: align tool execute adapters/signatures (legacy + parameter order + arg normalization).
|
||||
- Tools: treat `"*"` tool allowlist entries as valid to avoid spurious unknown-entry warnings.
|
||||
- Skills: update session-logs paths from .clawdbot to .openclaw. (#4502)
|
||||
- Slack: harden media fetch limits and Slack file URL validation. (#6639) Thanks @davidiach.
|
||||
- Lint: satisfy curly rule after import sorting. (#6310)
|
||||
- Process: resolve Windows `spawn()` failures for npm-family CLIs by appending `.cmd` when needed. (#5815) Thanks @thejhinvirtuoso.
|
||||
- Discord: resolve PluralKit proxied senders for allowlists and labels. (#5838) Thanks @thewilloftheshadow.
|
||||
- Tlon: add timeout to SSE client fetch calls (CWE-400). (#5926)
|
||||
- Memory search: L2-normalize local embedding vectors to fix semantic search. (#5332)
|
||||
- Agents: align embedded runner + typings with pi-coding-agent API updates (pi 0.51.0).
|
||||
- Agents: ensure OpenRouter attribution headers apply in the embedded runner.
|
||||
- Agents: cap context window resolution for compaction safeguard. (#6187) Thanks @iamEvanYT.
|
||||
- System prompt: resolve overrides and hint using session_status for current date/time. (#1897, #1928, #2108, #3677)
|
||||
- Agents: fix Pi prompt template argument syntax. (#6543)
|
||||
- Subagents: fix announce failover race (always emit lifecycle end; timeout=0 means no-timeout). (#6621)
|
||||
- Teams: gate media auth retries.
|
||||
- Telegram: restore draft streaming partials. (#5543) Thanks @obviyus.
|
||||
- Onboarding: friendlier Windows onboarding message. (#6242) Thanks @shanselman.
|
||||
- TUI: prevent crash when searching with digits in the model selector.
|
||||
- Agents: wire before_tool_call plugin hook into tool execution. (#6570, #6660) Thanks @ryancnelson.
|
||||
- Browser: secure Chrome extension relay CDP sessions.
|
||||
- Docker: use container port for gateway command instead of host port. (#5110) Thanks @mise42.
|
||||
- Docker: start gateway CMD by default for container deployments. (#6635) Thanks @kaizen403.
|
||||
- fix(lobster): block arbitrary exec via lobsterPath/cwd injection (GHSA-4mhr-g7xj-cg8j). (#5335) Thanks @vignesh07.
|
||||
- Security: sanitize WhatsApp accountId to prevent path traversal. (#4610)
|
||||
- Security: restrict MEDIA path extraction to prevent LFI. (#4930)
|
||||
- Security: validate message-tool filePath/path against sandbox root. (#6398)
|
||||
- Security: block LD*/DYLD* env overrides for host exec. (#4896) Thanks @HassanFleyah.
|
||||
- Security: harden web tool content wrapping + file parsing safeguards. (#4058) Thanks @VACInc.
|
||||
- Security: enforce Twitch `allowFrom` allowlist gating (deny non-allowlisted senders). Thanks @MegaManSec.
|
||||
|
||||
## 2026.1.30
|
||||
|
||||
### Changes
|
||||
|
||||
- CLI: add `completion` command (Zsh/Bash/PowerShell/Fish) and auto-setup during postinstall/onboarding.
|
||||
- CLI: add per-agent `models status` (`--agent` filter). (#4780) Thanks @jlowin.
|
||||
- Agents: add Kimi K2.5 to the synthetic model catalog. (#4407) Thanks @manikv12.
|
||||
- Auth: switch Kimi Coding to built-in provider; normalize OAuth profile email.
|
||||
- Auth: add MiniMax OAuth plugin + onboarding option. (#4521) Thanks @Maosghoul.
|
||||
- Agents: update pi SDK/API usage and dependencies.
|
||||
- Web UI: refresh sessions after chat commands and improve session display names.
|
||||
- Build: move TypeScript builds to `tsdown` + `tsgo` (faster builds, CI typechecks), update tsconfig target, and clean up lint rules.
|
||||
- Build: align npm tar override and bin metadata so the `openclaw` CLI entrypoint is preserved in npm publishes.
|
||||
- Docs: add pi/pi-dev docs and update OpenClaw branding + install links.
|
||||
- Docker E2E: stabilize gateway readiness, plugin installs/manifests, and cleanup/doctor switch entrypoint checks.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Security: restrict local path extraction in media parser to prevent LFI. (#4880)
|
||||
- Gateway: prevent token defaults from becoming the literal "undefined". (#4873) Thanks @Hisleren.
|
||||
- Control UI: fix assets resolution for npm global installs. (#4909) Thanks @YuriNachos.
|
||||
- macOS: avoid stderr pipe backpressure in gateway discovery. (#3304) Thanks @abhijeet117.
|
||||
- Telegram: normalize account token lookup for non-normalized IDs. (#5055) Thanks @jasonsschin.
|
||||
- Telegram: preserve delivery thread fallback and fix threadId handling in delivery context.
|
||||
- Telegram: fix HTML nesting for overlapping styles/links. (#4578) Thanks @ThanhNguyxn.
|
||||
- Telegram: accept numeric messageId/chatId in react actions. (#4533) Thanks @Ayush10.
|
||||
- Telegram: honor per-account proxy dispatcher via undici fetch. (#4456) Thanks @spiceoogway.
|
||||
- Telegram: scope skill commands to bound agent per bot. (#4360) Thanks @robhparker.
|
||||
- BlueBubbles: debounce by messageId to preserve attachments in text+image messages. (#4984)
|
||||
- Routing: prefer requesterOrigin over stale session entries for sub-agent announce delivery. (#4957)
|
||||
- Extensions: restore embedded extension discovery typings.
|
||||
- CLI: fix `tui:dev` port resolution.
|
||||
- LINE: fix status command TypeError. (#4651)
|
||||
- OAuth: skip expired-token warnings when refresh tokens are still valid. (#4593)
|
||||
- Build: skip redundant UI install step in Dockerfile. (#4584) Thanks @obviyus.
|
||||
|
||||
## 2026.1.29
|
||||
|
||||
### Changes
|
||||
|
||||
- Rebrand: rename the npm package/CLI to `openclaw`, add a `openclaw` compatibility shim, and move extensions to the `@openclaw/*` scope.
|
||||
- Onboarding: strengthen security warning copy for beta + access control expectations.
|
||||
- Onboarding: add Venice API key to non-interactive flow. (#1893) Thanks @jonisjongithub.
|
||||
@@ -41,9 +358,7 @@ Status: stable.
|
||||
- Routing: add per-account DM session scope and document multi-account isolation. (#3095) Thanks @jarvis-sam.
|
||||
- Routing: precompile session key regexes. (#1697) Thanks @Ray0907.
|
||||
- CLI: use Node's module compile cache for faster startup. (#2808) Thanks @pi0.
|
||||
- CLI: add per-agent model status and auth order scoping. (#4780) Thanks @jlowin.
|
||||
- Auth: show copyable Google auth URL after ASCII prompt. (#1787) Thanks @robbyczgw-cla.
|
||||
- Agents: add Kimi K2.5 to the synthetic model catalog. (#4407) Thanks @manikv12.
|
||||
- TUI: avoid width overflow when rendering selection lists. (#1686) Thanks @mossein.
|
||||
- macOS: finish OpenClaw app rename for macOS sources, bundle identifiers, and shared kit paths. (#2844) Thanks @fal3.
|
||||
- Branding: update launchd labels, mobile bundle IDs, and logging subsystems to bot.molt (legacy bundle ID migrations). Thanks @thewilloftheshadow.
|
||||
@@ -69,18 +384,15 @@ Status: stable.
|
||||
- Docs: credit both contributors for Control UI refresh. (#1852) Thanks @EnzeD.
|
||||
- Docs: keep docs header sticky so navbar stays visible while scrolling. (#2445) Thanks @chenyuan99.
|
||||
- Docs: update exe.dev install instructions. (#https://github.com/openclaw/openclaw/pull/3047) Thanks @zackerthescar.
|
||||
- Build: skip redundant UI install step in the Dockerfile. (#4584) Thanks @obviyus.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Gateway auth mode "none" is removed; gateway now requires token/password (Tailscale Serve identity still allowed).
|
||||
|
||||
### Fixes
|
||||
- Infra: resolve Control UI assets for npm global installs. (#4909) Thanks @YuriNachos.
|
||||
- Gateway: prevent blank token prompts from storing "undefined". (#4873) Thanks @Hisleren.
|
||||
- Telegram: use undici fetch for per-account proxy dispatcher. (#4456) Thanks @spiceoogway.
|
||||
- Telegram: fix HTML nesting for overlapping styles and links. (#4578) Thanks @ThanhNguyxn.
|
||||
|
||||
- Skills: update session-logs paths to use ~/.openclaw. (#4502) Thanks @bonald.
|
||||
- Telegram: avoid silent empty replies by tracking normalization skips before fallback. (#3796)
|
||||
- Telegram: accept numeric messageId/chatId in react action and honor channelId fallback. (#4533) Thanks @Ayush10.
|
||||
- Telegram: scope native skill commands to bound agent per bot. (#4360) Thanks @robhparker.
|
||||
- Mentions: honor mentionPatterns even when explicit mentions are present. (#3303) Thanks @HirokiKobayashi-R.
|
||||
- Discord: restore username directory lookup in target resolution. (#3131) Thanks @bonald.
|
||||
- Agents: align MiniMax base URL test expectation with default provider config. (#3131) Thanks @bonald.
|
||||
@@ -92,7 +404,6 @@ Status: stable.
|
||||
- TTS: read OPENAI_TTS_BASE_URL at runtime instead of module load to honor config.env. (#3341) Thanks @hclsys.
|
||||
- macOS: auto-scroll to bottom when sending a new message while scrolled up. (#2471) Thanks @kennyklee.
|
||||
- Web UI: auto-expand the chat compose textarea while typing (with sensible max height). (#2950) Thanks @shivamraut101.
|
||||
- Web UI: refresh sessions after queued /new or /reset commands once the run completes.
|
||||
- Gateway: prevent crashes on transient network errors (fetch failures, timeouts, DNS). Added fatal error detection to only exit on truly critical errors. Fixes #2895, #2879, #2873. (#2980) Thanks @elliotsecops.
|
||||
- Agents: guard channel tool listActions to avoid plugin crashes. (#2859) Thanks @mbelinky.
|
||||
- Discord: stop resolveDiscordTarget from passing directory params into messaging target parsers. Fixes #3167. Thanks @thewilloftheshadow.
|
||||
@@ -133,6 +444,7 @@ Status: stable.
|
||||
## 2026.1.24-3
|
||||
|
||||
### Fixes
|
||||
|
||||
- Slack: fix image downloads failing due to missing Authorization header on cross-origin redirects. (#1936) Thanks @sanderhelgesen.
|
||||
- Gateway: harden reverse proxy handling for local-client detection and unauthenticated proxied connects. (#1795) Thanks @orlyjamie.
|
||||
- Security audit: flag loopback Control UI with auth disabled as critical. (#1795) Thanks @orlyjamie.
|
||||
@@ -141,16 +453,19 @@ Status: stable.
|
||||
## 2026.1.24-2
|
||||
|
||||
### Fixes
|
||||
|
||||
- Packaging: include dist/link-understanding output in npm tarball (fixes missing apply.js import on install).
|
||||
|
||||
## 2026.1.24-1
|
||||
|
||||
### Fixes
|
||||
|
||||
- Packaging: include dist/shared output in npm tarball (fixes missing reasoning-tags import on install).
|
||||
|
||||
## 2026.1.24
|
||||
|
||||
### Highlights
|
||||
|
||||
- Providers: Ollama discovery + docs; Venice guide upgrades + cross-links. (#1606) Thanks @abhaymundhara. https://docs.openclaw.ai/providers/ollama https://docs.openclaw.ai/providers/venice
|
||||
- Channels: LINE plugin (Messaging API) with rich replies + quick replies. (#1630) Thanks @plum-dawg.
|
||||
- TTS: Edge fallback (keyless) + `/tts` auto modes. (#1668, #1667) Thanks @steipete, @sebslight. https://docs.openclaw.ai/tts
|
||||
@@ -158,6 +473,7 @@ Status: stable.
|
||||
- Telegram: DM topics as separate sessions + outbound link preview toggle. (#1597, #1700) Thanks @rohannagpal, @zerone0x. https://docs.openclaw.ai/channels/telegram
|
||||
|
||||
### Changes
|
||||
|
||||
- Channels: add LINE plugin (Messaging API) with rich replies, quick replies, and plugin HTTP registry. (#1630) Thanks @plum-dawg.
|
||||
- TTS: add Edge TTS provider fallback, defaulting to keyless Edge with MP3 retry on format failures. (#1668) Thanks @steipete. https://docs.openclaw.ai/tts
|
||||
- TTS: add auto mode enum (off/always/inbound/tagged) with per-session `/tts` override. (#1667) Thanks @sebslight. https://docs.openclaw.ai/tts
|
||||
@@ -176,6 +492,7 @@ Status: stable.
|
||||
- Dev: add prek pre-commit hooks + dependabot config for weekly updates. (#1720) Thanks @dguido.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Web UI: fix config/debug layout overflow, scrolling, and code block sizing. (#1715) Thanks @saipreetham589.
|
||||
- Web UI: show Stop button during active runs, swap back to New session when idle. (#1664) Thanks @ndbroadbent.
|
||||
- Web UI: clear stale disconnect banners on reconnect; allow form saves with unsupported schema paths but block missing schema. (#1707) Thanks @Glucksberg.
|
||||
@@ -219,11 +536,13 @@ Status: stable.
|
||||
## 2026.1.23-1
|
||||
|
||||
### Fixes
|
||||
|
||||
- Packaging: include dist/tts output in npm tarball (fixes missing dist/tts/tts.js).
|
||||
|
||||
## 2026.1.23
|
||||
|
||||
### Highlights
|
||||
|
||||
- TTS: move Telegram TTS into core + enable model-driven TTS tags by default for expressive audio replies. (#1559) Thanks @Glucksberg. https://docs.openclaw.ai/tts
|
||||
- Gateway: add `/tools/invoke` HTTP endpoint for direct tool calls (auth + tool policy enforced). (#1575) Thanks @vignesh07. https://docs.openclaw.ai/gateway/tools-invoke-http-api
|
||||
- Heartbeat: per-channel visibility controls (OK/alerts/indicator). (#1452) Thanks @dlauer. https://docs.openclaw.ai/gateway/heartbeat
|
||||
@@ -231,6 +550,7 @@ Status: stable.
|
||||
- Channels: add Tlon/Urbit channel plugin (DMs, group mentions, thread replies). (#1544) Thanks @wca4a. https://docs.openclaw.ai/channels/tlon
|
||||
|
||||
### Changes
|
||||
|
||||
- Channels: allow per-group tool allow/deny policies across built-in + plugin channels. (#1546) Thanks @adam91holt. https://docs.openclaw.ai/multi-agent-sandbox-tools
|
||||
- Agents: add Bedrock auto-discovery defaults + config overrides. (#1553) Thanks @fal3. https://docs.openclaw.ai/bedrock
|
||||
- CLI: add `openclaw system` for system events + heartbeat controls; remove standalone `wake`. (commit 71203829d) https://docs.openclaw.ai/cli/system
|
||||
@@ -245,6 +565,7 @@ Status: stable.
|
||||
- Docs: clarify HEARTBEAT.md empty file skips heartbeats, missing file still runs. (#1535) Thanks @JustYannicc. https://docs.openclaw.ai/gateway/heartbeat
|
||||
|
||||
### Fixes
|
||||
|
||||
- Sessions: accept non-UUID sessionIds for history/send/status while preserving agent scoping. (#1518)
|
||||
- Heartbeat: accept plugin channel ids for heartbeat target validation + UI hints.
|
||||
- Messaging/Sessions: mirror outbound sends into target session keys (threads + dmScope), create session entries on send, and normalize session key casing. (#1520, commit 4b6cdd1d3)
|
||||
@@ -283,6 +604,7 @@ Status: stable.
|
||||
## 2026.1.22
|
||||
|
||||
### Changes
|
||||
|
||||
- Highlight: Compaction safeguard now uses adaptive chunking, progressive fallback, and UI status + retries. (#1466) Thanks @dlauer.
|
||||
- Providers: add Antigravity usage tracking to status output. (#1490) Thanks @patelhiren.
|
||||
- Slack: add chat-type reply threading overrides via `replyToModeByChatType`. (#1442) Thanks @stefangalescu.
|
||||
@@ -290,6 +612,7 @@ Status: stable.
|
||||
- Onboarding: add hatch choice (TUI/Web/Later), token explainer, background dashboard seed on macOS, and showcase link.
|
||||
|
||||
### Fixes
|
||||
|
||||
- BlueBubbles: stop typing indicator on idle/no-reply. (#1439) Thanks @Nicell.
|
||||
- Message tool: keep path/filePath as-is for send; hydrate buffers only for sendAttachment. (#1444) Thanks @hopyky.
|
||||
- Auto-reply: only report a model switch when session state is available. (#1465) Thanks @robbyczgw-cla.
|
||||
@@ -320,12 +643,14 @@ Status: stable.
|
||||
## 2026.1.21-2
|
||||
|
||||
### Fixes
|
||||
|
||||
- Control UI: ignore bootstrap identity placeholder text for avatar values and fall back to the default avatar. https://docs.openclaw.ai/cli/agents https://docs.openclaw.ai/web/control-ui
|
||||
- Slack: remove deprecated `filetype` field from `files.uploadV2` to eliminate API warnings. (#1447)
|
||||
|
||||
## 2026.1.21
|
||||
|
||||
### Changes
|
||||
|
||||
- Highlight: Lobster optional plugin tool for typed workflows + approval gates. https://docs.openclaw.ai/tools/lobster
|
||||
- Lobster: allow workflow file args via `argsJson` in the plugin tool. https://docs.openclaw.ai/tools/lobster
|
||||
- Heartbeat: allow running heartbeats in an explicit session key. (#1256) Thanks @zknicker.
|
||||
@@ -348,10 +673,12 @@ Status: stable.
|
||||
- Docs: add per-message Gmail search example for gog. (#1220) Thanks @mbelinky.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Control UI now rejects insecure HTTP without device identity by default. Use HTTPS (Tailscale Serve) or set `gateway.controlUi.allowInsecureAuth: true` to allow token-only auth. https://docs.openclaw.ai/web/control-ui#insecure-http
|
||||
- **BREAKING:** Envelope and system event timestamps now default to host-local time (was UTC) so agents don’t have to constantly convert.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Nodes/macOS: prompt on allowlist miss for node exec approvals, persist allowlist decisions, and flatten node invoke errors. (#1394) Thanks @ngutman.
|
||||
- Gateway: keep auto bind loopback-first and add explicit tailnet binding to avoid Tailscale taking over local UI. (#1380)
|
||||
- Memory: prevent CLI hangs by deferring vector probes, adding sqlite-vec/embedding timeouts, and showing sync progress early.
|
||||
@@ -374,6 +701,7 @@ Status: stable.
|
||||
## 2026.1.20
|
||||
|
||||
### Changes
|
||||
|
||||
- Control UI: add copy-as-markdown with error feedback. (#1345) https://docs.openclaw.ai/web/control-ui
|
||||
- Control UI: drop the legacy list view. (#1345) https://docs.openclaw.ai/web/control-ui
|
||||
- TUI: add syntax highlighting for code blocks. (#1200) https://docs.openclaw.ai/tui
|
||||
@@ -452,9 +780,11 @@ Status: stable.
|
||||
- Swabble: use the tagged Commander Swift package release.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Reject invalid/unknown config entries and refuse to start the gateway for safety. Run `openclaw doctor --fix` to repair, then update plugins (`openclaw plugins update`) if you use any.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Discovery: shorten Bonjour DNS-SD service type to `_moltbot-gw._tcp` and update discovery clients/docs.
|
||||
- Diagnostics: export OTLP logs, correct queue depth tracking, and document message-flow telemetry.
|
||||
- Diagnostics: emit message-flow diagnostics across channels via shared dispatch. (#1244)
|
||||
@@ -554,20 +884,23 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
## 2026.1.16-2
|
||||
|
||||
### Changes
|
||||
|
||||
- CLI: stamp build commit into dist metadata so banners show the commit in npm installs.
|
||||
- CLI: close memory manager after memory commands to avoid hanging processes. (#1127) — thanks @NicholasSpisak.
|
||||
|
||||
## 2026.1.16-1
|
||||
|
||||
### Highlights
|
||||
|
||||
- Hooks: add hooks system with bundled hooks, CLI tooling, and docs. (#1028) — thanks @ThomsenDrake. https://docs.openclaw.ai/hooks
|
||||
- Media: add inbound media understanding (image/audio/video) with provider + CLI fallbacks. https://docs.openclaw.ai/nodes/media-understanding
|
||||
- Plugins: add Zalo Personal plugin (`@openclaw/zalouser`) and unify channel directory for plugins. (#1032) — thanks @suminhthanh. https://docs.openclaw.ai/plugins/zalouser
|
||||
- Models: add Vercel AI Gateway auth choice + onboarding updates. (#1016) — thanks @timolins. https://docs.openclaw.ai/providers/vercel-ai-gateway
|
||||
- Sessions: add `session.identityLinks` for cross-platform DM session li nking. (#1033) — thanks @thewilloftheshadow. https://docs.openclaw.ai/concepts/session
|
||||
- Sessions: add `session.identityLinks` for cross-platform DM session li nking. (#1033) — thanks @thewilloftheshadow. https://docs.openclaw.ai/concepts/session
|
||||
- Web search: add `country`/`language` parameters (schema + Brave API) and docs. (#1046) — thanks @YuriNachos. https://docs.openclaw.ai/tools/web
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** `openclaw message` and message tool now require `target` (dropping `to`/`channelId` for destinations). (#1034) — thanks @tobalsan.
|
||||
- **BREAKING:** Channel auth now prefers config over env for Discord/Telegram/Matrix (env is fallback only). (#1040) — thanks @thewilloftheshadow.
|
||||
- **BREAKING:** Drop legacy `chatType: "room"` support; use `chatType: "channel"`.
|
||||
@@ -576,6 +909,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- **BREAKING:** `openclaw plugins install <path>` now copies into `~/.openclaw/extensions` (use `--link` to keep path-based loading).
|
||||
|
||||
### Changes
|
||||
|
||||
- Plugins: ship bundled plugins disabled by default and allow overrides by installed versions. (#1066) — thanks @ItzR3NO.
|
||||
- Plugins: add bundled Antigravity + Gemini CLI OAuth + Copilot Proxy provider plugins. (#1066) — thanks @ItzR3NO.
|
||||
- Tools: improve `web_fetch` extraction using Readability (with fallback).
|
||||
@@ -611,6 +945,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Plugins: add zip installs and `--link` to avoid copying local paths.
|
||||
|
||||
### Fixes
|
||||
|
||||
- macOS: drain subprocess pipes before waiting to avoid deadlocks. (#1081) — thanks @thesash.
|
||||
- Verbose: wrap tool summaries/output in markdown only for markdown-capable channels.
|
||||
- Tools: include provider/session context in elevated exec denial errors.
|
||||
@@ -667,17 +1002,20 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
## 2026.1.15
|
||||
|
||||
### Highlights
|
||||
|
||||
- Plugins: add provider auth registry + `openclaw models auth login` for plugin-driven OAuth/API key flows.
|
||||
- Browser: improve remote CDP/Browserless support (auth passthrough, `wss` upgrade, timeouts, clearer errors).
|
||||
- Heartbeat: per-agent configuration + 24h duplicate suppression. (#980) — thanks @voidserf.
|
||||
- Security: audit warns on weak model tiers; app nodes store auth tokens encrypted (Keychain/SecurePrefs).
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** iOS minimum version is now 18.0 to support Textual markdown rendering in native chat. (#702)
|
||||
- **BREAKING:** Microsoft Teams is now a plugin; install `@openclaw/msteams` via `openclaw plugins install @openclaw/msteams`.
|
||||
- **BREAKING:** Channel auth now prefers config over env for Discord/Telegram/Matrix (env is fallback only). (#1040) — thanks @thewilloftheshadow.
|
||||
|
||||
### Changes
|
||||
|
||||
- UI/Apps: move channel/config settings to schema-driven forms and rename Connections → Channels. (#1040) — thanks @thewilloftheshadow.
|
||||
- CLI: set process titles to `openclaw-<command>` for clearer process listings.
|
||||
- CLI/macOS: sync remote SSH target/identity to config and let `gateway status` auto-infer SSH targets (ssh-config aware).
|
||||
@@ -717,6 +1055,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Discord: allow emoji/sticker uploads + channel actions in config defaults. (#870) — thanks @JDIVE.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Messages: make `/stop` clear queued followups and pending session lane work for a hard abort.
|
||||
- Messages: make `/stop` abort active sub-agent runs spawned from the requester session and report how many were stopped.
|
||||
- WhatsApp: report linked status consistently in channel status. (#1050) — thanks @YuriNachos.
|
||||
@@ -753,12 +1092,14 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
## 2026.1.14-1
|
||||
|
||||
### Highlights
|
||||
|
||||
- Web search: `web_search`/`web_fetch` tools (Brave API) + first-time setup in onboarding/configure.
|
||||
- Browser control: Chrome extension relay takeover mode + remote browser control support.
|
||||
- Plugins: channel plugins (gateway HTTP hooks) + Zalo plugin + onboarding install flow. (#854) — thanks @longmaba.
|
||||
- Security: expanded `openclaw security audit` (+ `--fix`), detect-secrets CI scan, and a `SECURITY.md` reporting policy.
|
||||
|
||||
### Changes
|
||||
|
||||
- Docs: clarify per-agent auth stores, sandboxed skill binaries, and elevated semantics.
|
||||
- Docs: add FAQ entries for missing provider auth after adding agents and Gemini thinking signature errors.
|
||||
- Agents: add optional auth-profile copy prompt on `agents add` and improve auth error messaging.
|
||||
@@ -775,6 +1116,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Browser: add Chrome extension relay takeover mode (toolbar button), plus `openclaw browser extension install/path` and remote browser control (standalone server + token auth).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Sessions: refactor session store updates to lock + mutate per-entry, add chat.inject, and harden subagent cleanup flow. (#944) — thanks @tyler6204.
|
||||
- Browser: add tests for snapshot labels/efficient query params and labeled image responses.
|
||||
- Google: downgrade unsigned thinking blocks before send to avoid missing signature errors.
|
||||
@@ -796,6 +1138,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
## 2026.1.14
|
||||
|
||||
### Changes
|
||||
|
||||
- Usage: add MiniMax coding plan usage tracking.
|
||||
- Auth: label Claude Code CLI auth options. (#915) — thanks @SeanZoR.
|
||||
- Docs: standardize Claude Code CLI naming across docs and prompts. (follow-up to #915)
|
||||
@@ -803,14 +1146,16 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Config: add `channels.<provider>.configWrites` gating for channel-initiated config writes; migrate Slack channel IDs.
|
||||
|
||||
### Fixes
|
||||
- Mac: pass auth token/password to dashboard URL for authenticated access. (#918) — thanks @rahthakor.
|
||||
- UI: use application-defined WebSocket close code (browser compatibility). (#918) — thanks @rahthakor.
|
||||
|
||||
- Mac: pass auth token/password to dashboard URL for authenticated access. (#918) — thanks @rahthakor.
|
||||
- UI: use application-defined WebSocket close code (browser compatibility). (#918) — thanks @rahthakor.
|
||||
- TUI: render picker overlays via the overlay stack so /models and /settings display. (#921) — thanks @grizzdank.
|
||||
- TUI: add a bright spinner + elapsed time in the status line for send/stream/run states.
|
||||
- TUI: show LLM error messages (rate limits, auth, etc.) instead of `(no output)`.
|
||||
- Gateway/Dev: ensure `pnpm gateway:dev` always uses the dev profile config + state (`~/.openclaw-dev`).
|
||||
|
||||
#### Agents / Auth / Tools / Sandbox
|
||||
|
||||
- Agents: make user time zone and 24-hour time explicit in the system prompt. (#859) — thanks @CashWilliams.
|
||||
- Agents: strip downgraded tool call text without eating adjacent replies and filter thinking-tag leaks. (#905) — thanks @erikpr1994.
|
||||
- Agents: cap tool call IDs for OpenAI/OpenRouter to avoid request rejections. (#875) — thanks @j1philli.
|
||||
@@ -823,6 +1168,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Google: downgrade unsigned thinking blocks before send to avoid missing signature errors.
|
||||
|
||||
#### macOS / Apps
|
||||
|
||||
- macOS: ensure launchd log directory exists with a test-only override. (#909) — thanks @roshanasingh4.
|
||||
- macOS: format ConnectionsStore config to satisfy SwiftFormat lint. (#852) — thanks @mneves75.
|
||||
- macOS: pass auth token/password to dashboard URL for authenticated access. (#918) — thanks @rahthakor.
|
||||
@@ -842,12 +1188,14 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
## 2026.1.13
|
||||
|
||||
### Fixes
|
||||
|
||||
- Postinstall: treat already-applied pnpm patches as no-ops to avoid npm/bun install failures.
|
||||
- Packaging: pin `@mariozechner/pi-ai` to 0.45.7 and refresh patched dependency to match npm resolution.
|
||||
|
||||
## 2026.1.12-2
|
||||
|
||||
### Fixes
|
||||
|
||||
- Packaging: include `dist/memory/**` in the npm tarball (fixes `ERR_MODULE_NOT_FOUND` for `dist/memory/index.js`).
|
||||
- Agents: persist sub-agent registry across gateway restarts and resume announce flow safely. (#831) — thanks @roshanasingh4.
|
||||
- Agents: strip invalid Gemini thought signatures from OpenRouter history to avoid 400s. (#841, #845) — thanks @MatthieuBizien.
|
||||
@@ -855,11 +1203,13 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
## 2026.1.12-1
|
||||
|
||||
### Fixes
|
||||
|
||||
- Packaging: include `dist/channels/**` in the npm tarball (fixes `ERR_MODULE_NOT_FOUND` for `dist/channels/registry.js`).
|
||||
|
||||
## 2026.1.12
|
||||
|
||||
### Highlights
|
||||
|
||||
- **BREAKING:** rename chat “providers” (Slack/Telegram/WhatsApp/…) to **channels** across CLI/RPC/config; legacy config keys auto-migrate on load (and are written back as `channels.*`).
|
||||
- Memory: add vector search for agent memories (Markdown-only) with SQLite index, chunking, lazy sync + file watch, and per-agent enablement/fallback.
|
||||
- Plugins: restore full voice-call plugin parity (Telnyx/Twilio, streaming, inbound policies, tools/CLI).
|
||||
@@ -868,6 +1218,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Agents: add compaction mode config with optional safeguard summarization and per-agent model fallbacks. (#700) — thanks @thewilloftheshadow; (#583) — thanks @mitschabaude-bot.
|
||||
|
||||
### New & Improved
|
||||
|
||||
- Memory: add custom OpenAI-compatible embedding endpoints; support OpenAI/local `node-llama-cpp` embeddings with per-agent overrides and provider metadata in tools/CLI. (#819) — thanks @mukhtharcm.
|
||||
- Memory: new `openclaw memory` CLI plus `memory_search`/`memory_get` tools with snippets + line ranges; index stored under `~/.openclaw/memory/{agentId}.sqlite` with watch-on-by-default.
|
||||
- Agents: strengthen memory recall guidance; make workspace bootstrap truncation configurable (default 20k) with warnings; add default sub-agent model config.
|
||||
@@ -881,9 +1232,11 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Heartbeat: default `ackMaxChars` to 300 so short `HEARTBEAT_OK` replies stay internal.
|
||||
|
||||
### Installer
|
||||
|
||||
- Install: run `openclaw doctor --non-interactive` after git installs/updates and nudge daemon restarts when detected.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Doctor: warn on pnpm workspace mismatches, missing Control UI assets, and missing tsx binaries; offer UI rebuilds.
|
||||
- Tools: apply global tool allow/deny even when agent-specific tool policy is set.
|
||||
- Models/Providers: treat credential validation failures as auth errors to trigger fallback; normalize `${ENV_VAR}` apiKey values and auto-fill missing provider keys; preserve explicit GitHub Copilot provider config + agent-dir auth profiles. (#822) — thanks @sebslight; (#705) — thanks @TAGOOZ.
|
||||
@@ -908,6 +1261,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Connections UI: polish multi-account account cards. (#816) — thanks @steipete.
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Dependencies: bump Pi packages to 0.45.3 and refresh patched pi-ai.
|
||||
- Testing: update Vitest + browser-playwright to 4.0.17.
|
||||
- Docs: add Amazon Bedrock provider notes and link from models/FAQ.
|
||||
@@ -915,12 +1269,14 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
## 2026.1.11
|
||||
|
||||
### Highlights
|
||||
|
||||
- Plugins are now first-class: loader + CLI management, plus the new Voice Call plugin.
|
||||
- Config: modular `$include` support for split config files. (#731) — thanks @pasogott.
|
||||
- Agents/Pi: reserve compaction headroom so pre-compaction memory writes can run before auto-compaction.
|
||||
- Agents: automatic pre-compaction memory flush turn to store durable memories before compaction.
|
||||
|
||||
### Changes
|
||||
|
||||
- CLI/Onboarding: simplify MiniMax auth choice to a single M2.1 option.
|
||||
- CLI: configure section selection now loops until Continue.
|
||||
- Docs: explain MiniMax vs MiniMax Lightning (speed vs cost) and restore LM Studio example.
|
||||
@@ -956,6 +1312,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- macOS: remove the attach-only gateway setting; local mode now always manages launchd while still attaching to an existing gateway if present.
|
||||
|
||||
### Installer
|
||||
|
||||
- Postinstall: replace `git apply` with builtin JS patcher (works npm/pnpm/bun; no git dependency) plus regression tests.
|
||||
- Postinstall: skip pnpm patch fallback when the new patcher is active.
|
||||
- Installer tests: add root+non-root docker smokes, CI workflow to fetch openclaw.ai scripts and run install sh/cli with onboarding skipped.
|
||||
@@ -964,6 +1321,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Installer UX: add `--install-method git|npm` and auto-detect source checkouts (prompt to update git checkout vs migrate to npm).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Models/Onboarding: configure MiniMax (minimax.io) via Anthropic-compatible `/anthropic` endpoint by default (keep `minimax-api` as a legacy alias).
|
||||
- Models: normalize Gemini 3 Pro/Flash IDs to preview names for live model lookups. (#769) — thanks @steipete.
|
||||
- CLI: fix guardCancel typing for configure prompts. (#769) — thanks @steipete.
|
||||
@@ -1003,12 +1361,14 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
## 2026.1.10
|
||||
|
||||
### Highlights
|
||||
|
||||
- CLI: `openclaw status` now table-based + shows OS/update/gateway/daemon/agents/sessions; `status --all` adds a full read-only debug report (tables, log tails, Tailscale summary, and scan progress via OSC-9 + spinner).
|
||||
- CLI Backends: add Codex CLI fallback with resume support (text output) and JSONL parsing for new runs, plus a live CLI resume probe.
|
||||
- CLI: add `openclaw update` (safe-ish git checkout update) + `--update` shorthand. (#673) — thanks @fm1randa.
|
||||
- Gateway: add OpenAI-compatible `/v1/chat/completions` HTTP endpoint (auth, SSE streaming, per-agent routing). (#680).
|
||||
|
||||
### Changes
|
||||
|
||||
- Onboarding/Models: add first-class Z.AI (GLM) auth choice (`zai-api-key`) + `--zai-api-key` flag.
|
||||
- CLI/Onboarding: add OpenRouter API key auth option in configure/onboard. (#703) — thanks @mteam88.
|
||||
- Agents: add human-delay pacing between block replies (modes: off/natural/custom, per-agent configurable). (#446) — thanks @tony-freedomology.
|
||||
@@ -1022,6 +1382,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Docker: allow optional home volume + extra bind mounts in `docker-setup.sh`. (#679) — thanks @gabriel-trigo.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Auto-reply: suppress draft/typing streaming for `NO_REPLY` (silent system ops) so it doesn’t leak partial output.
|
||||
- CLI/Status: expand tables to full terminal width; clarify provider setup vs runtime warnings; richer per-provider detail; token previews in `status` while keeping `status --all` redacted; add troubleshooting link footer; keep log tails pasteable; show gateway auth used when reachable; surface provider runtime errors (Signal/iMessage/Slack); harden `tailscale status --json` parsing; make `status --all` scan progress determinate; and replace the footer with a 3-line “Next steps” recommendation (share/debug/probe).
|
||||
- CLI/Gateway: clarify that `openclaw gateway status` reports RPC health (connect + RPC) and shows RPC failures separately from connect failures.
|
||||
@@ -1093,10 +1454,10 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Agents: repair session transcripts by dropping duplicate tool results across the whole history (unblocks Anthropic-compatible APIs after retries).
|
||||
- Tests/Live: reset the gateway session between model runs to avoid cross-provider transcript incompatibilities (notably OpenAI Responses reasoning replay rules).
|
||||
|
||||
|
||||
## 2026.1.9
|
||||
|
||||
### Highlights
|
||||
|
||||
- Microsoft Teams provider: polling, attachments, outbound CLI send, per-channel policy.
|
||||
- Models/Auth expansion: OpenCode Zen + MiniMax API onboarding; token auth profiles + auth order; OAuth health in doctor/status.
|
||||
- CLI/Gateway UX: message subcommands, gateway discover/status/SSH, /config + /debug, sandbox CLI.
|
||||
@@ -1105,10 +1466,12 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Control UI/TUI: queued messages, session links, reasoning view, mobile polish, logs UX.
|
||||
|
||||
### Breaking
|
||||
|
||||
- CLI: `openclaw message` now subcommands (`message send|poll|...`) and requires `--provider` unless only one provider configured.
|
||||
- Commands/Tools: `/restart` and gateway restart tool disabled by default; enable with `commands.restart=true`.
|
||||
|
||||
### New Features and Changes
|
||||
|
||||
- Models/Auth: OpenCode Zen onboarding (#623) — thanks @magimetal; MiniMax Anthropic-compatible API + hosted onboarding (#590, #495) — thanks @mneves75, @tobiasbischoff.
|
||||
- Models/Auth: setup-token + token auth profiles; `openclaw models auth order {get,set,clear}`; per-agent auth candidates in `/model status`; OAuth expiry checks in doctor/status.
|
||||
- Agent/System: claude-cli runner; `session_status` tool (and sandbox allow); adaptive context pruning default; system prompt messaging guidance + no auto self-update; eligible skills list injection; sub-agent context trimmed.
|
||||
@@ -1130,6 +1493,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Apps/Branding: refreshed iOS/Android/macOS icons (#521) — thanks @fishfisher.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Packaging: include MS Teams send module in npm tarball.
|
||||
- Sandbox/Browser: auto-start CDP endpoint; proxy CDP out of container for attachOnly; relax Bun fetch typing; align sandbox list output with config images.
|
||||
- Agents/Runtime: gate heartbeat prompt to default sessions; /stop aborts between tool calls; require explicit system-event session keys; guard small context windows; fix model fallback stringification; sessions_spawn inherits provider; failover on billing/credits; respect auth cooldown ordering; restore Anthropic OAuth tool dispatch + tool-name bypass; avoid OpenAI invalid reasoning replay; harden Gmail hook model defaults.
|
||||
@@ -1147,7 +1511,8 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Onboarding/Configure: QuickStart single-select provider picker; avoid Codex CLI false-expiry warnings; clarify WhatsApp owner prompt; fix Minimax hosted onboarding (agents.defaults + msteams heartbeat target); remove configure Control UI prompt; honor gateway --dev flag.
|
||||
|
||||
### Maintenance
|
||||
- Dependencies: bump pi-* stack to 0.42.2.
|
||||
|
||||
- Dependencies: bump pi-\* stack to 0.42.2.
|
||||
- Dependencies: Pi 0.40.0 bump (#543) — thanks @mcinteerj.
|
||||
- Build: Docker build cache layer (#605) — thanks @zknicker.
|
||||
|
||||
@@ -1156,6 +1521,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
## 2026.1.8
|
||||
|
||||
### Highlights
|
||||
|
||||
- Security: DMs locked down by default across providers; pairing-first + allowlist guidance.
|
||||
- Sandbox: per-agent scope defaults + workspace access controls; tool/session isolation tuned.
|
||||
- Agent loop: compaction, pruning, streaming, and error handling hardened.
|
||||
@@ -1164,6 +1530,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- CLI/Gateway/Doctor: daemon/logs/status, auth migration, and diagnostics significantly expanded.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **SECURITY (update ASAP):** inbound DMs are now **locked down by default** on Telegram/WhatsApp/Signal/iMessage/Discord/Slack.
|
||||
- Previously, if you didn’t configure an allowlist, your bot could be **open to anyone** (especially discoverable Telegram bots).
|
||||
- New default: DM pairing (`dmPolicy="pairing"` / `discord.dm.policy="pairing"` / `slack.dm.policy="pairing"`).
|
||||
@@ -1178,6 +1545,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- CLI: remove `update`, `gateway-daemon`, `gateway {install|uninstall|start|stop|restart|daemon status|wake|send|agent}`, and `telegram` commands; move `login/logout` to `providers login/logout` (top-level aliases hidden); use `daemon` for service control, `send`/`agent`/`wake` for RPC, and `nodes canvas` for canvas ops.
|
||||
|
||||
### Fixes
|
||||
|
||||
- **CLI/Gateway/Doctor:** daemon runtime selection + improved logs/status/health/errors; auth/password handling for local CLI; richer close/timeout details; auto-migrate legacy config/sessions/state; integrity checks + repair prompts; `--yes`/`--non-interactive`; `--deep` gateway scans; better restart/service hints.
|
||||
- **Agent loop + compaction:** compaction/pruning tuning, overflow handling, safer bootstrap context, and per-provider threading/confirmations; opt-in tool-result pruning + compact tracking.
|
||||
- **Sandbox + tools:** per-agent sandbox overrides, workspaceAccess controls, session tool visibility, tool policy overrides, process isolation, and tool schema/timeout/reaction unification.
|
||||
@@ -1189,13 +1557,15 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- **Docs:** new FAQ/ClawHub/config examples/showcase entries and clarified auth, sandbox, and systemd docs.
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Skills additions (Himalaya email, CodexBar, 1Password).
|
||||
- Dependency refreshes (pi-* stack, Slack SDK, discord-api-types, file-type, zod, Biome, Vite).
|
||||
- Dependency refreshes (pi-\* stack, Slack SDK, discord-api-types, file-type, zod, Biome, Vite).
|
||||
- Refactors: centralized group allowlist/mention policy; lint/import cleanup; switch tsx → bun for TS execution.
|
||||
|
||||
## 2026.1.5
|
||||
|
||||
### Highlights
|
||||
|
||||
- Models: add image-specific model config (`agent.imageModel` + fallbacks) and scan support.
|
||||
- Agent tools: new `image` tool routed to the image model (when configured).
|
||||
- Config: default model shorthands (`opus`, `sonnet`, `gpt`, `gpt-mini`, `gemini`, `gemini-flash`).
|
||||
@@ -1203,6 +1573,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Bun: optional local install/build workflow without maintaining a Bun lockfile (see `docs/bun.md`).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Control UI: render Markdown in tool result cards.
|
||||
- Control UI: prevent overlapping action buttons in Discord guild rules on narrow layouts.
|
||||
- Android: tapping the foreground service notification brings the app to the front. (#179) — thanks @Syhids
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Welcome to the lobster tank! 🦞
|
||||
|
||||
## Quick Links
|
||||
|
||||
- **GitHub:** https://github.com/openclaw/openclaw
|
||||
- **Discord:** https://discord.gg/qkhbAGHRBT
|
||||
- **X/Twitter:** [@steipete](https://x.com/steipete) / [@openclaw](https://x.com/openclaw)
|
||||
@@ -18,22 +19,46 @@ Welcome to the lobster tank! 🦞
|
||||
- **Jos** - Telegram, API, Nix mode
|
||||
- GitHub: [@joshp123](https://github.com/joshp123) · X: [@jjpcodes](https://x.com/jjpcodes)
|
||||
|
||||
- **Christoph Nakazawa** - JS Infra
|
||||
- GitHub: [@cpojer](https://github.com/cpojer) · X: [@cnakazawa](https://x.com/cnakazawa)
|
||||
|
||||
- **Gustavo Madeira Santana** - Multi-agents, CLI, web UI
|
||||
- GitHub: [@gumadeiras](https://github.com/gumadeiras) · X: [@gumadeiras](https://x.com/gumadeiras)
|
||||
|
||||
## How to Contribute
|
||||
|
||||
1. **Bugs & small fixes** → Open a PR!
|
||||
2. **New features / architecture** → Start a [GitHub Discussion](https://github.com/openclaw/openclaw/discussions) or ask in Discord first
|
||||
3. **Questions** → Discord #setup-help
|
||||
|
||||
## Before You PR
|
||||
|
||||
- Test locally with your OpenClaw instance
|
||||
- Run linter: `npm run lint`
|
||||
- Run tests: `pnpm build && pnpm check && pnpm test`
|
||||
- Keep PRs focused (one thing per PR)
|
||||
- Describe what & why
|
||||
|
||||
## Control UI Decorators
|
||||
|
||||
The Control UI uses Lit with **legacy** decorators (current Rollup parsing does not support
|
||||
`accessor` fields required for standard decorators). When adding reactive fields, keep the
|
||||
legacy style:
|
||||
|
||||
```ts
|
||||
@state() foo = "bar";
|
||||
@property({ type: Number }) count = 0;
|
||||
```
|
||||
|
||||
The root `tsconfig.json` is configured for legacy decorators (`experimentalDecorators: true`)
|
||||
with `useDefineForClassFields: false`. Avoid flipping these unless you are also updating the UI
|
||||
build tooling to support standard decorators.
|
||||
|
||||
## AI/Vibe-Coded PRs Welcome! 🤖
|
||||
|
||||
Built with Codex, Claude, or other AI tools? **Awesome - just mark it!**
|
||||
|
||||
Please include in your PR:
|
||||
|
||||
- [ ] Mark as AI-assisted in the PR title or description
|
||||
- [ ] Note the degree of testing (untested / lightly tested / fully tested)
|
||||
- [ ] Include prompts or session logs if possible (super helpful!)
|
||||
@@ -44,6 +69,7 @@ AI PRs are first-class citizens here. We just want transparency so reviewers kno
|
||||
## Current Focus & Roadmap 🗺
|
||||
|
||||
We are currently prioritizing:
|
||||
|
||||
- **Stability**: Fixing edge cases in channel connections (WhatsApp/Telegram).
|
||||
- **UX**: Improving the onboarding wizard and error messages.
|
||||
- **Skills**: Expanding the library of bundled skills and improving the Skill Creation developer experience.
|
||||
|
||||
11
Dockerfile
11
Dockerfile
@@ -31,9 +31,18 @@ RUN pnpm ui:build
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# Allow non-root user to write temp files during runtime/tests.
|
||||
RUN chown -R node:node /app
|
||||
|
||||
# Security hardening: Run as non-root user
|
||||
# The node:22-bookworm image includes a 'node' user (uid 1000)
|
||||
# This reduces the attack surface by preventing container escape via root privileges
|
||||
USER node
|
||||
|
||||
CMD ["node", "dist/index.js"]
|
||||
# Start gateway server with default config.
|
||||
# Binds to loopback (127.0.0.1) by default for security.
|
||||
#
|
||||
# For container platforms requiring external health checks:
|
||||
# 1. Set OPENCLAW_GATEWAY_TOKEN or OPENCLAW_GATEWAY_PASSWORD env var
|
||||
# 2. Override CMD: ["node","openclaw.mjs","gateway","--allow-unconfigured","--bind","lan"]
|
||||
CMD ["node", "openclaw.mjs", "gateway", "--allow-unconfigured"]
|
||||
|
||||
140
README.md
140
README.md
@@ -18,22 +18,24 @@
|
||||
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge" alt="MIT License"></a>
|
||||
</p>
|
||||
|
||||
**OpenClaw** is a *personal AI assistant* you run on your own devices.
|
||||
**OpenClaw** is a _personal AI assistant_ you run on your own devices.
|
||||
It answers you on the channels you already use (WhatsApp, Telegram, Slack, Discord, Google Chat, Signal, iMessage, Microsoft Teams, WebChat), plus extension channels like BlueBubbles, Matrix, Zalo, and Zalo Personal. It can speak and listen on macOS/iOS/Android, and can render a live Canvas you control. The Gateway is just the control plane — the product is the assistant.
|
||||
|
||||
If you want a personal, single-user assistant that feels local, fast, and always-on, this is it.
|
||||
|
||||
[Website](https://openclaw.ai) · [Docs](https://docs.openclaw.ai) · [DeepWiki](https://deepwiki.com/openclaw/openclaw) · [Getting Started](https://docs.openclaw.ai/start/getting-started) · [Updating](https://docs.openclaw.ai/install/updating) · [Showcase](https://docs.openclaw.ai/start/showcase) · [FAQ](https://docs.openclaw.ai/start/faq) · [Wizard](https://docs.openclaw.ai/start/wizard) · [Nix](https://github.com/openclaw/nix-clawdbot) · [Docker](https://docs.openclaw.ai/install/docker) · [Discord](https://discord.gg/clawd)
|
||||
[Website](https://openclaw.ai) · [Docs](https://docs.openclaw.ai) · [DeepWiki](https://deepwiki.com/openclaw/openclaw) · [Getting Started](https://docs.openclaw.ai/start/getting-started) · [Updating](https://docs.openclaw.ai/install/updating) · [Showcase](https://docs.openclaw.ai/start/showcase) · [FAQ](https://docs.openclaw.ai/start/faq) · [Wizard](https://docs.openclaw.ai/start/wizard) · [Nix](https://github.com/openclaw/nix-openclaw) · [Docker](https://docs.openclaw.ai/install/docker) · [Discord](https://discord.gg/clawd)
|
||||
|
||||
Preferred setup: run the onboarding wizard (`openclaw onboard`). It walks through gateway, workspace, channels, and skills. The CLI wizard is the recommended path and works on **macOS, Linux, and Windows (via WSL2; strongly recommended)**.
|
||||
Preferred setup: run the onboarding wizard (`openclaw onboard`) in your terminal.
|
||||
The wizard guides you step by step through setting up the gateway, workspace, channels, and skills. The CLI wizard is the recommended path and works on **macOS, Linux, and Windows (via WSL2; strongly recommended)**.
|
||||
Works with npm, pnpm, or bun.
|
||||
New install? Start here: [Getting started](https://docs.openclaw.ai/start/getting-started)
|
||||
|
||||
**Subscriptions (OAuth):**
|
||||
|
||||
- **[Anthropic](https://www.anthropic.com/)** (Claude Pro/Max)
|
||||
- **[OpenAI](https://openai.com/)** (ChatGPT/Codex)
|
||||
|
||||
Model note: while any model is supported, I strongly recommend **Anthropic Pro/Max (100/200) + Opus 4.5** for long‑context strength and better prompt‑injection resistance. See [Onboarding](https://docs.openclaw.ai/start/onboarding).
|
||||
Model note: while any model is supported, I strongly recommend **Anthropic Pro/Max (100/200) + Opus 4.6** for long‑context strength and better prompt‑injection resistance. See [Onboarding](https://docs.openclaw.ai/start/onboarding).
|
||||
|
||||
## Models (selection + auth)
|
||||
|
||||
@@ -109,6 +111,7 @@ OpenClaw connects to real messaging surfaces. Treat inbound DMs as **untrusted i
|
||||
Full security guide: [Security](https://docs.openclaw.ai/gateway/security)
|
||||
|
||||
Default behavior on Telegram/WhatsApp/Signal/iMessage/Microsoft Teams/Discord/Google Chat/Slack:
|
||||
|
||||
- **DM pairing** (`dmPolicy="pairing"` / `channels.discord.dm.policy="pairing"` / `channels.slack.dm.policy="pairing"`): unknown senders receive a short pairing code and the bot does not process their message.
|
||||
- Approve with: `openclaw pairing approve <channel> <code>` (then the sender is added to a local allowlist store).
|
||||
- Public inbound DMs require an explicit opt-in: set `dmPolicy="open"` and include `"*"` in the channel allowlist (`allowFrom` / `channels.discord.dm.allowFrom` / `channels.slack.dm.allowFrom`).
|
||||
@@ -118,7 +121,7 @@ Run `openclaw doctor` to surface risky/misconfigured DM policies.
|
||||
## Highlights
|
||||
|
||||
- **[Local-first Gateway](https://docs.openclaw.ai/gateway)** — single control plane for sessions, channels, tools, and events.
|
||||
- **[Multi-channel inbox](https://docs.openclaw.ai/channels)** — WhatsApp, Telegram, Slack, Discord, Google Chat, Signal, iMessage, BlueBubbles, Microsoft Teams, Matrix, Zalo, Zalo Personal, WebChat, macOS, iOS/Android.
|
||||
- **[Multi-channel inbox](https://docs.openclaw.ai/channels)** — WhatsApp, Telegram, Slack, Discord, Google Chat, Signal, BlueBubbles (iMessage), iMessage (legacy), Microsoft Teams, Matrix, Zalo, Zalo Personal, WebChat, macOS, iOS/Android.
|
||||
- **[Multi-agent routing](https://docs.openclaw.ai/gateway/configuration)** — route inbound channels/accounts/peers to isolated agents (workspaces + per-agent sessions).
|
||||
- **[Voice Wake](https://docs.openclaw.ai/nodes/voicewake) + [Talk Mode](https://docs.openclaw.ai/nodes/talk)** — always-on speech for macOS/iOS/Android with ElevenLabs.
|
||||
- **[Live Canvas](https://docs.openclaw.ai/platforms/mac/canvas)** — agent-driven visual workspace with [A2UI](https://docs.openclaw.ai/platforms/mac/canvas#canvas-a2ui).
|
||||
@@ -133,6 +136,7 @@ Run `openclaw doctor` to surface risky/misconfigured DM policies.
|
||||
## Everything we built so far
|
||||
|
||||
### Core platform
|
||||
|
||||
- [Gateway WS control plane](https://docs.openclaw.ai/gateway) with sessions, presence, config, cron, webhooks, [Control UI](https://docs.openclaw.ai/web), and [Canvas host](https://docs.openclaw.ai/platforms/mac/canvas#canvas-a2ui).
|
||||
- [CLI surface](https://docs.openclaw.ai/tools/agent-send): gateway, agent, send, [wizard](https://docs.openclaw.ai/start/wizard), and [doctor](https://docs.openclaw.ai/gateway/doctor).
|
||||
- [Pi agent runtime](https://docs.openclaw.ai/concepts/agent) in RPC mode with tool streaming and block streaming.
|
||||
@@ -140,16 +144,19 @@ Run `openclaw doctor` to surface risky/misconfigured DM policies.
|
||||
- [Media pipeline](https://docs.openclaw.ai/nodes/images): images/audio/video, transcription hooks, size caps, temp file lifecycle. Audio details: [Audio](https://docs.openclaw.ai/nodes/audio).
|
||||
|
||||
### Channels
|
||||
- [Channels](https://docs.openclaw.ai/channels): [WhatsApp](https://docs.openclaw.ai/channels/whatsapp) (Baileys), [Telegram](https://docs.openclaw.ai/channels/telegram) (grammY), [Slack](https://docs.openclaw.ai/channels/slack) (Bolt), [Discord](https://docs.openclaw.ai/channels/discord) (discord.js), [Google Chat](https://docs.openclaw.ai/channels/googlechat) (Chat API), [Signal](https://docs.openclaw.ai/channels/signal) (signal-cli), [iMessage](https://docs.openclaw.ai/channels/imessage) (imsg), [BlueBubbles](https://docs.openclaw.ai/channels/bluebubbles) (extension), [Microsoft Teams](https://docs.openclaw.ai/channels/msteams) (extension), [Matrix](https://docs.openclaw.ai/channels/matrix) (extension), [Zalo](https://docs.openclaw.ai/channels/zalo) (extension), [Zalo Personal](https://docs.openclaw.ai/channels/zalouser) (extension), [WebChat](https://docs.openclaw.ai/web/webchat).
|
||||
|
||||
- [Channels](https://docs.openclaw.ai/channels): [WhatsApp](https://docs.openclaw.ai/channels/whatsapp) (Baileys), [Telegram](https://docs.openclaw.ai/channels/telegram) (grammY), [Slack](https://docs.openclaw.ai/channels/slack) (Bolt), [Discord](https://docs.openclaw.ai/channels/discord) (discord.js), [Google Chat](https://docs.openclaw.ai/channels/googlechat) (Chat API), [Signal](https://docs.openclaw.ai/channels/signal) (signal-cli), [BlueBubbles](https://docs.openclaw.ai/channels/bluebubbles) (iMessage, recommended), [iMessage](https://docs.openclaw.ai/channels/imessage) (legacy imsg), [Microsoft Teams](https://docs.openclaw.ai/channels/msteams) (extension), [Matrix](https://docs.openclaw.ai/channels/matrix) (extension), [Zalo](https://docs.openclaw.ai/channels/zalo) (extension), [Zalo Personal](https://docs.openclaw.ai/channels/zalouser) (extension), [WebChat](https://docs.openclaw.ai/web/webchat).
|
||||
- [Group routing](https://docs.openclaw.ai/concepts/group-messages): mention gating, reply tags, per-channel chunking and routing. Channel rules: [Channels](https://docs.openclaw.ai/channels).
|
||||
|
||||
### Apps + nodes
|
||||
|
||||
- [macOS app](https://docs.openclaw.ai/platforms/macos): menu bar control plane, [Voice Wake](https://docs.openclaw.ai/nodes/voicewake)/PTT, [Talk Mode](https://docs.openclaw.ai/nodes/talk) overlay, [WebChat](https://docs.openclaw.ai/web/webchat), debug tools, [remote gateway](https://docs.openclaw.ai/gateway/remote) control.
|
||||
- [iOS node](https://docs.openclaw.ai/platforms/ios): [Canvas](https://docs.openclaw.ai/platforms/mac/canvas), [Voice Wake](https://docs.openclaw.ai/nodes/voicewake), [Talk Mode](https://docs.openclaw.ai/nodes/talk), camera, screen recording, Bonjour pairing.
|
||||
- [Android node](https://docs.openclaw.ai/platforms/android): [Canvas](https://docs.openclaw.ai/platforms/mac/canvas), [Talk Mode](https://docs.openclaw.ai/nodes/talk), camera, screen recording, optional SMS.
|
||||
- [macOS node mode](https://docs.openclaw.ai/nodes): system.run/notify + canvas/camera exposure.
|
||||
|
||||
### Tools + automation
|
||||
|
||||
- [Browser control](https://docs.openclaw.ai/tools/browser): dedicated openclaw Chrome/Chromium, snapshots, actions, uploads, profiles.
|
||||
- [Canvas](https://docs.openclaw.ai/platforms/mac/canvas): [A2UI](https://docs.openclaw.ai/platforms/mac/canvas#canvas-a2ui) push/reset, eval, snapshot.
|
||||
- [Nodes](https://docs.openclaw.ai/nodes): camera snap/clip, screen record, [location.get](https://docs.openclaw.ai/nodes/location-command), notifications.
|
||||
@@ -157,12 +164,14 @@ Run `openclaw doctor` to surface risky/misconfigured DM policies.
|
||||
- [Skills platform](https://docs.openclaw.ai/tools/skills): bundled, managed, and workspace skills with install gating + UI.
|
||||
|
||||
### Runtime + safety
|
||||
|
||||
- [Channel routing](https://docs.openclaw.ai/concepts/channel-routing), [retry policy](https://docs.openclaw.ai/concepts/retry), and [streaming/chunking](https://docs.openclaw.ai/concepts/streaming).
|
||||
- [Presence](https://docs.openclaw.ai/concepts/presence), [typing indicators](https://docs.openclaw.ai/concepts/typing-indicators), and [usage tracking](https://docs.openclaw.ai/concepts/usage-tracking).
|
||||
- [Models](https://docs.openclaw.ai/concepts/models), [model failover](https://docs.openclaw.ai/concepts/model-failover), and [session pruning](https://docs.openclaw.ai/concepts/session-pruning).
|
||||
- [Security](https://docs.openclaw.ai/gateway/security) and [troubleshooting](https://docs.openclaw.ai/channels/troubleshooting).
|
||||
|
||||
### Ops + packaging
|
||||
|
||||
- [Control UI](https://docs.openclaw.ai/web) + [WebChat](https://docs.openclaw.ai/web/webchat) served directly from the Gateway.
|
||||
- [Tailscale Serve/Funnel](https://docs.openclaw.ai/gateway/tailscale) or [SSH tunnels](https://docs.openclaw.ai/gateway/remote) with token/password auth.
|
||||
- [Nix mode](https://docs.openclaw.ai/install/nix) for declarative config; [Docker](https://docs.openclaw.ai/install/docker)-based installs.
|
||||
@@ -205,6 +214,7 @@ OpenClaw can auto-configure Tailscale **Serve** (tailnet-only) or **Funnel** (pu
|
||||
- `funnel`: public HTTPS via `tailscale funnel` (requires shared password auth).
|
||||
|
||||
Notes:
|
||||
|
||||
- `gateway.bind` must stay `loopback` when Serve/Funnel is enabled (OpenClaw enforces this).
|
||||
- Serve can be forced to require a password by setting `gateway.auth.mode: "password"` or `gateway.auth.allowTailscale: false`.
|
||||
- Funnel refuses to start unless `gateway.auth.mode: "password"` is set.
|
||||
@@ -218,7 +228,7 @@ It’s perfectly fine to run the Gateway on a small Linux instance. Clients (mac
|
||||
|
||||
- **Gateway host** runs the exec tool and channel connections by default.
|
||||
- **Device nodes** run device‑local actions (`system.run`, camera, screen recording, notifications) via `node.invoke`.
|
||||
In short: exec runs where the Gateway lives; device actions run where the device lives.
|
||||
In short: exec runs where the Gateway lives; device actions run where the device lives.
|
||||
|
||||
Details: [Remote access](https://docs.openclaw.ai/gateway/remote) · [Nodes](https://docs.openclaw.ai/nodes) · [Security](https://docs.openclaw.ai/gateway/security)
|
||||
|
||||
@@ -237,7 +247,7 @@ Elevated bash (host permissions) is separate from macOS TCC:
|
||||
|
||||
Details: [Nodes](https://docs.openclaw.ai/nodes) · [macOS app](https://docs.openclaw.ai/platforms/macos) · [Gateway protocol](https://docs.openclaw.ai/concepts/architecture)
|
||||
|
||||
## Agent to Agent (sessions_* tools)
|
||||
## Agent to Agent (sessions\_\* tools)
|
||||
|
||||
- Use these to coordinate work across sessions without jumping between chat surfaces.
|
||||
- `sessions_list` — discover active sessions (agents) and their metadata.
|
||||
@@ -307,8 +317,8 @@ Minimal `~/.openclaw/openclaw.json` (model + defaults):
|
||||
```json5
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5"
|
||||
}
|
||||
model: "anthropic/claude-opus-4-6",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@@ -331,15 +341,15 @@ Details: [Security guide](https://docs.openclaw.ai/gateway/security) · [Docker
|
||||
### [Telegram](https://docs.openclaw.ai/channels/telegram)
|
||||
|
||||
- Set `TELEGRAM_BOT_TOKEN` or `channels.telegram.botToken` (env wins).
|
||||
- Optional: set `channels.telegram.groups` (with `channels.telegram.groups."*".requireMention`); when set, it is a group allowlist (include `"*"` to allow all). Also `channels.telegram.allowFrom` or `channels.telegram.webhookUrl` as needed.
|
||||
- Optional: set `channels.telegram.groups` (with `channels.telegram.groups."*".requireMention`); when set, it is a group allowlist (include `"*"` to allow all). Also `channels.telegram.allowFrom` or `channels.telegram.webhookUrl` + `channels.telegram.webhookSecret` as needed.
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
telegram: {
|
||||
botToken: "123456:ABCDEF"
|
||||
}
|
||||
}
|
||||
botToken: "123456:ABCDEF",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@@ -356,9 +366,9 @@ Details: [Security guide](https://docs.openclaw.ai/gateway/security) · [Docker
|
||||
{
|
||||
channels: {
|
||||
discord: {
|
||||
token: "1234abcd"
|
||||
}
|
||||
}
|
||||
token: "1234abcd",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@@ -366,9 +376,15 @@ Details: [Security guide](https://docs.openclaw.ai/gateway/security) · [Docker
|
||||
|
||||
- Requires `signal-cli` and a `channels.signal` config section.
|
||||
|
||||
### [iMessage](https://docs.openclaw.ai/channels/imessage)
|
||||
### [BlueBubbles (iMessage)](https://docs.openclaw.ai/channels/bluebubbles)
|
||||
|
||||
- macOS only; Messages must be signed in.
|
||||
- **Recommended** iMessage integration.
|
||||
- Configure `channels.bluebubbles.serverUrl` + `channels.bluebubbles.password` and a webhook (`channels.bluebubbles.webhookPath`).
|
||||
- The BlueBubbles server runs on macOS; the Gateway can run on macOS or elsewhere.
|
||||
|
||||
### [iMessage (legacy)](https://docs.openclaw.ai/channels/imessage)
|
||||
|
||||
- Legacy macOS-only integration via `imsg` (Messages must be signed in).
|
||||
- If `channels.imessage.groups` is set, it becomes a group allowlist; include `"*"` to allow all.
|
||||
|
||||
### [Microsoft Teams](https://docs.openclaw.ai/channels/msteams)
|
||||
@@ -386,14 +402,15 @@ Browser control (optional):
|
||||
{
|
||||
browser: {
|
||||
enabled: true,
|
||||
color: "#FF4500"
|
||||
}
|
||||
color: "#FF4500",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Docs
|
||||
|
||||
Use these when you’re past the onboarding flow and want the deeper reference.
|
||||
|
||||
- [Start with the docs index for navigation and “what’s where.”](https://docs.openclaw.ai)
|
||||
- [Read the architecture overview for the gateway + protocol model.](https://docs.openclaw.ai/concepts/architecture)
|
||||
- [Use the full configuration reference when you need every key and example.](https://docs.openclaw.ai/gateway/configuration)
|
||||
@@ -480,40 +497,49 @@ Special thanks to Adam Doppelt for lobster.bot.
|
||||
Thanks to all clawtributors:
|
||||
|
||||
<p align="left">
|
||||
<a href="https://github.com/steipete"><img src="https://avatars.githubusercontent.com/u/58493?v=4&s=48" width="48" height="48" alt="steipete" title="steipete"/></a> <a href="https://github.com/plum-dawg"><img src="https://avatars.githubusercontent.com/u/5909950?v=4&s=48" width="48" height="48" alt="plum-dawg" title="plum-dawg"/></a> <a href="https://github.com/bohdanpodvirnyi"><img src="https://avatars.githubusercontent.com/u/31819391?v=4&s=48" width="48" height="48" alt="bohdanpodvirnyi" title="bohdanpodvirnyi"/></a> <a href="https://github.com/iHildy"><img src="https://avatars.githubusercontent.com/u/25069719?v=4&s=48" width="48" height="48" alt="iHildy" title="iHildy"/></a> <a href="https://github.com/jaydenfyi"><img src="https://avatars.githubusercontent.com/u/213395523?v=4&s=48" width="48" height="48" alt="jaydenfyi" title="jaydenfyi"/></a> <a href="https://github.com/joaohlisboa"><img src="https://avatars.githubusercontent.com/u/8200873?v=4&s=48" width="48" height="48" alt="joaohlisboa" title="joaohlisboa"/></a> <a href="https://github.com/mneves75"><img src="https://avatars.githubusercontent.com/u/2423436?v=4&s=48" width="48" height="48" alt="mneves75" title="mneves75"/></a> <a href="https://github.com/MatthieuBizien"><img src="https://avatars.githubusercontent.com/u/173090?v=4&s=48" width="48" height="48" alt="MatthieuBizien" title="MatthieuBizien"/></a> <a href="https://github.com/MaudeBot"><img src="https://avatars.githubusercontent.com/u/255777700?v=4&s=48" width="48" height="48" alt="MaudeBot" title="MaudeBot"/></a> <a href="https://github.com/Glucksberg"><img src="https://avatars.githubusercontent.com/u/80581902?v=4&s=48" width="48" height="48" alt="Glucksberg" title="Glucksberg"/></a>
|
||||
<a href="https://github.com/rahthakor"><img src="https://avatars.githubusercontent.com/u/8470553?v=4&s=48" width="48" height="48" alt="rahthakor" title="rahthakor"/></a> <a href="https://github.com/vrknetha"><img src="https://avatars.githubusercontent.com/u/20596261?v=4&s=48" width="48" height="48" alt="vrknetha" title="vrknetha"/></a> <a href="https://github.com/radek-paclt"><img src="https://avatars.githubusercontent.com/u/50451445?v=4&s=48" width="48" height="48" alt="radek-paclt" title="radek-paclt"/></a> <a href="https://github.com/vignesh07"><img src="https://avatars.githubusercontent.com/u/1436853?v=4&s=48" width="48" height="48" alt="vignesh07" title="vignesh07"/></a> <a href="https://github.com/joshp123"><img src="https://avatars.githubusercontent.com/u/1497361?v=4&s=48" width="48" height="48" alt="joshp123" title="joshp123"/></a> <a href="https://github.com/tobiasbischoff"><img src="https://avatars.githubusercontent.com/u/711564?v=4&s=48" width="48" height="48" alt="Tobias Bischoff" title="Tobias Bischoff"/></a> <a href="https://github.com/czekaj"><img src="https://avatars.githubusercontent.com/u/1464539?v=4&s=48" width="48" height="48" alt="czekaj" title="czekaj"/></a> <a href="https://github.com/mukhtharcm"><img src="https://avatars.githubusercontent.com/u/56378562?v=4&s=48" width="48" height="48" alt="mukhtharcm" title="mukhtharcm"/></a> <a href="https://github.com/sebslight"><img src="https://avatars.githubusercontent.com/u/19554889?v=4&s=48" width="48" height="48" alt="sebslight" title="sebslight"/></a> <a href="https://github.com/maxsumrall"><img src="https://avatars.githubusercontent.com/u/628843?v=4&s=48" width="48" height="48" alt="maxsumrall" title="maxsumrall"/></a>
|
||||
<a href="https://github.com/xadenryan"><img src="https://avatars.githubusercontent.com/u/165437834?v=4&s=48" width="48" height="48" alt="xadenryan" title="xadenryan"/></a> <a href="https://github.com/rodrigouroz"><img src="https://avatars.githubusercontent.com/u/384037?v=4&s=48" width="48" height="48" alt="rodrigouroz" title="rodrigouroz"/></a> <a href="https://github.com/juanpablodlc"><img src="https://avatars.githubusercontent.com/u/92012363?v=4&s=48" width="48" height="48" alt="juanpablodlc" title="juanpablodlc"/></a> <a href="https://github.com/tyler6204"><img src="https://avatars.githubusercontent.com/u/64381258?v=4&s=48" width="48" height="48" alt="tyler6204" title="tyler6204"/></a> <a href="https://github.com/hsrvc"><img src="https://avatars.githubusercontent.com/u/129702169?v=4&s=48" width="48" height="48" alt="hsrvc" title="hsrvc"/></a> <a href="https://github.com/magimetal"><img src="https://avatars.githubusercontent.com/u/36491250?v=4&s=48" width="48" height="48" alt="magimetal" title="magimetal"/></a> <a href="https://github.com/zerone0x"><img src="https://avatars.githubusercontent.com/u/39543393?v=4&s=48" width="48" height="48" alt="zerone0x" title="zerone0x"/></a> <a href="https://github.com/meaningfool"><img src="https://avatars.githubusercontent.com/u/2862331?v=4&s=48" width="48" height="48" alt="meaningfool" title="meaningfool"/></a> <a href="https://github.com/patelhiren"><img src="https://avatars.githubusercontent.com/u/172098?v=4&s=48" width="48" height="48" alt="patelhiren" title="patelhiren"/></a> <a href="https://github.com/NicholasSpisak"><img src="https://avatars.githubusercontent.com/u/129075147?v=4&s=48" width="48" height="48" alt="NicholasSpisak" title="NicholasSpisak"/></a>
|
||||
<a href="https://github.com/jonisjongithub"><img src="https://avatars.githubusercontent.com/u/86072337?v=4&s=48" width="48" height="48" alt="jonisjongithub" title="jonisjongithub"/></a> <a href="https://github.com/AbhisekBasu1"><img src="https://avatars.githubusercontent.com/u/40645221?v=4&s=48" width="48" height="48" alt="abhisekbasu1" title="abhisekbasu1"/></a> <a href="https://github.com/jamesgroat"><img src="https://avatars.githubusercontent.com/u/2634024?v=4&s=48" width="48" height="48" alt="jamesgroat" title="jamesgroat"/></a> <a href="https://github.com/claude"><img src="https://avatars.githubusercontent.com/u/81847?v=4&s=48" width="48" height="48" alt="claude" title="claude"/></a> <a href="https://github.com/JustYannicc"><img src="https://avatars.githubusercontent.com/u/52761674?v=4&s=48" width="48" height="48" alt="JustYannicc" title="JustYannicc"/></a> <a href="https://github.com/mbelinky"><img src="https://avatars.githubusercontent.com/u/132747814?v=4&s=48" width="48" height="48" alt="Mariano Belinky" title="Mariano Belinky"/></a> <a href="https://github.com/Hyaxia"><img src="https://avatars.githubusercontent.com/u/36747317?v=4&s=48" width="48" height="48" alt="Hyaxia" title="Hyaxia"/></a> <a href="https://github.com/dantelex"><img src="https://avatars.githubusercontent.com/u/631543?v=4&s=48" width="48" height="48" alt="dantelex" title="dantelex"/></a> <a href="https://github.com/SocialNerd42069"><img src="https://avatars.githubusercontent.com/u/118244303?v=4&s=48" width="48" height="48" alt="SocialNerd42069" title="SocialNerd42069"/></a> <a href="https://github.com/daveonkels"><img src="https://avatars.githubusercontent.com/u/533642?v=4&s=48" width="48" height="48" alt="daveonkels" title="daveonkels"/></a>
|
||||
<a href="https://github.com/apps/google-labs-jules"><img src="https://avatars.githubusercontent.com/in/842251?v=4&s=48" width="48" height="48" alt="google-labs-jules[bot]" title="google-labs-jules[bot]"/></a> <a href="https://github.com/lc0rp"><img src="https://avatars.githubusercontent.com/u/2609441?v=4&s=48" width="48" height="48" alt="lc0rp" title="lc0rp"/></a> <a href="https://github.com/mousberg"><img src="https://avatars.githubusercontent.com/u/57605064?v=4&s=48" width="48" height="48" alt="mousberg" title="mousberg"/></a> <a href="https://github.com/adam91holt"><img src="https://avatars.githubusercontent.com/u/9592417?v=4&s=48" width="48" height="48" alt="adam91holt" title="adam91holt"/></a> <a href="https://github.com/hougangdev"><img src="https://avatars.githubusercontent.com/u/105773686?v=4&s=48" width="48" height="48" alt="hougangdev" title="hougangdev"/></a> <a href="https://github.com/gumadeiras"><img src="https://avatars.githubusercontent.com/u/5599352?v=4&s=48" width="48" height="48" alt="gumadeiras" title="gumadeiras"/></a> <a href="https://github.com/shakkernerd"><img src="https://avatars.githubusercontent.com/u/165377636?v=4&s=48" width="48" height="48" alt="shakkernerd" title="shakkernerd"/></a> <a href="https://github.com/mteam88"><img src="https://avatars.githubusercontent.com/u/84196639?v=4&s=48" width="48" height="48" alt="mteam88" title="mteam88"/></a> <a href="https://github.com/hirefrank"><img src="https://avatars.githubusercontent.com/u/183158?v=4&s=48" width="48" height="48" alt="hirefrank" title="hirefrank"/></a> <a href="https://github.com/joeynyc"><img src="https://avatars.githubusercontent.com/u/17919866?v=4&s=48" width="48" height="48" alt="joeynyc" title="joeynyc"/></a>
|
||||
<a href="https://github.com/orlyjamie"><img src="https://avatars.githubusercontent.com/u/6668807?v=4&s=48" width="48" height="48" alt="orlyjamie" title="orlyjamie"/></a> <a href="https://github.com/dbhurley"><img src="https://avatars.githubusercontent.com/u/5251425?v=4&s=48" width="48" height="48" alt="dbhurley" title="dbhurley"/></a> <a href="https://github.com/omniwired"><img src="https://avatars.githubusercontent.com/u/322761?v=4&s=48" width="48" height="48" alt="Eng. Juan Combetto" title="Eng. Juan Combetto"/></a> <a href="https://github.com/TSavo"><img src="https://avatars.githubusercontent.com/u/877990?v=4&s=48" width="48" height="48" alt="TSavo" title="TSavo"/></a> <a href="https://github.com/julianengel"><img src="https://avatars.githubusercontent.com/u/10634231?v=4&s=48" width="48" height="48" alt="julianengel" title="julianengel"/></a> <a href="https://github.com/bradleypriest"><img src="https://avatars.githubusercontent.com/u/167215?v=4&s=48" width="48" height="48" alt="bradleypriest" title="bradleypriest"/></a> <a href="https://github.com/benithors"><img src="https://avatars.githubusercontent.com/u/20652882?v=4&s=48" width="48" height="48" alt="benithors" title="benithors"/></a> <a href="https://github.com/rohannagpal"><img src="https://avatars.githubusercontent.com/u/4009239?v=4&s=48" width="48" height="48" alt="rohannagpal" title="rohannagpal"/></a> <a href="https://github.com/timolins"><img src="https://avatars.githubusercontent.com/u/1440854?v=4&s=48" width="48" height="48" alt="timolins" title="timolins"/></a> <a href="https://github.com/f-trycua"><img src="https://avatars.githubusercontent.com/u/195596869?v=4&s=48" width="48" height="48" alt="f-trycua" title="f-trycua"/></a>
|
||||
<a href="https://github.com/benostein"><img src="https://avatars.githubusercontent.com/u/31802821?v=4&s=48" width="48" height="48" alt="benostein" title="benostein"/></a> <a href="https://github.com/elliotsecops"><img src="https://avatars.githubusercontent.com/u/141947839?v=4&s=48" width="48" height="48" alt="elliotsecops" title="elliotsecops"/></a> <a href="https://github.com/Nachx639"><img src="https://avatars.githubusercontent.com/u/71144023?v=4&s=48" width="48" height="48" alt="nachx639" title="nachx639"/></a> <a href="https://github.com/pvoo"><img src="https://avatars.githubusercontent.com/u/20116814?v=4&s=48" width="48" height="48" alt="pvoo" title="pvoo"/></a> <a href="https://github.com/sreekaransrinath"><img src="https://avatars.githubusercontent.com/u/50989977?v=4&s=48" width="48" height="48" alt="sreekaransrinath" title="sreekaransrinath"/></a> <a href="https://github.com/gupsammy"><img src="https://avatars.githubusercontent.com/u/20296019?v=4&s=48" width="48" height="48" alt="gupsammy" title="gupsammy"/></a> <a href="https://github.com/cristip73"><img src="https://avatars.githubusercontent.com/u/24499421?v=4&s=48" width="48" height="48" alt="cristip73" title="cristip73"/></a> <a href="https://github.com/stefangalescu"><img src="https://avatars.githubusercontent.com/u/52995748?v=4&s=48" width="48" height="48" alt="stefangalescu" title="stefangalescu"/></a> <a href="https://github.com/nachoiacovino"><img src="https://avatars.githubusercontent.com/u/50103937?v=4&s=48" width="48" height="48" alt="nachoiacovino" title="nachoiacovino"/></a> <a href="https://github.com/vsabavat"><img src="https://avatars.githubusercontent.com/u/50385532?v=4&s=48" width="48" height="48" alt="Vasanth Rao Naik Sabavat" title="Vasanth Rao Naik Sabavat"/></a>
|
||||
<a href="https://github.com/petter-b"><img src="https://avatars.githubusercontent.com/u/62076402?v=4&s=48" width="48" height="48" alt="petter-b" title="petter-b"/></a> <a href="https://github.com/thewilloftheshadow"><img src="https://avatars.githubusercontent.com/u/35580099?v=4&s=48" width="48" height="48" alt="thewilloftheshadow" title="thewilloftheshadow"/></a> <a href="https://github.com/cpojer"><img src="https://avatars.githubusercontent.com/u/13352?v=4&s=48" width="48" height="48" alt="cpojer" title="cpojer"/></a> <a href="https://github.com/scald"><img src="https://avatars.githubusercontent.com/u/1215913?v=4&s=48" width="48" height="48" alt="scald" title="scald"/></a> <a href="https://github.com/andranik-sahakyan"><img src="https://avatars.githubusercontent.com/u/8908029?v=4&s=48" width="48" height="48" alt="andranik-sahakyan" title="andranik-sahakyan"/></a> <a href="https://github.com/davidguttman"><img src="https://avatars.githubusercontent.com/u/431696?v=4&s=48" width="48" height="48" alt="davidguttman" title="davidguttman"/></a> <a href="https://github.com/sleontenko"><img src="https://avatars.githubusercontent.com/u/7135949?v=4&s=48" width="48" height="48" alt="sleontenko" title="sleontenko"/></a> <a href="https://github.com/denysvitali"><img src="https://avatars.githubusercontent.com/u/4939519?v=4&s=48" width="48" height="48" alt="denysvitali" title="denysvitali"/></a> <a href="https://github.com/sircrumpet"><img src="https://avatars.githubusercontent.com/u/4436535?v=4&s=48" width="48" height="48" alt="sircrumpet" title="sircrumpet"/></a> <a href="https://github.com/peschee"><img src="https://avatars.githubusercontent.com/u/63866?v=4&s=48" width="48" height="48" alt="peschee" title="peschee"/></a>
|
||||
<a href="https://github.com/nonggialiang"><img src="https://avatars.githubusercontent.com/u/14367839?v=4&s=48" width="48" height="48" alt="nonggialiang" title="nonggialiang"/></a> <a href="https://github.com/rafaelreis-r"><img src="https://avatars.githubusercontent.com/u/57492577?v=4&s=48" width="48" height="48" alt="rafaelreis-r" title="rafaelreis-r"/></a> <a href="https://github.com/dominicnunez"><img src="https://avatars.githubusercontent.com/u/43616264?v=4&s=48" width="48" height="48" alt="dominicnunez" title="dominicnunez"/></a> <a href="https://github.com/lploc94"><img src="https://avatars.githubusercontent.com/u/28453843?v=4&s=48" width="48" height="48" alt="lploc94" title="lploc94"/></a> <a href="https://github.com/ratulsarna"><img src="https://avatars.githubusercontent.com/u/105903728?v=4&s=48" width="48" height="48" alt="ratulsarna" title="ratulsarna"/></a> <a href="https://github.com/lutr0"><img src="https://avatars.githubusercontent.com/u/76906369?v=4&s=48" width="48" height="48" alt="lutr0" title="lutr0"/></a> <a href="https://github.com/kiranjd"><img src="https://avatars.githubusercontent.com/u/25822851?v=4&s=48" width="48" height="48" alt="kiranjd" title="kiranjd"/></a> <a href="https://github.com/danielz1z"><img src="https://avatars.githubusercontent.com/u/235270390?v=4&s=48" width="48" height="48" alt="danielz1z" title="danielz1z"/></a> <a href="https://github.com/AdeboyeDN"><img src="https://avatars.githubusercontent.com/u/65312338?v=4&s=48" width="48" height="48" alt="AdeboyeDN" title="AdeboyeDN"/></a> <a href="https://github.com/Alg0rix"><img src="https://avatars.githubusercontent.com/u/53804949?v=4&s=48" width="48" height="48" alt="Alg0rix" title="Alg0rix"/></a>
|
||||
<a href="https://github.com/papago2355"><img src="https://avatars.githubusercontent.com/u/68721273?v=4&s=48" width="48" height="48" alt="papago2355" title="papago2355"/></a> <a href="https://github.com/emanuelst"><img src="https://avatars.githubusercontent.com/u/9994339?v=4&s=48" width="48" height="48" alt="emanuelst" title="emanuelst"/></a> <a href="https://github.com/KristijanJovanovski"><img src="https://avatars.githubusercontent.com/u/8942284?v=4&s=48" width="48" height="48" alt="KristijanJovanovski" title="KristijanJovanovski"/></a> <a href="https://github.com/rdev"><img src="https://avatars.githubusercontent.com/u/8418866?v=4&s=48" width="48" height="48" alt="rdev" title="rdev"/></a> <a href="https://github.com/rhuanssauro"><img src="https://avatars.githubusercontent.com/u/164682191?v=4&s=48" width="48" height="48" alt="rhuanssauro" title="rhuanssauro"/></a> <a href="https://github.com/joshrad-dev"><img src="https://avatars.githubusercontent.com/u/62785552?v=4&s=48" width="48" height="48" alt="joshrad-dev" title="joshrad-dev"/></a> <a href="https://github.com/osolmaz"><img src="https://avatars.githubusercontent.com/u/2453968?v=4&s=48" width="48" height="48" alt="osolmaz" title="osolmaz"/></a> <a href="https://github.com/adityashaw2"><img src="https://avatars.githubusercontent.com/u/41204444?v=4&s=48" width="48" height="48" alt="adityashaw2" title="adityashaw2"/></a> <a href="https://github.com/CashWilliams"><img src="https://avatars.githubusercontent.com/u/613573?v=4&s=48" width="48" height="48" alt="CashWilliams" title="CashWilliams"/></a> <a href="https://github.com/search?q=sheeek"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="sheeek" title="sheeek"/></a>
|
||||
<a href="https://github.com/ryancontent"><img src="https://avatars.githubusercontent.com/u/39743613?v=4&s=48" width="48" height="48" alt="ryancontent" title="ryancontent"/></a> <a href="https://github.com/artuskg"><img src="https://avatars.githubusercontent.com/u/11966157?v=4&s=48" width="48" height="48" alt="artuskg" title="artuskg"/></a> <a href="https://github.com/Takhoffman"><img src="https://avatars.githubusercontent.com/u/781889?v=4&s=48" width="48" height="48" alt="Takhoffman" title="Takhoffman"/></a> <a href="https://github.com/onutc"><img src="https://avatars.githubusercontent.com/u/152018508?v=4&s=48" width="48" height="48" alt="onutc" title="onutc"/></a> <a href="https://github.com/pauloportella"><img src="https://avatars.githubusercontent.com/u/22947229?v=4&s=48" width="48" height="48" alt="pauloportella" title="pauloportella"/></a> <a href="https://github.com/HirokiKobayashi-R"><img src="https://avatars.githubusercontent.com/u/37167840?v=4&s=48" width="48" height="48" alt="HirokiKobayashi-R" title="HirokiKobayashi-R"/></a> <a href="https://github.com/neooriginal"><img src="https://avatars.githubusercontent.com/u/54811660?v=4&s=48" width="48" height="48" alt="neooriginal" title="neooriginal"/></a> <a href="https://github.com/obviyus"><img src="https://avatars.githubusercontent.com/u/22031114?v=4&s=48" width="48" height="48" alt="obviyus" title="obviyus"/></a> <a href="https://github.com/ManuelHettich"><img src="https://avatars.githubusercontent.com/u/17690367?v=4&s=48" width="48" height="48" alt="manuelhettich" title="manuelhettich"/></a> <a href="https://github.com/minghinmatthewlam"><img src="https://avatars.githubusercontent.com/u/14224566?v=4&s=48" width="48" height="48" alt="minghinmatthewlam" title="minghinmatthewlam"/></a>
|
||||
<a href="https://github.com/manikv12"><img src="https://avatars.githubusercontent.com/u/49544491?v=4&s=48" width="48" height="48" alt="manikv12" title="manikv12"/></a> <a href="https://github.com/myfunc"><img src="https://avatars.githubusercontent.com/u/19294627?v=4&s=48" width="48" height="48" alt="myfunc" title="myfunc"/></a> <a href="https://github.com/travisirby"><img src="https://avatars.githubusercontent.com/u/5958376?v=4&s=48" width="48" height="48" alt="travisirby" title="travisirby"/></a> <a href="https://github.com/buddyh"><img src="https://avatars.githubusercontent.com/u/31752869?v=4&s=48" width="48" height="48" alt="buddyh" title="buddyh"/></a> <a href="https://github.com/connorshea"><img src="https://avatars.githubusercontent.com/u/2977353?v=4&s=48" width="48" height="48" alt="connorshea" title="connorshea"/></a> <a href="https://github.com/kyleok"><img src="https://avatars.githubusercontent.com/u/58307870?v=4&s=48" width="48" height="48" alt="kyleok" title="kyleok"/></a> <a href="https://github.com/mcinteerj"><img src="https://avatars.githubusercontent.com/u/3613653?v=4&s=48" width="48" height="48" alt="mcinteerj" title="mcinteerj"/></a> <a href="https://github.com/apps/dependabot"><img src="https://avatars.githubusercontent.com/in/29110?v=4&s=48" width="48" height="48" alt="dependabot[bot]" title="dependabot[bot]"/></a> <a href="https://github.com/John-Rood"><img src="https://avatars.githubusercontent.com/u/62669593?v=4&s=48" width="48" height="48" alt="John-Rood" title="John-Rood"/></a> <a href="https://github.com/timkrase"><img src="https://avatars.githubusercontent.com/u/38947626?v=4&s=48" width="48" height="48" alt="timkrase" title="timkrase"/></a>
|
||||
<a href="https://github.com/uos-status"><img src="https://avatars.githubusercontent.com/u/255712580?v=4&s=48" width="48" height="48" alt="uos-status" title="uos-status"/></a> <a href="https://github.com/gerardward2007"><img src="https://avatars.githubusercontent.com/u/3002155?v=4&s=48" width="48" height="48" alt="gerardward2007" title="gerardward2007"/></a> <a href="https://github.com/roshanasingh4"><img src="https://avatars.githubusercontent.com/u/88576930?v=4&s=48" width="48" height="48" alt="roshanasingh4" title="roshanasingh4"/></a> <a href="https://github.com/tosh-hamburg"><img src="https://avatars.githubusercontent.com/u/58424326?v=4&s=48" width="48" height="48" alt="tosh-hamburg" title="tosh-hamburg"/></a> <a href="https://github.com/azade-c"><img src="https://avatars.githubusercontent.com/u/252790079?v=4&s=48" width="48" height="48" alt="azade-c" title="azade-c"/></a> <a href="https://github.com/dlauer"><img src="https://avatars.githubusercontent.com/u/757041?v=4&s=48" width="48" height="48" alt="dlauer" title="dlauer"/></a> <a href="https://github.com/JonUleis"><img src="https://avatars.githubusercontent.com/u/7644941?v=4&s=48" width="48" height="48" alt="JonUleis" title="JonUleis"/></a> <a href="https://github.com/shivamraut101"><img src="https://avatars.githubusercontent.com/u/110457469?v=4&s=48" width="48" height="48" alt="shivamraut101" title="shivamraut101"/></a> <a href="https://github.com/bjesuiter"><img src="https://avatars.githubusercontent.com/u/2365676?v=4&s=48" width="48" height="48" alt="bjesuiter" title="bjesuiter"/></a> <a href="https://github.com/cheeeee"><img src="https://avatars.githubusercontent.com/u/21245729?v=4&s=48" width="48" height="48" alt="cheeeee" title="cheeeee"/></a>
|
||||
<a href="https://github.com/robbyczgw-cla"><img src="https://avatars.githubusercontent.com/u/239660374?v=4&s=48" width="48" height="48" alt="robbyczgw-cla" title="robbyczgw-cla"/></a> <a href="https://github.com/conroywhitney"><img src="https://avatars.githubusercontent.com/u/249891?v=4&s=48" width="48" height="48" alt="conroywhitney" title="conroywhitney"/></a> <a href="https://github.com/j1philli"><img src="https://avatars.githubusercontent.com/u/3744255?v=4&s=48" width="48" height="48" alt="Josh Phillips" title="Josh Phillips"/></a> <a href="https://github.com/YuriNachos"><img src="https://avatars.githubusercontent.com/u/19365375?v=4&s=48" width="48" height="48" alt="YuriNachos" title="YuriNachos"/></a> <a href="https://github.com/pookNast"><img src="https://avatars.githubusercontent.com/u/14242552?v=4&s=48" width="48" height="48" alt="pookNast" title="pookNast"/></a> <a href="https://github.com/Whoaa512"><img src="https://avatars.githubusercontent.com/u/1581943?v=4&s=48" width="48" height="48" alt="Whoaa512" title="Whoaa512"/></a> <a href="https://github.com/chriseidhof"><img src="https://avatars.githubusercontent.com/u/5382?v=4&s=48" width="48" height="48" alt="chriseidhof" title="chriseidhof"/></a> <a href="https://github.com/ngutman"><img src="https://avatars.githubusercontent.com/u/1540134?v=4&s=48" width="48" height="48" alt="ngutman" title="ngutman"/></a> <a href="https://github.com/ysqander"><img src="https://avatars.githubusercontent.com/u/80843820?v=4&s=48" width="48" height="48" alt="ysqander" title="ysqander"/></a> <a href="https://github.com/aj47"><img src="https://avatars.githubusercontent.com/u/8023513?v=4&s=48" width="48" height="48" alt="aj47" title="aj47"/></a>
|
||||
<a href="https://github.com/kennyklee"><img src="https://avatars.githubusercontent.com/u/1432489?v=4&s=48" width="48" height="48" alt="kennyklee" title="kennyklee"/></a> <a href="https://github.com/superman32432432"><img src="https://avatars.githubusercontent.com/u/7228420?v=4&s=48" width="48" height="48" alt="superman32432432" title="superman32432432"/></a> <a href="https://github.com/search?q=Yurii%20Chukhlib"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Yurii Chukhlib" title="Yurii Chukhlib"/></a> <a href="https://github.com/grp06"><img src="https://avatars.githubusercontent.com/u/1573959?v=4&s=48" width="48" height="48" alt="grp06" title="grp06"/></a> <a href="https://github.com/antons"><img src="https://avatars.githubusercontent.com/u/129705?v=4&s=48" width="48" height="48" alt="antons" title="antons"/></a> <a href="https://github.com/austinm911"><img src="https://avatars.githubusercontent.com/u/31991302?v=4&s=48" width="48" height="48" alt="austinm911" title="austinm911"/></a> <a href="https://github.com/apps/blacksmith-sh"><img src="https://avatars.githubusercontent.com/in/807020?v=4&s=48" width="48" height="48" alt="blacksmith-sh[bot]" title="blacksmith-sh[bot]"/></a> <a href="https://github.com/damoahdominic"><img src="https://avatars.githubusercontent.com/u/4623434?v=4&s=48" width="48" height="48" alt="damoahdominic" title="damoahdominic"/></a> <a href="https://github.com/dan-dr"><img src="https://avatars.githubusercontent.com/u/6669808?v=4&s=48" width="48" height="48" alt="dan-dr" title="dan-dr"/></a> <a href="https://github.com/HeimdallStrategy"><img src="https://avatars.githubusercontent.com/u/223014405?v=4&s=48" width="48" height="48" alt="HeimdallStrategy" title="HeimdallStrategy"/></a>
|
||||
<a href="https://github.com/imfing"><img src="https://avatars.githubusercontent.com/u/5097752?v=4&s=48" width="48" height="48" alt="imfing" title="imfing"/></a> <a href="https://github.com/jalehman"><img src="https://avatars.githubusercontent.com/u/550978?v=4&s=48" width="48" height="48" alt="jalehman" title="jalehman"/></a> <a href="https://github.com/jarvis-medmatic"><img src="https://avatars.githubusercontent.com/u/252428873?v=4&s=48" width="48" height="48" alt="jarvis-medmatic" title="jarvis-medmatic"/></a> <a href="https://github.com/kkarimi"><img src="https://avatars.githubusercontent.com/u/875218?v=4&s=48" width="48" height="48" alt="kkarimi" title="kkarimi"/></a> <a href="https://github.com/mahmoudashraf93"><img src="https://avatars.githubusercontent.com/u/9130129?v=4&s=48" width="48" height="48" alt="mahmoudashraf93" title="mahmoudashraf93"/></a> <a href="https://github.com/pkrmf"><img src="https://avatars.githubusercontent.com/u/1714267?v=4&s=48" width="48" height="48" alt="pkrmf" title="pkrmf"/></a> <a href="https://github.com/RandyVentures"><img src="https://avatars.githubusercontent.com/u/149904821?v=4&s=48" width="48" height="48" alt="RandyVentures" title="RandyVentures"/></a> <a href="https://github.com/robhparker"><img src="https://avatars.githubusercontent.com/u/7404740?v=4&s=48" width="48" height="48" alt="robhparker" title="robhparker"/></a> <a href="https://github.com/search?q=Ryan%20Lisse"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ryan Lisse" title="Ryan Lisse"/></a> <a href="https://github.com/dougvk"><img src="https://avatars.githubusercontent.com/u/401660?v=4&s=48" width="48" height="48" alt="dougvk" title="dougvk"/></a>
|
||||
<a href="https://github.com/erikpr1994"><img src="https://avatars.githubusercontent.com/u/6299331?v=4&s=48" width="48" height="48" alt="erikpr1994" title="erikpr1994"/></a> <a href="https://github.com/fal3"><img src="https://avatars.githubusercontent.com/u/6484295?v=4&s=48" width="48" height="48" alt="fal3" title="fal3"/></a> <a href="https://github.com/search?q=Ghost"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ghost" title="Ghost"/></a> <a href="https://github.com/jonasjancarik"><img src="https://avatars.githubusercontent.com/u/2459191?v=4&s=48" width="48" height="48" alt="jonasjancarik" title="jonasjancarik"/></a> <a href="https://github.com/search?q=Keith%20the%20Silly%20Goose"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Keith the Silly Goose" title="Keith the Silly Goose"/></a> <a href="https://github.com/search?q=L36%20Server"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="L36 Server" title="L36 Server"/></a> <a href="https://github.com/search?q=Marc"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Marc" title="Marc"/></a> <a href="https://github.com/mitschabaude-bot"><img src="https://avatars.githubusercontent.com/u/247582884?v=4&s=48" width="48" height="48" alt="mitschabaude-bot" title="mitschabaude-bot"/></a> <a href="https://github.com/mkbehr"><img src="https://avatars.githubusercontent.com/u/1285?v=4&s=48" width="48" height="48" alt="mkbehr" title="mkbehr"/></a> <a href="https://github.com/neist"><img src="https://avatars.githubusercontent.com/u/1029724?v=4&s=48" width="48" height="48" alt="neist" title="neist"/></a>
|
||||
<a href="https://github.com/sibbl"><img src="https://avatars.githubusercontent.com/u/866535?v=4&s=48" width="48" height="48" alt="sibbl" title="sibbl"/></a> <a href="https://github.com/chrisrodz"><img src="https://avatars.githubusercontent.com/u/2967620?v=4&s=48" width="48" height="48" alt="chrisrodz" title="chrisrodz"/></a> <a href="https://github.com/search?q=Friederike%20Seiler"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Friederike Seiler" title="Friederike Seiler"/></a> <a href="https://github.com/gabriel-trigo"><img src="https://avatars.githubusercontent.com/u/38991125?v=4&s=48" width="48" height="48" alt="gabriel-trigo" title="gabriel-trigo"/></a> <a href="https://github.com/Iamadig"><img src="https://avatars.githubusercontent.com/u/102129234?v=4&s=48" width="48" height="48" alt="iamadig" title="iamadig"/></a> <a href="https://github.com/jdrhyne"><img src="https://avatars.githubusercontent.com/u/7828464?v=4&s=48" width="48" height="48" alt="Jonathan D. Rhyne (DJ-D)" title="Jonathan D. Rhyne (DJ-D)"/></a> <a href="https://github.com/search?q=Joshua%20Mitchell"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Joshua Mitchell" title="Joshua Mitchell"/></a> <a href="https://github.com/search?q=Kit"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kit" title="Kit"/></a> <a href="https://github.com/koala73"><img src="https://avatars.githubusercontent.com/u/996596?v=4&s=48" width="48" height="48" alt="koala73" title="koala73"/></a> <a href="https://github.com/manmal"><img src="https://avatars.githubusercontent.com/u/142797?v=4&s=48" width="48" height="48" alt="manmal" title="manmal"/></a>
|
||||
<a href="https://github.com/ogulcancelik"><img src="https://avatars.githubusercontent.com/u/7064011?v=4&s=48" width="48" height="48" alt="ogulcancelik" title="ogulcancelik"/></a> <a href="https://github.com/pasogott"><img src="https://avatars.githubusercontent.com/u/23458152?v=4&s=48" width="48" height="48" alt="pasogott" title="pasogott"/></a> <a href="https://github.com/petradonka"><img src="https://avatars.githubusercontent.com/u/7353770?v=4&s=48" width="48" height="48" alt="petradonka" title="petradonka"/></a> <a href="https://github.com/rubyrunsstuff"><img src="https://avatars.githubusercontent.com/u/246602379?v=4&s=48" width="48" height="48" alt="rubyrunsstuff" title="rubyrunsstuff"/></a> <a href="https://github.com/siddhantjain"><img src="https://avatars.githubusercontent.com/u/4835232?v=4&s=48" width="48" height="48" alt="siddhantjain" title="siddhantjain"/></a> <a href="https://github.com/suminhthanh"><img src="https://avatars.githubusercontent.com/u/2907636?v=4&s=48" width="48" height="48" alt="suminhthanh" title="suminhthanh"/></a> <a href="https://github.com/svkozak"><img src="https://avatars.githubusercontent.com/u/31941359?v=4&s=48" width="48" height="48" alt="svkozak" title="svkozak"/></a> <a href="https://github.com/VACInc"><img src="https://avatars.githubusercontent.com/u/3279061?v=4&s=48" width="48" height="48" alt="VACInc" title="VACInc"/></a> <a href="https://github.com/wes-davis"><img src="https://avatars.githubusercontent.com/u/16506720?v=4&s=48" width="48" height="48" alt="wes-davis" title="wes-davis"/></a> <a href="https://github.com/zats"><img src="https://avatars.githubusercontent.com/u/2688806?v=4&s=48" width="48" height="48" alt="zats" title="zats"/></a>
|
||||
<a href="https://github.com/24601"><img src="https://avatars.githubusercontent.com/u/1157207?v=4&s=48" width="48" height="48" alt="24601" title="24601"/></a> <a href="https://github.com/ameno-"><img src="https://avatars.githubusercontent.com/u/2416135?v=4&s=48" width="48" height="48" alt="ameno-" title="ameno-"/></a> <a href="https://github.com/search?q=Chris%20Taylor"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Chris Taylor" title="Chris Taylor"/></a> <a href="https://github.com/dguido"><img src="https://avatars.githubusercontent.com/u/294844?v=4&s=48" width="48" height="48" alt="dguido" title="dguido"/></a> <a href="https://github.com/djangonavarro220"><img src="https://avatars.githubusercontent.com/u/251162586?v=4&s=48" width="48" height="48" alt="Django Navarro" title="Django Navarro"/></a> <a href="https://github.com/evalexpr"><img src="https://avatars.githubusercontent.com/u/23485511?v=4&s=48" width="48" height="48" alt="evalexpr" title="evalexpr"/></a> <a href="https://github.com/henrino3"><img src="https://avatars.githubusercontent.com/u/4260288?v=4&s=48" width="48" height="48" alt="henrino3" title="henrino3"/></a> <a href="https://github.com/humanwritten"><img src="https://avatars.githubusercontent.com/u/206531610?v=4&s=48" width="48" height="48" alt="humanwritten" title="humanwritten"/></a> <a href="https://github.com/larlyssa"><img src="https://avatars.githubusercontent.com/u/13128869?v=4&s=48" width="48" height="48" alt="larlyssa" title="larlyssa"/></a> <a href="https://github.com/Lukavyi"><img src="https://avatars.githubusercontent.com/u/1013690?v=4&s=48" width="48" height="48" alt="Lukavyi" title="Lukavyi"/></a>
|
||||
<a href="https://github.com/odysseus0"><img src="https://avatars.githubusercontent.com/u/8635094?v=4&s=48" width="48" height="48" alt="odysseus0" title="odysseus0"/></a> <a href="https://github.com/oswalpalash"><img src="https://avatars.githubusercontent.com/u/6431196?v=4&s=48" width="48" height="48" alt="oswalpalash" title="oswalpalash"/></a> <a href="https://github.com/pcty-nextgen-service-account"><img src="https://avatars.githubusercontent.com/u/112553441?v=4&s=48" width="48" height="48" alt="pcty-nextgen-service-account" title="pcty-nextgen-service-account"/></a> <a href="https://github.com/pi0"><img src="https://avatars.githubusercontent.com/u/5158436?v=4&s=48" width="48" height="48" alt="pi0" title="pi0"/></a> <a href="https://github.com/rmorse"><img src="https://avatars.githubusercontent.com/u/853547?v=4&s=48" width="48" height="48" alt="rmorse" title="rmorse"/></a> <a href="https://github.com/search?q=Roopak%20Nijhara"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Roopak Nijhara" title="Roopak Nijhara"/></a> <a href="https://github.com/Syhids"><img src="https://avatars.githubusercontent.com/u/671202?v=4&s=48" width="48" height="48" alt="Syhids" title="Syhids"/></a> <a href="https://github.com/search?q=Aaron%20Konyer"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Aaron Konyer" title="Aaron Konyer"/></a> <a href="https://github.com/aaronveklabs"><img src="https://avatars.githubusercontent.com/u/225997828?v=4&s=48" width="48" height="48" alt="aaronveklabs" title="aaronveklabs"/></a> <a href="https://github.com/andreabadesso"><img src="https://avatars.githubusercontent.com/u/3586068?v=4&s=48" width="48" height="48" alt="andreabadesso" title="andreabadesso"/></a>
|
||||
<a href="https://github.com/search?q=Andrii"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Andrii" title="Andrii"/></a> <a href="https://github.com/cash-echo-bot"><img src="https://avatars.githubusercontent.com/u/252747386?v=4&s=48" width="48" height="48" alt="cash-echo-bot" title="cash-echo-bot"/></a> <a href="https://github.com/search?q=Clawd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Clawd" title="Clawd"/></a> <a href="https://github.com/search?q=ClawdFx"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ClawdFx" title="ClawdFx"/></a> <a href="https://github.com/EnzeD"><img src="https://avatars.githubusercontent.com/u/9866900?v=4&s=48" width="48" height="48" alt="EnzeD" title="EnzeD"/></a> <a href="https://github.com/erik-agens"><img src="https://avatars.githubusercontent.com/u/80908960?v=4&s=48" width="48" height="48" alt="erik-agens" title="erik-agens"/></a> <a href="https://github.com/Evizero"><img src="https://avatars.githubusercontent.com/u/10854026?v=4&s=48" width="48" height="48" alt="Evizero" title="Evizero"/></a> <a href="https://github.com/fcatuhe"><img src="https://avatars.githubusercontent.com/u/17382215?v=4&s=48" width="48" height="48" alt="fcatuhe" title="fcatuhe"/></a> <a href="https://github.com/itsjaydesu"><img src="https://avatars.githubusercontent.com/u/220390?v=4&s=48" width="48" height="48" alt="itsjaydesu" title="itsjaydesu"/></a> <a href="https://github.com/ivancasco"><img src="https://avatars.githubusercontent.com/u/2452858?v=4&s=48" width="48" height="48" alt="ivancasco" title="ivancasco"/></a>
|
||||
<a href="https://github.com/ivanrvpereira"><img src="https://avatars.githubusercontent.com/u/183991?v=4&s=48" width="48" height="48" alt="ivanrvpereira" title="ivanrvpereira"/></a> <a href="https://github.com/search?q=Jarvis"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jarvis" title="Jarvis"/></a> <a href="https://github.com/jayhickey"><img src="https://avatars.githubusercontent.com/u/1676460?v=4&s=48" width="48" height="48" alt="jayhickey" title="jayhickey"/></a> <a href="https://github.com/jeffersonwarrior"><img src="https://avatars.githubusercontent.com/u/89030989?v=4&s=48" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a> <a href="https://github.com/search?q=jeffersonwarrior"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a> <a href="https://github.com/jverdi"><img src="https://avatars.githubusercontent.com/u/345050?v=4&s=48" width="48" height="48" alt="jverdi" title="jverdi"/></a> <a href="https://github.com/longmaba"><img src="https://avatars.githubusercontent.com/u/9361500?v=4&s=48" width="48" height="48" alt="longmaba" title="longmaba"/></a> <a href="https://github.com/MarvinCui"><img src="https://avatars.githubusercontent.com/u/130876763?v=4&s=48" width="48" height="48" alt="MarvinCui" title="MarvinCui"/></a> <a href="https://github.com/mjrussell"><img src="https://avatars.githubusercontent.com/u/1641895?v=4&s=48" width="48" height="48" alt="mjrussell" title="mjrussell"/></a> <a href="https://github.com/odnxe"><img src="https://avatars.githubusercontent.com/u/403141?v=4&s=48" width="48" height="48" alt="odnxe" title="odnxe"/></a>
|
||||
<a href="https://github.com/optimikelabs"><img src="https://avatars.githubusercontent.com/u/31423109?v=4&s=48" width="48" height="48" alt="optimikelabs" title="optimikelabs"/></a> <a href="https://github.com/p6l-richard"><img src="https://avatars.githubusercontent.com/u/18185649?v=4&s=48" width="48" height="48" alt="p6l-richard" title="p6l-richard"/></a> <a href="https://github.com/philipp-spiess"><img src="https://avatars.githubusercontent.com/u/458591?v=4&s=48" width="48" height="48" alt="philipp-spiess" title="philipp-spiess"/></a> <a href="https://github.com/search?q=Pocket%20Clawd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Pocket Clawd" title="Pocket Clawd"/></a> <a href="https://github.com/robaxelsen"><img src="https://avatars.githubusercontent.com/u/13132899?v=4&s=48" width="48" height="48" alt="robaxelsen" title="robaxelsen"/></a> <a href="https://github.com/search?q=Sash%20Catanzarite"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Sash Catanzarite" title="Sash Catanzarite"/></a> <a href="https://github.com/Suksham-sharma"><img src="https://avatars.githubusercontent.com/u/94667656?v=4&s=48" width="48" height="48" alt="Suksham-sharma" title="Suksham-sharma"/></a> <a href="https://github.com/T5-AndyML"><img src="https://avatars.githubusercontent.com/u/22801233?v=4&s=48" width="48" height="48" alt="T5-AndyML" title="T5-AndyML"/></a> <a href="https://github.com/tewatia"><img src="https://avatars.githubusercontent.com/u/22875334?v=4&s=48" width="48" height="48" alt="tewatia" title="tewatia"/></a> <a href="https://github.com/travisp"><img src="https://avatars.githubusercontent.com/u/165698?v=4&s=48" width="48" height="48" alt="travisp" title="travisp"/></a>
|
||||
<a href="https://github.com/search?q=VAC"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="VAC" title="VAC"/></a> <a href="https://github.com/search?q=william%20arzt"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="william arzt" title="william arzt"/></a> <a href="https://github.com/zknicker"><img src="https://avatars.githubusercontent.com/u/1164085?v=4&s=48" width="48" height="48" alt="zknicker" title="zknicker"/></a> <a href="https://github.com/0oAstro"><img src="https://avatars.githubusercontent.com/u/79555780?v=4&s=48" width="48" height="48" alt="0oAstro" title="0oAstro"/></a> <a href="https://github.com/abhaymundhara"><img src="https://avatars.githubusercontent.com/u/62872231?v=4&s=48" width="48" height="48" alt="abhaymundhara" title="abhaymundhara"/></a> <a href="https://github.com/aduk059"><img src="https://avatars.githubusercontent.com/u/257603478?v=4&s=48" width="48" height="48" alt="aduk059" title="aduk059"/></a> <a href="https://github.com/search?q=alejandro%20maza"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="alejandro maza" title="alejandro maza"/></a> <a href="https://github.com/Alex-Alaniz"><img src="https://avatars.githubusercontent.com/u/88956822?v=4&s=48" width="48" height="48" alt="Alex-Alaniz" title="Alex-Alaniz"/></a> <a href="https://github.com/alexstyl"><img src="https://avatars.githubusercontent.com/u/1665273?v=4&s=48" width="48" height="48" alt="alexstyl" title="alexstyl"/></a> <a href="https://github.com/andrewting19"><img src="https://avatars.githubusercontent.com/u/10536704?v=4&s=48" width="48" height="48" alt="andrewting19" title="andrewting19"/></a>
|
||||
<a href="https://github.com/anpoirier"><img src="https://avatars.githubusercontent.com/u/1245729?v=4&s=48" width="48" height="48" alt="anpoirier" title="anpoirier"/></a> <a href="https://github.com/araa47"><img src="https://avatars.githubusercontent.com/u/22760261?v=4&s=48" width="48" height="48" alt="araa47" title="araa47"/></a> <a href="https://github.com/arthyn"><img src="https://avatars.githubusercontent.com/u/5466421?v=4&s=48" width="48" height="48" alt="arthyn" title="arthyn"/></a> <a href="https://github.com/Asleep123"><img src="https://avatars.githubusercontent.com/u/122379135?v=4&s=48" width="48" height="48" alt="Asleep123" title="Asleep123"/></a> <a href="https://github.com/bguidolim"><img src="https://avatars.githubusercontent.com/u/987360?v=4&s=48" width="48" height="48" alt="bguidolim" title="bguidolim"/></a> <a href="https://github.com/bolismauro"><img src="https://avatars.githubusercontent.com/u/771999?v=4&s=48" width="48" height="48" alt="bolismauro" title="bolismauro"/></a> <a href="https://github.com/chenyuan99"><img src="https://avatars.githubusercontent.com/u/25518100?v=4&s=48" width="48" height="48" alt="chenyuan99" title="chenyuan99"/></a> <a href="https://github.com/Chloe-VP"><img src="https://avatars.githubusercontent.com/u/257371598?v=4&s=48" width="48" height="48" alt="Chloe-VP" title="Chloe-VP"/></a> <a href="https://github.com/conhecendoia"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendoia" title="conhecendoia"/></a> <a href="https://github.com/dasilva333"><img src="https://avatars.githubusercontent.com/u/947827?v=4&s=48" width="48" height="48" alt="dasilva333" title="dasilva333"/></a>
|
||||
<a href="https://github.com/David-Marsh-Photo"><img src="https://avatars.githubusercontent.com/u/228404527?v=4&s=48" width="48" height="48" alt="David-Marsh-Photo" title="David-Marsh-Photo"/></a> <a href="https://github.com/search?q=Developer"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Developer" title="Developer"/></a> <a href="https://github.com/search?q=Dimitrios%20Ploutarchos"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Dimitrios Ploutarchos" title="Dimitrios Ploutarchos"/></a> <a href="https://github.com/search?q=Drake%20Thomsen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Drake Thomsen" title="Drake Thomsen"/></a> <a href="https://github.com/dylanneve1"><img src="https://avatars.githubusercontent.com/u/31746704?v=4&s=48" width="48" height="48" alt="dylanneve1" title="dylanneve1"/></a> <a href="https://github.com/search?q=Felix%20Krause"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Felix Krause" title="Felix Krause"/></a> <a href="https://github.com/foeken"><img src="https://avatars.githubusercontent.com/u/13864?v=4&s=48" width="48" height="48" alt="foeken" title="foeken"/></a> <a href="https://github.com/frankekn"><img src="https://avatars.githubusercontent.com/u/4488090?v=4&s=48" width="48" height="48" alt="frankekn" title="frankekn"/></a> <a href="https://github.com/search?q=ganghyun%20kim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ganghyun kim" title="ganghyun kim"/></a> <a href="https://github.com/grrowl"><img src="https://avatars.githubusercontent.com/u/907140?v=4&s=48" width="48" height="48" alt="grrowl" title="grrowl"/></a>
|
||||
<a href="https://github.com/gtsifrikas"><img src="https://avatars.githubusercontent.com/u/8904378?v=4&s=48" width="48" height="48" alt="gtsifrikas" title="gtsifrikas"/></a> <a href="https://github.com/HazAT"><img src="https://avatars.githubusercontent.com/u/363802?v=4&s=48" width="48" height="48" alt="HazAT" title="HazAT"/></a> <a href="https://github.com/hrdwdmrbl"><img src="https://avatars.githubusercontent.com/u/554881?v=4&s=48" width="48" height="48" alt="hrdwdmrbl" title="hrdwdmrbl"/></a> <a href="https://github.com/hugobarauna"><img src="https://avatars.githubusercontent.com/u/2719?v=4&s=48" width="48" height="48" alt="hugobarauna" title="hugobarauna"/></a> <a href="https://github.com/search?q=Jamie%20Openshaw"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jamie Openshaw" title="Jamie Openshaw"/></a> <a href="https://github.com/search?q=Jane"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jane" title="Jane"/></a> <a href="https://github.com/search?q=Jarvis%20Deploy"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jarvis Deploy" title="Jarvis Deploy"/></a> <a href="https://github.com/search?q=Jefferson%20Nunn"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jefferson Nunn" title="Jefferson Nunn"/></a> <a href="https://github.com/jogi47"><img src="https://avatars.githubusercontent.com/u/1710139?v=4&s=48" width="48" height="48" alt="jogi47" title="jogi47"/></a> <a href="https://github.com/kentaro"><img src="https://avatars.githubusercontent.com/u/3458?v=4&s=48" width="48" height="48" alt="kentaro" title="kentaro"/></a>
|
||||
<a href="https://github.com/search?q=Kevin%20Lin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kevin Lin" title="Kevin Lin"/></a> <a href="https://github.com/kira-ariaki"><img src="https://avatars.githubusercontent.com/u/257352493?v=4&s=48" width="48" height="48" alt="kira-ariaki" title="kira-ariaki"/></a> <a href="https://github.com/kitze"><img src="https://avatars.githubusercontent.com/u/1160594?v=4&s=48" width="48" height="48" alt="kitze" title="kitze"/></a> <a href="https://github.com/Kiwitwitter"><img src="https://avatars.githubusercontent.com/u/25277769?v=4&s=48" width="48" height="48" alt="Kiwitwitter" title="Kiwitwitter"/></a> <a href="https://github.com/levifig"><img src="https://avatars.githubusercontent.com/u/1605?v=4&s=48" width="48" height="48" alt="levifig" title="levifig"/></a> <a href="https://github.com/search?q=Lloyd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Lloyd" title="Lloyd"/></a> <a href="https://github.com/longjos"><img src="https://avatars.githubusercontent.com/u/740160?v=4&s=48" width="48" height="48" alt="longjos" title="longjos"/></a> <a href="https://github.com/loukotal"><img src="https://avatars.githubusercontent.com/u/18210858?v=4&s=48" width="48" height="48" alt="loukotal" title="loukotal"/></a> <a href="https://github.com/louzhixian"><img src="https://avatars.githubusercontent.com/u/7994361?v=4&s=48" width="48" height="48" alt="louzhixian" title="louzhixian"/></a> <a href="https://github.com/martinpucik"><img src="https://avatars.githubusercontent.com/u/5503097?v=4&s=48" width="48" height="48" alt="martinpucik" title="martinpucik"/></a>
|
||||
<a href="https://github.com/search?q=Matt%20mini"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Matt mini" title="Matt mini"/></a> <a href="https://github.com/mertcicekci0"><img src="https://avatars.githubusercontent.com/u/179321902?v=4&s=48" width="48" height="48" alt="mertcicekci0" title="mertcicekci0"/></a> <a href="https://github.com/search?q=Miles"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Miles" title="Miles"/></a> <a href="https://github.com/mrdbstn"><img src="https://avatars.githubusercontent.com/u/58957632?v=4&s=48" width="48" height="48" alt="mrdbstn" title="mrdbstn"/></a> <a href="https://github.com/MSch"><img src="https://avatars.githubusercontent.com/u/7475?v=4&s=48" width="48" height="48" alt="MSch" title="MSch"/></a> <a href="https://github.com/search?q=Mustafa%20Tag%20Eldeen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mustafa Tag Eldeen" title="Mustafa Tag Eldeen"/></a> <a href="https://github.com/mylukin"><img src="https://avatars.githubusercontent.com/u/1021019?v=4&s=48" width="48" height="48" alt="mylukin" title="mylukin"/></a> <a href="https://github.com/nathanbosse"><img src="https://avatars.githubusercontent.com/u/4040669?v=4&s=48" width="48" height="48" alt="nathanbosse" title="nathanbosse"/></a> <a href="https://github.com/ndraiman"><img src="https://avatars.githubusercontent.com/u/12609607?v=4&s=48" width="48" height="48" alt="ndraiman" title="ndraiman"/></a> <a href="https://github.com/nexty5870"><img src="https://avatars.githubusercontent.com/u/3869659?v=4&s=48" width="48" height="48" alt="nexty5870" title="nexty5870"/></a>
|
||||
<a href="https://github.com/Noctivoro"><img src="https://avatars.githubusercontent.com/u/183974570?v=4&s=48" width="48" height="48" alt="Noctivoro" title="Noctivoro"/></a> <a href="https://github.com/ppamment"><img src="https://avatars.githubusercontent.com/u/2122919?v=4&s=48" width="48" height="48" alt="ppamment" title="ppamment"/></a> <a href="https://github.com/prathamdby"><img src="https://avatars.githubusercontent.com/u/134331217?v=4&s=48" width="48" height="48" alt="prathamdby" title="prathamdby"/></a> <a href="https://github.com/ptn1411"><img src="https://avatars.githubusercontent.com/u/57529765?v=4&s=48" width="48" height="48" alt="ptn1411" title="ptn1411"/></a> <a href="https://github.com/reeltimeapps"><img src="https://avatars.githubusercontent.com/u/637338?v=4&s=48" width="48" height="48" alt="reeltimeapps" title="reeltimeapps"/></a> <a href="https://github.com/RLTCmpe"><img src="https://avatars.githubusercontent.com/u/10762242?v=4&s=48" width="48" height="48" alt="RLTCmpe" title="RLTCmpe"/></a> <a href="https://github.com/search?q=Rolf%20Fredheim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rolf Fredheim" title="Rolf Fredheim"/></a> <a href="https://github.com/search?q=Rony%20Kelner"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rony Kelner" title="Rony Kelner"/></a> <a href="https://github.com/search?q=Samrat%20Jha"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Samrat Jha" title="Samrat Jha"/></a> <a href="https://github.com/senoldogann"><img src="https://avatars.githubusercontent.com/u/45736551?v=4&s=48" width="48" height="48" alt="senoldogann" title="senoldogann"/></a>
|
||||
<a href="https://github.com/Seredeep"><img src="https://avatars.githubusercontent.com/u/22802816?v=4&s=48" width="48" height="48" alt="Seredeep" title="Seredeep"/></a> <a href="https://github.com/sergical"><img src="https://avatars.githubusercontent.com/u/3760543?v=4&s=48" width="48" height="48" alt="sergical" title="sergical"/></a> <a href="https://github.com/shiv19"><img src="https://avatars.githubusercontent.com/u/9407019?v=4&s=48" width="48" height="48" alt="shiv19" title="shiv19"/></a> <a href="https://github.com/shiyuanhai"><img src="https://avatars.githubusercontent.com/u/1187370?v=4&s=48" width="48" height="48" alt="shiyuanhai" title="shiyuanhai"/></a> <a href="https://github.com/siraht"><img src="https://avatars.githubusercontent.com/u/73152895?v=4&s=48" width="48" height="48" alt="siraht" title="siraht"/></a> <a href="https://github.com/snopoke"><img src="https://avatars.githubusercontent.com/u/249606?v=4&s=48" width="48" height="48" alt="snopoke" title="snopoke"/></a> <a href="https://github.com/spiceoogway"><img src="https://avatars.githubusercontent.com/u/105812383?v=4&s=48" width="48" height="48" alt="spiceoogway" title="spiceoogway"/></a> <a href="https://github.com/search?q=techboss"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="techboss" title="techboss"/></a> <a href="https://github.com/testingabc321"><img src="https://avatars.githubusercontent.com/u/8577388?v=4&s=48" width="48" height="48" alt="testingabc321" title="testingabc321"/></a> <a href="https://github.com/search?q=The%20Admiral"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="The Admiral" title="The Admiral"/></a>
|
||||
<a href="https://github.com/thesash"><img src="https://avatars.githubusercontent.com/u/1166151?v=4&s=48" width="48" height="48" alt="thesash" title="thesash"/></a> <a href="https://github.com/search?q=Ubuntu"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ubuntu" title="Ubuntu"/></a> <a href="https://github.com/search?q=Vibe%20Kanban"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Vibe Kanban" title="Vibe Kanban"/></a> <a href="https://github.com/voidserf"><img src="https://avatars.githubusercontent.com/u/477673?v=4&s=48" width="48" height="48" alt="voidserf" title="voidserf"/></a> <a href="https://github.com/search?q=Vultr-Clawd%20Admin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Vultr-Clawd Admin" title="Vultr-Clawd Admin"/></a> <a href="https://github.com/search?q=Wimmie"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Wimmie" title="Wimmie"/></a> <a href="https://github.com/search?q=wolfred"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="wolfred" title="wolfred"/></a> <a href="https://github.com/wstock"><img src="https://avatars.githubusercontent.com/u/1394687?v=4&s=48" width="48" height="48" alt="wstock" title="wstock"/></a> <a href="https://github.com/YangHuang2280"><img src="https://avatars.githubusercontent.com/u/201681634?v=4&s=48" width="48" height="48" alt="YangHuang2280" title="YangHuang2280"/></a> <a href="https://github.com/yazinsai"><img src="https://avatars.githubusercontent.com/u/1846034?v=4&s=48" width="48" height="48" alt="yazinsai" title="yazinsai"/></a>
|
||||
<a href="https://github.com/YiWang24"><img src="https://avatars.githubusercontent.com/u/176262341?v=4&s=48" width="48" height="48" alt="YiWang24" title="YiWang24"/></a> <a href="https://github.com/search?q=ymat19"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ymat19" title="ymat19"/></a> <a href="https://github.com/search?q=Zach%20Knickerbocker"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Zach Knickerbocker" title="Zach Knickerbocker"/></a> <a href="https://github.com/zackerthescar"><img src="https://avatars.githubusercontent.com/u/38077284?v=4&s=48" width="48" height="48" alt="zackerthescar" title="zackerthescar"/></a> <a href="https://github.com/0xJonHoldsCrypto"><img src="https://avatars.githubusercontent.com/u/81202085?v=4&s=48" width="48" height="48" alt="0xJonHoldsCrypto" title="0xJonHoldsCrypto"/></a> <a href="https://github.com/aaronn"><img src="https://avatars.githubusercontent.com/u/1653630?v=4&s=48" width="48" height="48" alt="aaronn" title="aaronn"/></a> <a href="https://github.com/Alphonse-arianee"><img src="https://avatars.githubusercontent.com/u/254457365?v=4&s=48" width="48" height="48" alt="Alphonse-arianee" title="Alphonse-arianee"/></a> <a href="https://github.com/atalovesyou"><img src="https://avatars.githubusercontent.com/u/3534502?v=4&s=48" width="48" height="48" alt="atalovesyou" title="atalovesyou"/></a> <a href="https://github.com/search?q=Azade"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Azade" title="Azade"/></a> <a href="https://github.com/carlulsoe"><img src="https://avatars.githubusercontent.com/u/34673973?v=4&s=48" width="48" height="48" alt="carlulsoe" title="carlulsoe"/></a>
|
||||
<a href="https://github.com/search?q=ddyo"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ddyo" title="ddyo"/></a> <a href="https://github.com/search?q=Erik"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Erik" title="Erik"/></a> <a href="https://github.com/latitudeki5223"><img src="https://avatars.githubusercontent.com/u/119656367?v=4&s=48" width="48" height="48" alt="latitudeki5223" title="latitudeki5223"/></a> <a href="https://github.com/search?q=Manuel%20Maly"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Manuel Maly" title="Manuel Maly"/></a> <a href="https://github.com/search?q=Mourad%20Boustani"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mourad Boustani" title="Mourad Boustani"/></a> <a href="https://github.com/odrobnik"><img src="https://avatars.githubusercontent.com/u/333270?v=4&s=48" width="48" height="48" alt="odrobnik" title="odrobnik"/></a> <a href="https://github.com/pcty-nextgen-ios-builder"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="pcty-nextgen-ios-builder" title="pcty-nextgen-ios-builder"/></a> <a href="https://github.com/search?q=Quentin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Quentin" title="Quentin"/></a> <a href="https://github.com/search?q=Randy%20Torres"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Randy Torres" title="Randy Torres"/></a> <a href="https://github.com/rhjoh"><img src="https://avatars.githubusercontent.com/u/105699450?v=4&s=48" width="48" height="48" alt="rhjoh" title="rhjoh"/></a>
|
||||
<a href="https://github.com/ronak-guliani"><img src="https://avatars.githubusercontent.com/u/23518228?v=4&s=48" width="48" height="48" alt="ronak-guliani" title="ronak-guliani"/></a> <a href="https://github.com/search?q=William%20Stock"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="William Stock" title="William Stock"/></a>
|
||||
<a href="https://github.com/steipete"><img src="https://avatars.githubusercontent.com/u/58493?v=4&s=48" width="48" height="48" alt="steipete" title="steipete"/></a> <a href="https://github.com/joshp123"><img src="https://avatars.githubusercontent.com/u/1497361?v=4&s=48" width="48" height="48" alt="joshp123" title="joshp123"/></a> <a href="https://github.com/cpojer"><img src="https://avatars.githubusercontent.com/u/13352?v=4&s=48" width="48" height="48" alt="cpojer" title="cpojer"/></a> <a href="https://github.com/mbelinky"><img src="https://avatars.githubusercontent.com/u/132747814?v=4&s=48" width="48" height="48" alt="Mariano Belinky" title="Mariano Belinky"/></a> <a href="https://github.com/plum-dawg"><img src="https://avatars.githubusercontent.com/u/5909950?v=4&s=48" width="48" height="48" alt="plum-dawg" title="plum-dawg"/></a> <a href="https://github.com/bohdanpodvirnyi"><img src="https://avatars.githubusercontent.com/u/31819391?v=4&s=48" width="48" height="48" alt="bohdanpodvirnyi" title="bohdanpodvirnyi"/></a> <a href="https://github.com/sebslight"><img src="https://avatars.githubusercontent.com/u/19554889?v=4&s=48" width="48" height="48" alt="sebslight" title="sebslight"/></a> <a href="https://github.com/iHildy"><img src="https://avatars.githubusercontent.com/u/25069719?v=4&s=48" width="48" height="48" alt="iHildy" title="iHildy"/></a> <a href="https://github.com/jaydenfyi"><img src="https://avatars.githubusercontent.com/u/213395523?v=4&s=48" width="48" height="48" alt="jaydenfyi" title="jaydenfyi"/></a> <a href="https://github.com/joaohlisboa"><img src="https://avatars.githubusercontent.com/u/8200873?v=4&s=48" width="48" height="48" alt="joaohlisboa" title="joaohlisboa"/></a>
|
||||
<a href="https://github.com/mneves75"><img src="https://avatars.githubusercontent.com/u/2423436?v=4&s=48" width="48" height="48" alt="mneves75" title="mneves75"/></a> <a href="https://github.com/MatthieuBizien"><img src="https://avatars.githubusercontent.com/u/173090?v=4&s=48" width="48" height="48" alt="MatthieuBizien" title="MatthieuBizien"/></a> <a href="https://github.com/Glucksberg"><img src="https://avatars.githubusercontent.com/u/80581902?v=4&s=48" width="48" height="48" alt="Glucksberg" title="Glucksberg"/></a> <a href="https://github.com/MaudeBot"><img src="https://avatars.githubusercontent.com/u/255777700?v=4&s=48" width="48" height="48" alt="MaudeBot" title="MaudeBot"/></a> <a href="https://github.com/gumadeiras"><img src="https://avatars.githubusercontent.com/u/5599352?v=4&s=48" width="48" height="48" alt="gumadeiras" title="gumadeiras"/></a> <a href="https://github.com/tyler6204"><img src="https://avatars.githubusercontent.com/u/64381258?v=4&s=48" width="48" height="48" alt="tyler6204" title="tyler6204"/></a> <a href="https://github.com/rahthakor"><img src="https://avatars.githubusercontent.com/u/8470553?v=4&s=48" width="48" height="48" alt="rahthakor" title="rahthakor"/></a> <a href="https://github.com/vrknetha"><img src="https://avatars.githubusercontent.com/u/20596261?v=4&s=48" width="48" height="48" alt="vrknetha" title="vrknetha"/></a> <a href="https://github.com/vignesh07"><img src="https://avatars.githubusercontent.com/u/1436853?v=4&s=48" width="48" height="48" alt="vignesh07" title="vignesh07"/></a> <a href="https://github.com/radek-paclt"><img src="https://avatars.githubusercontent.com/u/50451445?v=4&s=48" width="48" height="48" alt="radek-paclt" title="radek-paclt"/></a>
|
||||
<a href="https://github.com/abdelsfane"><img src="https://avatars.githubusercontent.com/u/32418586?v=4&s=48" width="48" height="48" alt="abdelsfane" title="abdelsfane"/></a> <a href="https://github.com/tobiasbischoff"><img src="https://avatars.githubusercontent.com/u/711564?v=4&s=48" width="48" height="48" alt="Tobias Bischoff" title="Tobias Bischoff"/></a> <a href="https://github.com/christianklotz"><img src="https://avatars.githubusercontent.com/u/69443?v=4&s=48" width="48" height="48" alt="christianklotz" title="christianklotz"/></a> <a href="https://github.com/czekaj"><img src="https://avatars.githubusercontent.com/u/1464539?v=4&s=48" width="48" height="48" alt="czekaj" title="czekaj"/></a> <a href="https://github.com/ethanpalm"><img src="https://avatars.githubusercontent.com/u/56270045?v=4&s=48" width="48" height="48" alt="ethanpalm" title="ethanpalm"/></a> <a href="https://github.com/mukhtharcm"><img src="https://avatars.githubusercontent.com/u/56378562?v=4&s=48" width="48" height="48" alt="mukhtharcm" title="mukhtharcm"/></a> <a href="https://github.com/maxsumrall"><img src="https://avatars.githubusercontent.com/u/628843?v=4&s=48" width="48" height="48" alt="maxsumrall" title="maxsumrall"/></a> <a href="https://github.com/xadenryan"><img src="https://avatars.githubusercontent.com/u/165437834?v=4&s=48" width="48" height="48" alt="xadenryan" title="xadenryan"/></a> <a href="https://github.com/VACInc"><img src="https://avatars.githubusercontent.com/u/3279061?v=4&s=48" width="48" height="48" alt="VACInc" title="VACInc"/></a> <a href="https://github.com/rodrigouroz"><img src="https://avatars.githubusercontent.com/u/384037?v=4&s=48" width="48" height="48" alt="rodrigouroz" title="rodrigouroz"/></a>
|
||||
<a href="https://github.com/juanpablodlc"><img src="https://avatars.githubusercontent.com/u/92012363?v=4&s=48" width="48" height="48" alt="juanpablodlc" title="juanpablodlc"/></a> <a href="https://github.com/conroywhitney"><img src="https://avatars.githubusercontent.com/u/249891?v=4&s=48" width="48" height="48" alt="conroywhitney" title="conroywhitney"/></a> <a href="https://github.com/hsrvc"><img src="https://avatars.githubusercontent.com/u/129702169?v=4&s=48" width="48" height="48" alt="hsrvc" title="hsrvc"/></a> <a href="https://github.com/magimetal"><img src="https://avatars.githubusercontent.com/u/36491250?v=4&s=48" width="48" height="48" alt="magimetal" title="magimetal"/></a> <a href="https://github.com/zerone0x"><img src="https://avatars.githubusercontent.com/u/39543393?v=4&s=48" width="48" height="48" alt="zerone0x" title="zerone0x"/></a> <a href="https://github.com/Takhoffman"><img src="https://avatars.githubusercontent.com/u/781889?v=4&s=48" width="48" height="48" alt="Takhoffman" title="Takhoffman"/></a> <a href="https://github.com/meaningfool"><img src="https://avatars.githubusercontent.com/u/2862331?v=4&s=48" width="48" height="48" alt="meaningfool" title="meaningfool"/></a> <a href="https://github.com/mudrii"><img src="https://avatars.githubusercontent.com/u/220262?v=4&s=48" width="48" height="48" alt="mudrii" title="mudrii"/></a> <a href="https://github.com/patelhiren"><img src="https://avatars.githubusercontent.com/u/172098?v=4&s=48" width="48" height="48" alt="patelhiren" title="patelhiren"/></a> <a href="https://github.com/NicholasSpisak"><img src="https://avatars.githubusercontent.com/u/129075147?v=4&s=48" width="48" height="48" alt="NicholasSpisak" title="NicholasSpisak"/></a>
|
||||
<a href="https://github.com/jonisjongithub"><img src="https://avatars.githubusercontent.com/u/86072337?v=4&s=48" width="48" height="48" alt="jonisjongithub" title="jonisjongithub"/></a> <a href="https://github.com/AbhisekBasu1"><img src="https://avatars.githubusercontent.com/u/40645221?v=4&s=48" width="48" height="48" alt="abhisekbasu1" title="abhisekbasu1"/></a> <a href="https://github.com/jamesgroat"><img src="https://avatars.githubusercontent.com/u/2634024?v=4&s=48" width="48" height="48" alt="jamesgroat" title="jamesgroat"/></a> <a href="https://github.com/BunsDev"><img src="https://avatars.githubusercontent.com/u/68980965?v=4&s=48" width="48" height="48" alt="BunsDev" title="BunsDev"/></a> <a href="https://github.com/claude"><img src="https://avatars.githubusercontent.com/u/81847?v=4&s=48" width="48" height="48" alt="claude" title="claude"/></a> <a href="https://github.com/JustYannicc"><img src="https://avatars.githubusercontent.com/u/52761674?v=4&s=48" width="48" height="48" alt="JustYannicc" title="JustYannicc"/></a> <a href="https://github.com/Hyaxia"><img src="https://avatars.githubusercontent.com/u/36747317?v=4&s=48" width="48" height="48" alt="Hyaxia" title="Hyaxia"/></a> <a href="https://github.com/dantelex"><img src="https://avatars.githubusercontent.com/u/631543?v=4&s=48" width="48" height="48" alt="dantelex" title="dantelex"/></a> <a href="https://github.com/SocialNerd42069"><img src="https://avatars.githubusercontent.com/u/118244303?v=4&s=48" width="48" height="48" alt="SocialNerd42069" title="SocialNerd42069"/></a> <a href="https://github.com/daveonkels"><img src="https://avatars.githubusercontent.com/u/533642?v=4&s=48" width="48" height="48" alt="daveonkels" title="daveonkels"/></a>
|
||||
<a href="https://github.com/apps/google-labs-jules"><img src="https://avatars.githubusercontent.com/in/842251?v=4&s=48" width="48" height="48" alt="google-labs-jules[bot]" title="google-labs-jules[bot]"/></a> <a href="https://github.com/lc0rp"><img src="https://avatars.githubusercontent.com/u/2609441?v=4&s=48" width="48" height="48" alt="lc0rp" title="lc0rp"/></a> <a href="https://github.com/adam91holt"><img src="https://avatars.githubusercontent.com/u/9592417?v=4&s=48" width="48" height="48" alt="adam91holt" title="adam91holt"/></a> <a href="https://github.com/mousberg"><img src="https://avatars.githubusercontent.com/u/57605064?v=4&s=48" width="48" height="48" alt="mousberg" title="mousberg"/></a> <a href="https://github.com/hougangdev"><img src="https://avatars.githubusercontent.com/u/105773686?v=4&s=48" width="48" height="48" alt="hougangdev" title="hougangdev"/></a> <a href="https://github.com/shakkernerd"><img src="https://avatars.githubusercontent.com/u/165377636?v=4&s=48" width="48" height="48" alt="shakkernerd" title="shakkernerd"/></a> <a href="https://github.com/coygeek"><img src="https://avatars.githubusercontent.com/u/65363919?v=4&s=48" width="48" height="48" alt="coygeek" title="coygeek"/></a> <a href="https://github.com/mteam88"><img src="https://avatars.githubusercontent.com/u/84196639?v=4&s=48" width="48" height="48" alt="mteam88" title="mteam88"/></a> <a href="https://github.com/hirefrank"><img src="https://avatars.githubusercontent.com/u/183158?v=4&s=48" width="48" height="48" alt="hirefrank" title="hirefrank"/></a> <a href="https://github.com/M00N7682"><img src="https://avatars.githubusercontent.com/u/170746674?v=4&s=48" width="48" height="48" alt="M00N7682" title="M00N7682"/></a>
|
||||
<a href="https://github.com/joeynyc"><img src="https://avatars.githubusercontent.com/u/17919866?v=4&s=48" width="48" height="48" alt="joeynyc" title="joeynyc"/></a> <a href="https://github.com/orlyjamie"><img src="https://avatars.githubusercontent.com/u/6668807?v=4&s=48" width="48" height="48" alt="orlyjamie" title="orlyjamie"/></a> <a href="https://github.com/dbhurley"><img src="https://avatars.githubusercontent.com/u/5251425?v=4&s=48" width="48" height="48" alt="dbhurley" title="dbhurley"/></a> <a href="https://github.com/omniwired"><img src="https://avatars.githubusercontent.com/u/322761?v=4&s=48" width="48" height="48" alt="Eng. Juan Combetto" title="Eng. Juan Combetto"/></a> <a href="https://github.com/TSavo"><img src="https://avatars.githubusercontent.com/u/877990?v=4&s=48" width="48" height="48" alt="TSavo" title="TSavo"/></a> <a href="https://github.com/aerolalit"><img src="https://avatars.githubusercontent.com/u/17166039?v=4&s=48" width="48" height="48" alt="aerolalit" title="aerolalit"/></a> <a href="https://github.com/julianengel"><img src="https://avatars.githubusercontent.com/u/10634231?v=4&s=48" width="48" height="48" alt="julianengel" title="julianengel"/></a> <a href="https://github.com/bradleypriest"><img src="https://avatars.githubusercontent.com/u/167215?v=4&s=48" width="48" height="48" alt="bradleypriest" title="bradleypriest"/></a> <a href="https://github.com/benithors"><img src="https://avatars.githubusercontent.com/u/20652882?v=4&s=48" width="48" height="48" alt="benithors" title="benithors"/></a> <a href="https://github.com/lsh411"><img src="https://avatars.githubusercontent.com/u/6801488?v=4&s=48" width="48" height="48" alt="lsh411" title="lsh411"/></a>
|
||||
<a href="https://github.com/gut-puncture"><img src="https://avatars.githubusercontent.com/u/75851986?v=4&s=48" width="48" height="48" alt="gut-puncture" title="gut-puncture"/></a> <a href="https://github.com/rohannagpal"><img src="https://avatars.githubusercontent.com/u/4009239?v=4&s=48" width="48" height="48" alt="rohannagpal" title="rohannagpal"/></a> <a href="https://github.com/timolins"><img src="https://avatars.githubusercontent.com/u/1440854?v=4&s=48" width="48" height="48" alt="timolins" title="timolins"/></a> <a href="https://github.com/f-trycua"><img src="https://avatars.githubusercontent.com/u/195596869?v=4&s=48" width="48" height="48" alt="f-trycua" title="f-trycua"/></a> <a href="https://github.com/benostein"><img src="https://avatars.githubusercontent.com/u/31802821?v=4&s=48" width="48" height="48" alt="benostein" title="benostein"/></a> <a href="https://github.com/elliotsecops"><img src="https://avatars.githubusercontent.com/u/141947839?v=4&s=48" width="48" height="48" alt="elliotsecops" title="elliotsecops"/></a> <a href="https://github.com/Nachx639"><img src="https://avatars.githubusercontent.com/u/71144023?v=4&s=48" width="48" height="48" alt="nachx639" title="nachx639"/></a> <a href="https://github.com/pvoo"><img src="https://avatars.githubusercontent.com/u/20116814?v=4&s=48" width="48" height="48" alt="pvoo" title="pvoo"/></a> <a href="https://github.com/sreekaransrinath"><img src="https://avatars.githubusercontent.com/u/50989977?v=4&s=48" width="48" height="48" alt="sreekaransrinath" title="sreekaransrinath"/></a> <a href="https://github.com/gupsammy"><img src="https://avatars.githubusercontent.com/u/20296019?v=4&s=48" width="48" height="48" alt="gupsammy" title="gupsammy"/></a>
|
||||
<a href="https://github.com/cristip73"><img src="https://avatars.githubusercontent.com/u/24499421?v=4&s=48" width="48" height="48" alt="cristip73" title="cristip73"/></a> <a href="https://github.com/stefangalescu"><img src="https://avatars.githubusercontent.com/u/52995748?v=4&s=48" width="48" height="48" alt="stefangalescu" title="stefangalescu"/></a> <a href="https://github.com/nachoiacovino"><img src="https://avatars.githubusercontent.com/u/50103937?v=4&s=48" width="48" height="48" alt="nachoiacovino" title="nachoiacovino"/></a> <a href="https://github.com/vsabavat"><img src="https://avatars.githubusercontent.com/u/50385532?v=4&s=48" width="48" height="48" alt="Vasanth Rao Naik Sabavat" title="Vasanth Rao Naik Sabavat"/></a> <a href="https://github.com/petter-b"><img src="https://avatars.githubusercontent.com/u/62076402?v=4&s=48" width="48" height="48" alt="petter-b" title="petter-b"/></a> <a href="https://github.com/thewilloftheshadow"><img src="https://avatars.githubusercontent.com/u/35580099?v=4&s=48" width="48" height="48" alt="thewilloftheshadow" title="thewilloftheshadow"/></a> <a href="https://github.com/leszekszpunar"><img src="https://avatars.githubusercontent.com/u/13106764?v=4&s=48" width="48" height="48" alt="leszekszpunar" title="leszekszpunar"/></a> <a href="https://github.com/scald"><img src="https://avatars.githubusercontent.com/u/1215913?v=4&s=48" width="48" height="48" alt="scald" title="scald"/></a> <a href="https://github.com/pycckuu"><img src="https://avatars.githubusercontent.com/u/1489583?v=4&s=48" width="48" height="48" alt="pycckuu" title="pycckuu"/></a> <a href="https://github.com/andranik-sahakyan"><img src="https://avatars.githubusercontent.com/u/8908029?v=4&s=48" width="48" height="48" alt="andranik-sahakyan" title="andranik-sahakyan"/></a>
|
||||
<a href="https://github.com/davidguttman"><img src="https://avatars.githubusercontent.com/u/431696?v=4&s=48" width="48" height="48" alt="davidguttman" title="davidguttman"/></a> <a href="https://github.com/sleontenko"><img src="https://avatars.githubusercontent.com/u/7135949?v=4&s=48" width="48" height="48" alt="sleontenko" title="sleontenko"/></a> <a href="https://github.com/denysvitali"><img src="https://avatars.githubusercontent.com/u/4939519?v=4&s=48" width="48" height="48" alt="denysvitali" title="denysvitali"/></a> <a href="https://github.com/apps/clawdinator"><img src="https://avatars.githubusercontent.com/in/2607181?v=4&s=48" width="48" height="48" alt="clawdinator[bot]" title="clawdinator[bot]"/></a> <a href="https://github.com/TinyTb"><img src="https://avatars.githubusercontent.com/u/5957298?v=4&s=48" width="48" height="48" alt="TinyTb" title="TinyTb"/></a> <a href="https://github.com/sircrumpet"><img src="https://avatars.githubusercontent.com/u/4436535?v=4&s=48" width="48" height="48" alt="sircrumpet" title="sircrumpet"/></a> <a href="https://github.com/peschee"><img src="https://avatars.githubusercontent.com/u/63866?v=4&s=48" width="48" height="48" alt="peschee" title="peschee"/></a> <a href="https://github.com/nicolasstanley"><img src="https://avatars.githubusercontent.com/u/60584925?v=4&s=48" width="48" height="48" alt="nicolasstanley" title="nicolasstanley"/></a> <a href="https://github.com/davidiach"><img src="https://avatars.githubusercontent.com/u/28102235?v=4&s=48" width="48" height="48" alt="davidiach" title="davidiach"/></a> <a href="https://github.com/nonggialiang"><img src="https://avatars.githubusercontent.com/u/14367839?v=4&s=48" width="48" height="48" alt="nonggialiang" title="nonggialiang"/></a>
|
||||
<a href="https://github.com/ironbyte-rgb"><img src="https://avatars.githubusercontent.com/u/230665944?v=4&s=48" width="48" height="48" alt="ironbyte-rgb" title="ironbyte-rgb"/></a> <a href="https://github.com/rafaelreis-r"><img src="https://avatars.githubusercontent.com/u/57492577?v=4&s=48" width="48" height="48" alt="rafaelreis-r" title="rafaelreis-r"/></a> <a href="https://github.com/dominicnunez"><img src="https://avatars.githubusercontent.com/u/43616264?v=4&s=48" width="48" height="48" alt="dominicnunez" title="dominicnunez"/></a> <a href="https://github.com/lploc94"><img src="https://avatars.githubusercontent.com/u/28453843?v=4&s=48" width="48" height="48" alt="lploc94" title="lploc94"/></a> <a href="https://github.com/ratulsarna"><img src="https://avatars.githubusercontent.com/u/105903728?v=4&s=48" width="48" height="48" alt="ratulsarna" title="ratulsarna"/></a> <a href="https://github.com/sfo2001"><img src="https://avatars.githubusercontent.com/u/103369858?v=4&s=48" width="48" height="48" alt="sfo2001" title="sfo2001"/></a> <a href="https://github.com/lutr0"><img src="https://avatars.githubusercontent.com/u/76906369?v=4&s=48" width="48" height="48" alt="lutr0" title="lutr0"/></a> <a href="https://github.com/kiranjd"><img src="https://avatars.githubusercontent.com/u/25822851?v=4&s=48" width="48" height="48" alt="kiranjd" title="kiranjd"/></a> <a href="https://github.com/danielz1z"><img src="https://avatars.githubusercontent.com/u/235270390?v=4&s=48" width="48" height="48" alt="danielz1z" title="danielz1z"/></a> <a href="https://github.com/Iranb"><img src="https://avatars.githubusercontent.com/u/49674669?v=4&s=48" width="48" height="48" alt="Iranb" title="Iranb"/></a>
|
||||
<a href="https://github.com/AdeboyeDN"><img src="https://avatars.githubusercontent.com/u/65312338?v=4&s=48" width="48" height="48" alt="AdeboyeDN" title="AdeboyeDN"/></a> <a href="https://github.com/Alg0rix"><img src="https://avatars.githubusercontent.com/u/53804949?v=4&s=48" width="48" height="48" alt="Alg0rix" title="Alg0rix"/></a> <a href="https://github.com/obviyus"><img src="https://avatars.githubusercontent.com/u/22031114?v=4&s=48" width="48" height="48" alt="obviyus" title="obviyus"/></a> <a href="https://github.com/papago2355"><img src="https://avatars.githubusercontent.com/u/68721273?v=4&s=48" width="48" height="48" alt="papago2355" title="papago2355"/></a> <a href="https://github.com/emanuelst"><img src="https://avatars.githubusercontent.com/u/9994339?v=4&s=48" width="48" height="48" alt="emanuelst" title="emanuelst"/></a> <a href="https://github.com/evanotero"><img src="https://avatars.githubusercontent.com/u/13204105?v=4&s=48" width="48" height="48" alt="evanotero" title="evanotero"/></a> <a href="https://github.com/KristijanJovanovski"><img src="https://avatars.githubusercontent.com/u/8942284?v=4&s=48" width="48" height="48" alt="KristijanJovanovski" title="KristijanJovanovski"/></a> <a href="https://github.com/jlowin"><img src="https://avatars.githubusercontent.com/u/153965?v=4&s=48" width="48" height="48" alt="jlowin" title="jlowin"/></a> <a href="https://github.com/rdev"><img src="https://avatars.githubusercontent.com/u/8418866?v=4&s=48" width="48" height="48" alt="rdev" title="rdev"/></a> <a href="https://github.com/rhuanssauro"><img src="https://avatars.githubusercontent.com/u/164682191?v=4&s=48" width="48" height="48" alt="rhuanssauro" title="rhuanssauro"/></a>
|
||||
<a href="https://github.com/joshrad-dev"><img src="https://avatars.githubusercontent.com/u/62785552?v=4&s=48" width="48" height="48" alt="joshrad-dev" title="joshrad-dev"/></a> <a href="https://github.com/osolmaz"><img src="https://avatars.githubusercontent.com/u/2453968?v=4&s=48" width="48" height="48" alt="osolmaz" title="osolmaz"/></a> <a href="https://github.com/adityashaw2"><img src="https://avatars.githubusercontent.com/u/41204444?v=4&s=48" width="48" height="48" alt="adityashaw2" title="adityashaw2"/></a> <a href="https://github.com/CashWilliams"><img src="https://avatars.githubusercontent.com/u/613573?v=4&s=48" width="48" height="48" alt="CashWilliams" title="CashWilliams"/></a> <a href="https://github.com/search?q=sheeek"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="sheeek" title="sheeek"/></a> <a href="https://github.com/ryancontent"><img src="https://avatars.githubusercontent.com/u/39743613?v=4&s=48" width="48" height="48" alt="ryancontent" title="ryancontent"/></a> <a href="https://github.com/jasonsschin"><img src="https://avatars.githubusercontent.com/u/1456889?v=4&s=48" width="48" height="48" alt="jasonsschin" title="jasonsschin"/></a> <a href="https://github.com/artuskg"><img src="https://avatars.githubusercontent.com/u/11966157?v=4&s=48" width="48" height="48" alt="artuskg" title="artuskg"/></a> <a href="https://github.com/onutc"><img src="https://avatars.githubusercontent.com/u/152018508?v=4&s=48" width="48" height="48" alt="onutc" title="onutc"/></a> <a href="https://github.com/pauloportella"><img src="https://avatars.githubusercontent.com/u/22947229?v=4&s=48" width="48" height="48" alt="pauloportella" title="pauloportella"/></a>
|
||||
<a href="https://github.com/HirokiKobayashi-R"><img src="https://avatars.githubusercontent.com/u/37167840?v=4&s=48" width="48" height="48" alt="HirokiKobayashi-R" title="HirokiKobayashi-R"/></a> <a href="https://github.com/ThanhNguyxn"><img src="https://avatars.githubusercontent.com/u/74597207?v=4&s=48" width="48" height="48" alt="ThanhNguyxn" title="ThanhNguyxn"/></a> <a href="https://github.com/18-RAJAT"><img src="https://avatars.githubusercontent.com/u/78920780?v=4&s=48" width="48" height="48" alt="18-RAJAT" title="18-RAJAT"/></a> <a href="https://github.com/kimitaka"><img src="https://avatars.githubusercontent.com/u/167225?v=4&s=48" width="48" height="48" alt="kimitaka" title="kimitaka"/></a> <a href="https://github.com/yuting0624"><img src="https://avatars.githubusercontent.com/u/32728916?v=4&s=48" width="48" height="48" alt="yuting0624" title="yuting0624"/></a> <a href="https://github.com/neooriginal"><img src="https://avatars.githubusercontent.com/u/54811660?v=4&s=48" width="48" height="48" alt="neooriginal" title="neooriginal"/></a> <a href="https://github.com/ManuelHettich"><img src="https://avatars.githubusercontent.com/u/17690367?v=4&s=48" width="48" height="48" alt="manuelhettich" title="manuelhettich"/></a> <a href="https://github.com/minghinmatthewlam"><img src="https://avatars.githubusercontent.com/u/14224566?v=4&s=48" width="48" height="48" alt="minghinmatthewlam" title="minghinmatthewlam"/></a> <a href="https://github.com/unisone"><img src="https://avatars.githubusercontent.com/u/32521398?v=4&s=48" width="48" height="48" alt="unisone" title="unisone"/></a> <a href="https://github.com/baccula"><img src="https://avatars.githubusercontent.com/u/22080883?v=4&s=48" width="48" height="48" alt="baccula" title="baccula"/></a>
|
||||
<a href="https://github.com/manikv12"><img src="https://avatars.githubusercontent.com/u/49544491?v=4&s=48" width="48" height="48" alt="manikv12" title="manikv12"/></a> <a href="https://github.com/myfunc"><img src="https://avatars.githubusercontent.com/u/19294627?v=4&s=48" width="48" height="48" alt="myfunc" title="myfunc"/></a> <a href="https://github.com/travisirby"><img src="https://avatars.githubusercontent.com/u/5958376?v=4&s=48" width="48" height="48" alt="travisirby" title="travisirby"/></a> <a href="https://github.com/fujiwara-tofu-shop"><img src="https://avatars.githubusercontent.com/u/259415332?v=4&s=48" width="48" height="48" alt="fujiwara-tofu-shop" title="fujiwara-tofu-shop"/></a> <a href="https://github.com/buddyh"><img src="https://avatars.githubusercontent.com/u/31752869?v=4&s=48" width="48" height="48" alt="buddyh" title="buddyh"/></a> <a href="https://github.com/connorshea"><img src="https://avatars.githubusercontent.com/u/2977353?v=4&s=48" width="48" height="48" alt="connorshea" title="connorshea"/></a> <a href="https://github.com/bjesuiter"><img src="https://avatars.githubusercontent.com/u/2365676?v=4&s=48" width="48" height="48" alt="bjesuiter" title="bjesuiter"/></a> <a href="https://github.com/kyleok"><img src="https://avatars.githubusercontent.com/u/58307870?v=4&s=48" width="48" height="48" alt="kyleok" title="kyleok"/></a> <a href="https://github.com/slonce70"><img src="https://avatars.githubusercontent.com/u/130596182?v=4&s=48" width="48" height="48" alt="slonce70" title="slonce70"/></a> <a href="https://github.com/mcinteerj"><img src="https://avatars.githubusercontent.com/u/3613653?v=4&s=48" width="48" height="48" alt="mcinteerj" title="mcinteerj"/></a>
|
||||
<a href="https://github.com/badlogic"><img src="https://avatars.githubusercontent.com/u/514052?v=4&s=48" width="48" height="48" alt="badlogic" title="badlogic"/></a> <a href="https://github.com/apps/dependabot"><img src="https://avatars.githubusercontent.com/in/29110?v=4&s=48" width="48" height="48" alt="dependabot[bot]" title="dependabot[bot]"/></a> <a href="https://github.com/amitbiswal007"><img src="https://avatars.githubusercontent.com/u/108086198?v=4&s=48" width="48" height="48" alt="amitbiswal007" title="amitbiswal007"/></a> <a href="https://github.com/John-Rood"><img src="https://avatars.githubusercontent.com/u/62669593?v=4&s=48" width="48" height="48" alt="John-Rood" title="John-Rood"/></a> <a href="https://github.com/timkrase"><img src="https://avatars.githubusercontent.com/u/38947626?v=4&s=48" width="48" height="48" alt="timkrase" title="timkrase"/></a> <a href="https://github.com/uos-status"><img src="https://avatars.githubusercontent.com/u/255712580?v=4&s=48" width="48" height="48" alt="uos-status" title="uos-status"/></a> <a href="https://github.com/gerardward2007"><img src="https://avatars.githubusercontent.com/u/3002155?v=4&s=48" width="48" height="48" alt="gerardward2007" title="gerardward2007"/></a> <a href="https://github.com/roshanasingh4"><img src="https://avatars.githubusercontent.com/u/88576930?v=4&s=48" width="48" height="48" alt="roshanasingh4" title="roshanasingh4"/></a> <a href="https://github.com/tosh-hamburg"><img src="https://avatars.githubusercontent.com/u/58424326?v=4&s=48" width="48" height="48" alt="tosh-hamburg" title="tosh-hamburg"/></a> <a href="https://github.com/azade-c"><img src="https://avatars.githubusercontent.com/u/252790079?v=4&s=48" width="48" height="48" alt="azade-c" title="azade-c"/></a>
|
||||
<a href="https://github.com/dlauer"><img src="https://avatars.githubusercontent.com/u/757041?v=4&s=48" width="48" height="48" alt="dlauer" title="dlauer"/></a> <a href="https://github.com/grp06"><img src="https://avatars.githubusercontent.com/u/1573959?v=4&s=48" width="48" height="48" alt="grp06" title="grp06"/></a> <a href="https://github.com/JonUleis"><img src="https://avatars.githubusercontent.com/u/7644941?v=4&s=48" width="48" height="48" alt="JonUleis" title="JonUleis"/></a> <a href="https://github.com/shivamraut101"><img src="https://avatars.githubusercontent.com/u/110457469?v=4&s=48" width="48" height="48" alt="shivamraut101" title="shivamraut101"/></a> <a href="https://github.com/cheeeee"><img src="https://avatars.githubusercontent.com/u/21245729?v=4&s=48" width="48" height="48" alt="cheeeee" title="cheeeee"/></a> <a href="https://github.com/robbyczgw-cla"><img src="https://avatars.githubusercontent.com/u/239660374?v=4&s=48" width="48" height="48" alt="robbyczgw-cla" title="robbyczgw-cla"/></a> <a href="https://github.com/YuriNachos"><img src="https://avatars.githubusercontent.com/u/19365375?v=4&s=48" width="48" height="48" alt="YuriNachos" title="YuriNachos"/></a> <a href="https://github.com/j1philli"><img src="https://avatars.githubusercontent.com/u/3744255?v=4&s=48" width="48" height="48" alt="Josh Phillips" title="Josh Phillips"/></a> <a href="https://github.com/Wangnov"><img src="https://avatars.githubusercontent.com/u/48670012?v=4&s=48" width="48" height="48" alt="Wangnov" title="Wangnov"/></a> <a href="https://github.com/kaizen403"><img src="https://avatars.githubusercontent.com/u/134706404?v=4&s=48" width="48" height="48" alt="kaizen403" title="kaizen403"/></a>
|
||||
<a href="https://github.com/pookNast"><img src="https://avatars.githubusercontent.com/u/14242552?v=4&s=48" width="48" height="48" alt="pookNast" title="pookNast"/></a> <a href="https://github.com/Whoaa512"><img src="https://avatars.githubusercontent.com/u/1581943?v=4&s=48" width="48" height="48" alt="Whoaa512" title="Whoaa512"/></a> <a href="https://github.com/chriseidhof"><img src="https://avatars.githubusercontent.com/u/5382?v=4&s=48" width="48" height="48" alt="chriseidhof" title="chriseidhof"/></a> <a href="https://github.com/ngutman"><img src="https://avatars.githubusercontent.com/u/1540134?v=4&s=48" width="48" height="48" alt="ngutman" title="ngutman"/></a> <a href="https://github.com/therealZpoint-bot"><img src="https://avatars.githubusercontent.com/u/258706705?v=4&s=48" width="48" height="48" alt="therealZpoint-bot" title="therealZpoint-bot"/></a> <a href="https://github.com/wangai-studio"><img src="https://avatars.githubusercontent.com/u/256938352?v=4&s=48" width="48" height="48" alt="wangai-studio" title="wangai-studio"/></a> <a href="https://github.com/ysqander"><img src="https://avatars.githubusercontent.com/u/80843820?v=4&s=48" width="48" height="48" alt="ysqander" title="ysqander"/></a> <a href="https://github.com/search?q=Yurii%20Chukhlib"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Yurii Chukhlib" title="Yurii Chukhlib"/></a> <a href="https://github.com/aj47"><img src="https://avatars.githubusercontent.com/u/8023513?v=4&s=48" width="48" height="48" alt="aj47" title="aj47"/></a> <a href="https://github.com/kennyklee"><img src="https://avatars.githubusercontent.com/u/1432489?v=4&s=48" width="48" height="48" alt="kennyklee" title="kennyklee"/></a>
|
||||
<a href="https://github.com/superman32432432"><img src="https://avatars.githubusercontent.com/u/7228420?v=4&s=48" width="48" height="48" alt="superman32432432" title="superman32432432"/></a> <a href="https://github.com/Hisleren"><img src="https://avatars.githubusercontent.com/u/83217244?v=4&s=48" width="48" height="48" alt="Hisleren" title="Hisleren"/></a> <a href="https://github.com/shatner"><img src="https://avatars.githubusercontent.com/u/17735435?v=4&s=48" width="48" height="48" alt="shatner" title="shatner"/></a> <a href="https://github.com/antons"><img src="https://avatars.githubusercontent.com/u/129705?v=4&s=48" width="48" height="48" alt="antons" title="antons"/></a> <a href="https://github.com/austinm911"><img src="https://avatars.githubusercontent.com/u/31991302?v=4&s=48" width="48" height="48" alt="austinm911" title="austinm911"/></a> <a href="https://github.com/apps/blacksmith-sh"><img src="https://avatars.githubusercontent.com/in/807020?v=4&s=48" width="48" height="48" alt="blacksmith-sh[bot]" title="blacksmith-sh[bot]"/></a> <a href="https://github.com/damoahdominic"><img src="https://avatars.githubusercontent.com/u/4623434?v=4&s=48" width="48" height="48" alt="damoahdominic" title="damoahdominic"/></a> <a href="https://github.com/dan-dr"><img src="https://avatars.githubusercontent.com/u/6669808?v=4&s=48" width="48" height="48" alt="dan-dr" title="dan-dr"/></a> <a href="https://github.com/GHesericsu"><img src="https://avatars.githubusercontent.com/u/60202455?v=4&s=48" width="48" height="48" alt="GHesericsu" title="GHesericsu"/></a> <a href="https://github.com/HeimdallStrategy"><img src="https://avatars.githubusercontent.com/u/223014405?v=4&s=48" width="48" height="48" alt="HeimdallStrategy" title="HeimdallStrategy"/></a>
|
||||
<a href="https://github.com/imfing"><img src="https://avatars.githubusercontent.com/u/5097752?v=4&s=48" width="48" height="48" alt="imfing" title="imfing"/></a> <a href="https://github.com/jalehman"><img src="https://avatars.githubusercontent.com/u/550978?v=4&s=48" width="48" height="48" alt="jalehman" title="jalehman"/></a> <a href="https://github.com/jarvis-medmatic"><img src="https://avatars.githubusercontent.com/u/252428873?v=4&s=48" width="48" height="48" alt="jarvis-medmatic" title="jarvis-medmatic"/></a> <a href="https://github.com/kkarimi"><img src="https://avatars.githubusercontent.com/u/875218?v=4&s=48" width="48" height="48" alt="kkarimi" title="kkarimi"/></a> <a href="https://github.com/Lukavyi"><img src="https://avatars.githubusercontent.com/u/1013690?v=4&s=48" width="48" height="48" alt="Lukavyi" title="Lukavyi"/></a> <a href="https://github.com/mahmoudashraf93"><img src="https://avatars.githubusercontent.com/u/9130129?v=4&s=48" width="48" height="48" alt="mahmoudashraf93" title="mahmoudashraf93"/></a> <a href="https://github.com/pkrmf"><img src="https://avatars.githubusercontent.com/u/1714267?v=4&s=48" width="48" height="48" alt="pkrmf" title="pkrmf"/></a> <a href="https://github.com/RandyVentures"><img src="https://avatars.githubusercontent.com/u/149904821?v=4&s=48" width="48" height="48" alt="RandyVentures" title="RandyVentures"/></a> <a href="https://github.com/robhparker"><img src="https://avatars.githubusercontent.com/u/7404740?v=4&s=48" width="48" height="48" alt="robhparker" title="robhparker"/></a> <a href="https://github.com/search?q=Ryan%20Lisse"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ryan Lisse" title="Ryan Lisse"/></a>
|
||||
<a href="https://github.com/Yeom-JinHo"><img src="https://avatars.githubusercontent.com/u/81306489?v=4&s=48" width="48" height="48" alt="Yeom-JinHo" title="Yeom-JinHo"/></a> <a href="https://github.com/doodlewind"><img src="https://avatars.githubusercontent.com/u/7312949?v=4&s=48" width="48" height="48" alt="doodlewind" title="doodlewind"/></a> <a href="https://github.com/dougvk"><img src="https://avatars.githubusercontent.com/u/401660?v=4&s=48" width="48" height="48" alt="dougvk" title="dougvk"/></a> <a href="https://github.com/erikpr1994"><img src="https://avatars.githubusercontent.com/u/6299331?v=4&s=48" width="48" height="48" alt="erikpr1994" title="erikpr1994"/></a> <a href="https://github.com/fal3"><img src="https://avatars.githubusercontent.com/u/6484295?v=4&s=48" width="48" height="48" alt="fal3" title="fal3"/></a> <a href="https://github.com/search?q=Ghost"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ghost" title="Ghost"/></a> <a href="https://github.com/hyf0-agent"><img src="https://avatars.githubusercontent.com/u/258783736?v=4&s=48" width="48" height="48" alt="hyf0-agent" title="hyf0-agent"/></a> <a href="https://github.com/jonasjancarik"><img src="https://avatars.githubusercontent.com/u/2459191?v=4&s=48" width="48" height="48" alt="jonasjancarik" title="jonasjancarik"/></a> <a href="https://github.com/search?q=Keith%20the%20Silly%20Goose"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Keith the Silly Goose" title="Keith the Silly Goose"/></a> <a href="https://github.com/search?q=L36%20Server"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="L36 Server" title="L36 Server"/></a>
|
||||
<a href="https://github.com/search?q=Marc"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Marc" title="Marc"/></a> <a href="https://github.com/mitschabaude-bot"><img src="https://avatars.githubusercontent.com/u/247582884?v=4&s=48" width="48" height="48" alt="mitschabaude-bot" title="mitschabaude-bot"/></a> <a href="https://github.com/mkbehr"><img src="https://avatars.githubusercontent.com/u/1285?v=4&s=48" width="48" height="48" alt="mkbehr" title="mkbehr"/></a> <a href="https://github.com/neist"><img src="https://avatars.githubusercontent.com/u/1029724?v=4&s=48" width="48" height="48" alt="neist" title="neist"/></a> <a href="https://github.com/sibbl"><img src="https://avatars.githubusercontent.com/u/866535?v=4&s=48" width="48" height="48" alt="sibbl" title="sibbl"/></a> <a href="https://github.com/zats"><img src="https://avatars.githubusercontent.com/u/2688806?v=4&s=48" width="48" height="48" alt="zats" title="zats"/></a> <a href="https://github.com/abhijeet117"><img src="https://avatars.githubusercontent.com/u/192859219?v=4&s=48" width="48" height="48" alt="abhijeet117" title="abhijeet117"/></a> <a href="https://github.com/chrisrodz"><img src="https://avatars.githubusercontent.com/u/2967620?v=4&s=48" width="48" height="48" alt="chrisrodz" title="chrisrodz"/></a> <a href="https://github.com/search?q=Friederike%20Seiler"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Friederike Seiler" title="Friederike Seiler"/></a> <a href="https://github.com/gabriel-trigo"><img src="https://avatars.githubusercontent.com/u/38991125?v=4&s=48" width="48" height="48" alt="gabriel-trigo" title="gabriel-trigo"/></a>
|
||||
<a href="https://github.com/Iamadig"><img src="https://avatars.githubusercontent.com/u/102129234?v=4&s=48" width="48" height="48" alt="iamadig" title="iamadig"/></a> <a href="https://github.com/itsjling"><img src="https://avatars.githubusercontent.com/u/2521993?v=4&s=48" width="48" height="48" alt="itsjling" title="itsjling"/></a> <a href="https://github.com/jdrhyne"><img src="https://avatars.githubusercontent.com/u/7828464?v=4&s=48" width="48" height="48" alt="Jonathan D. Rhyne (DJ-D)" title="Jonathan D. Rhyne (DJ-D)"/></a> <a href="https://github.com/search?q=Joshua%20Mitchell"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Joshua Mitchell" title="Joshua Mitchell"/></a> <a href="https://github.com/kelvinCB"><img src="https://avatars.githubusercontent.com/u/50544379?v=4&s=48" width="48" height="48" alt="kelvinCB" title="kelvinCB"/></a> <a href="https://github.com/search?q=Kit"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kit" title="Kit"/></a> <a href="https://github.com/koala73"><img src="https://avatars.githubusercontent.com/u/996596?v=4&s=48" width="48" height="48" alt="koala73" title="koala73"/></a> <a href="https://github.com/manmal"><img src="https://avatars.githubusercontent.com/u/142797?v=4&s=48" width="48" height="48" alt="manmal" title="manmal"/></a> <a href="https://github.com/mattqdev"><img src="https://avatars.githubusercontent.com/u/115874885?v=4&s=48" width="48" height="48" alt="mattqdev" title="mattqdev"/></a> <a href="https://github.com/mitsuhiko"><img src="https://avatars.githubusercontent.com/u/7396?v=4&s=48" width="48" height="48" alt="mitsuhiko" title="mitsuhiko"/></a>
|
||||
<a href="https://github.com/ogulcancelik"><img src="https://avatars.githubusercontent.com/u/7064011?v=4&s=48" width="48" height="48" alt="ogulcancelik" title="ogulcancelik"/></a> <a href="https://github.com/pasogott"><img src="https://avatars.githubusercontent.com/u/23458152?v=4&s=48" width="48" height="48" alt="pasogott" title="pasogott"/></a> <a href="https://github.com/petradonka"><img src="https://avatars.githubusercontent.com/u/7353770?v=4&s=48" width="48" height="48" alt="petradonka" title="petradonka"/></a> <a href="https://github.com/rubyrunsstuff"><img src="https://avatars.githubusercontent.com/u/246602379?v=4&s=48" width="48" height="48" alt="rubyrunsstuff" title="rubyrunsstuff"/></a> <a href="https://github.com/siddhantjain"><img src="https://avatars.githubusercontent.com/u/4835232?v=4&s=48" width="48" height="48" alt="siddhantjain" title="siddhantjain"/></a> <a href="https://github.com/spiceoogway"><img src="https://avatars.githubusercontent.com/u/105812383?v=4&s=48" width="48" height="48" alt="spiceoogway" title="spiceoogway"/></a> <a href="https://github.com/suminhthanh"><img src="https://avatars.githubusercontent.com/u/2907636?v=4&s=48" width="48" height="48" alt="suminhthanh" title="suminhthanh"/></a> <a href="https://github.com/svkozak"><img src="https://avatars.githubusercontent.com/u/31941359?v=4&s=48" width="48" height="48" alt="svkozak" title="svkozak"/></a> <a href="https://github.com/wes-davis"><img src="https://avatars.githubusercontent.com/u/16506720?v=4&s=48" width="48" height="48" alt="wes-davis" title="wes-davis"/></a> <a href="https://github.com/24601"><img src="https://avatars.githubusercontent.com/u/1157207?v=4&s=48" width="48" height="48" alt="24601" title="24601"/></a>
|
||||
<a href="https://github.com/ameno-"><img src="https://avatars.githubusercontent.com/u/2416135?v=4&s=48" width="48" height="48" alt="ameno-" title="ameno-"/></a> <a href="https://github.com/bonald"><img src="https://avatars.githubusercontent.com/u/12394874?v=4&s=48" width="48" height="48" alt="bonald" title="bonald"/></a> <a href="https://github.com/bravostation"><img src="https://avatars.githubusercontent.com/u/257991910?v=4&s=48" width="48" height="48" alt="bravostation" title="bravostation"/></a> <a href="https://github.com/search?q=Chris%20Taylor"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Chris Taylor" title="Chris Taylor"/></a> <a href="https://github.com/dguido"><img src="https://avatars.githubusercontent.com/u/294844?v=4&s=48" width="48" height="48" alt="dguido" title="dguido"/></a> <a href="https://github.com/djangonavarro220"><img src="https://avatars.githubusercontent.com/u/251162586?v=4&s=48" width="48" height="48" alt="Django Navarro" title="Django Navarro"/></a> <a href="https://github.com/evalexpr"><img src="https://avatars.githubusercontent.com/u/23485511?v=4&s=48" width="48" height="48" alt="evalexpr" title="evalexpr"/></a> <a href="https://github.com/henrino3"><img src="https://avatars.githubusercontent.com/u/4260288?v=4&s=48" width="48" height="48" alt="henrino3" title="henrino3"/></a> <a href="https://github.com/humanwritten"><img src="https://avatars.githubusercontent.com/u/206531610?v=4&s=48" width="48" height="48" alt="humanwritten" title="humanwritten"/></a> <a href="https://github.com/j2h4u"><img src="https://avatars.githubusercontent.com/u/39818683?v=4&s=48" width="48" height="48" alt="j2h4u" title="j2h4u"/></a>
|
||||
<a href="https://github.com/larlyssa"><img src="https://avatars.githubusercontent.com/u/13128869?v=4&s=48" width="48" height="48" alt="larlyssa" title="larlyssa"/></a> <a href="https://github.com/odysseus0"><img src="https://avatars.githubusercontent.com/u/8635094?v=4&s=48" width="48" height="48" alt="odysseus0" title="odysseus0"/></a> <a href="https://github.com/oswalpalash"><img src="https://avatars.githubusercontent.com/u/6431196?v=4&s=48" width="48" height="48" alt="oswalpalash" title="oswalpalash"/></a> <a href="https://github.com/pcty-nextgen-service-account"><img src="https://avatars.githubusercontent.com/u/112553441?v=4&s=48" width="48" height="48" alt="pcty-nextgen-service-account" title="pcty-nextgen-service-account"/></a> <a href="https://github.com/pi0"><img src="https://avatars.githubusercontent.com/u/5158436?v=4&s=48" width="48" height="48" alt="pi0" title="pi0"/></a> <a href="https://github.com/rmorse"><img src="https://avatars.githubusercontent.com/u/853547?v=4&s=48" width="48" height="48" alt="rmorse" title="rmorse"/></a> <a href="https://github.com/search?q=Roopak%20Nijhara"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Roopak Nijhara" title="Roopak Nijhara"/></a> <a href="https://github.com/Syhids"><img src="https://avatars.githubusercontent.com/u/671202?v=4&s=48" width="48" height="48" alt="Syhids" title="Syhids"/></a> <a href="https://github.com/search?q=Ubuntu"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ubuntu" title="Ubuntu"/></a> <a href="https://github.com/search?q=xiaose"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="xiaose" title="xiaose"/></a>
|
||||
<a href="https://github.com/search?q=Aaron%20Konyer"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Aaron Konyer" title="Aaron Konyer"/></a> <a href="https://github.com/aaronveklabs"><img src="https://avatars.githubusercontent.com/u/225997828?v=4&s=48" width="48" height="48" alt="aaronveklabs" title="aaronveklabs"/></a> <a href="https://github.com/aldoeliacim"><img src="https://avatars.githubusercontent.com/u/17973757?v=4&s=48" width="48" height="48" alt="aldoeliacim" title="aldoeliacim"/></a> <a href="https://github.com/andreabadesso"><img src="https://avatars.githubusercontent.com/u/3586068?v=4&s=48" width="48" height="48" alt="andreabadesso" title="andreabadesso"/></a> <a href="https://github.com/search?q=Andrii"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Andrii" title="Andrii"/></a> <a href="https://github.com/BinaryMuse"><img src="https://avatars.githubusercontent.com/u/189606?v=4&s=48" width="48" height="48" alt="BinaryMuse" title="BinaryMuse"/></a> <a href="https://github.com/bqcfjwhz85-arch"><img src="https://avatars.githubusercontent.com/u/239267175?v=4&s=48" width="48" height="48" alt="bqcfjwhz85-arch" title="bqcfjwhz85-arch"/></a> <a href="https://github.com/cash-echo-bot"><img src="https://avatars.githubusercontent.com/u/252747386?v=4&s=48" width="48" height="48" alt="cash-echo-bot" title="cash-echo-bot"/></a> <a href="https://github.com/search?q=Clawd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Clawd" title="Clawd"/></a> <a href="https://github.com/search?q=ClawdFx"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ClawdFx" title="ClawdFx"/></a>
|
||||
<a href="https://github.com/search?q=damaozi"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="damaozi" title="damaozi"/></a> <a href="https://github.com/danballance"><img src="https://avatars.githubusercontent.com/u/13839912?v=4&s=48" width="48" height="48" alt="danballance" title="danballance"/></a> <a href="https://github.com/Elarwei001"><img src="https://avatars.githubusercontent.com/u/168552401?v=4&s=48" width="48" height="48" alt="Elarwei001" title="Elarwei001"/></a> <a href="https://github.com/EnzeD"><img src="https://avatars.githubusercontent.com/u/9866900?v=4&s=48" width="48" height="48" alt="EnzeD" title="EnzeD"/></a> <a href="https://github.com/erik-agens"><img src="https://avatars.githubusercontent.com/u/80908960?v=4&s=48" width="48" height="48" alt="erik-agens" title="erik-agens"/></a> <a href="https://github.com/Evizero"><img src="https://avatars.githubusercontent.com/u/10854026?v=4&s=48" width="48" height="48" alt="Evizero" title="Evizero"/></a> <a href="https://github.com/fcatuhe"><img src="https://avatars.githubusercontent.com/u/17382215?v=4&s=48" width="48" height="48" alt="fcatuhe" title="fcatuhe"/></a> <a href="https://github.com/gildo"><img src="https://avatars.githubusercontent.com/u/133645?v=4&s=48" width="48" height="48" alt="gildo" title="gildo"/></a> <a href="https://github.com/hclsys"><img src="https://avatars.githubusercontent.com/u/7755017?v=4&s=48" width="48" height="48" alt="hclsys" title="hclsys"/></a> <a href="https://github.com/itsjaydesu"><img src="https://avatars.githubusercontent.com/u/220390?v=4&s=48" width="48" height="48" alt="itsjaydesu" title="itsjaydesu"/></a>
|
||||
<a href="https://github.com/ivancasco"><img src="https://avatars.githubusercontent.com/u/2452858?v=4&s=48" width="48" height="48" alt="ivancasco" title="ivancasco"/></a> <a href="https://github.com/ivanrvpereira"><img src="https://avatars.githubusercontent.com/u/183991?v=4&s=48" width="48" height="48" alt="ivanrvpereira" title="ivanrvpereira"/></a> <a href="https://github.com/search?q=Jarvis"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jarvis" title="Jarvis"/></a> <a href="https://github.com/jayhickey"><img src="https://avatars.githubusercontent.com/u/1676460?v=4&s=48" width="48" height="48" alt="jayhickey" title="jayhickey"/></a> <a href="https://github.com/jeffersonwarrior"><img src="https://avatars.githubusercontent.com/u/89030989?v=4&s=48" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a> <a href="https://github.com/search?q=jeffersonwarrior"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a> <a href="https://github.com/jverdi"><img src="https://avatars.githubusercontent.com/u/345050?v=4&s=48" width="48" height="48" alt="jverdi" title="jverdi"/></a> <a href="https://github.com/lailoo"><img src="https://avatars.githubusercontent.com/u/20536249?v=4&s=48" width="48" height="48" alt="lailoo" title="lailoo"/></a> <a href="https://github.com/longmaba"><img src="https://avatars.githubusercontent.com/u/9361500?v=4&s=48" width="48" height="48" alt="longmaba" title="longmaba"/></a> <a href="https://github.com/search?q=Marco%20Marandiz"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Marco Marandiz" title="Marco Marandiz"/></a>
|
||||
<a href="https://github.com/MarvinCui"><img src="https://avatars.githubusercontent.com/u/130876763?v=4&s=48" width="48" height="48" alt="MarvinCui" title="MarvinCui"/></a> <a href="https://github.com/mattezell"><img src="https://avatars.githubusercontent.com/u/361409?v=4&s=48" width="48" height="48" alt="mattezell" title="mattezell"/></a> <a href="https://github.com/mjrussell"><img src="https://avatars.githubusercontent.com/u/1641895?v=4&s=48" width="48" height="48" alt="mjrussell" title="mjrussell"/></a> <a href="https://github.com/odnxe"><img src="https://avatars.githubusercontent.com/u/403141?v=4&s=48" width="48" height="48" alt="odnxe" title="odnxe"/></a> <a href="https://github.com/optimikelabs"><img src="https://avatars.githubusercontent.com/u/31423109?v=4&s=48" width="48" height="48" alt="optimikelabs" title="optimikelabs"/></a> <a href="https://github.com/p6l-richard"><img src="https://avatars.githubusercontent.com/u/18185649?v=4&s=48" width="48" height="48" alt="p6l-richard" title="p6l-richard"/></a> <a href="https://github.com/philipp-spiess"><img src="https://avatars.githubusercontent.com/u/458591?v=4&s=48" width="48" height="48" alt="philipp-spiess" title="philipp-spiess"/></a> <a href="https://github.com/search?q=Pocket%20Clawd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Pocket Clawd" title="Pocket Clawd"/></a> <a href="https://github.com/robaxelsen"><img src="https://avatars.githubusercontent.com/u/13132899?v=4&s=48" width="48" height="48" alt="robaxelsen" title="robaxelsen"/></a> <a href="https://github.com/search?q=Sash%20Catanzarite"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Sash Catanzarite" title="Sash Catanzarite"/></a>
|
||||
<a href="https://github.com/Suksham-sharma"><img src="https://avatars.githubusercontent.com/u/94667656?v=4&s=48" width="48" height="48" alt="Suksham-sharma" title="Suksham-sharma"/></a> <a href="https://github.com/T5-AndyML"><img src="https://avatars.githubusercontent.com/u/22801233?v=4&s=48" width="48" height="48" alt="T5-AndyML" title="T5-AndyML"/></a> <a href="https://github.com/tewatia"><img src="https://avatars.githubusercontent.com/u/22875334?v=4&s=48" width="48" height="48" alt="tewatia" title="tewatia"/></a> <a href="https://github.com/thejhinvirtuoso"><img src="https://avatars.githubusercontent.com/u/258521837?v=4&s=48" width="48" height="48" alt="thejhinvirtuoso" title="thejhinvirtuoso"/></a> <a href="https://github.com/travisp"><img src="https://avatars.githubusercontent.com/u/165698?v=4&s=48" width="48" height="48" alt="travisp" title="travisp"/></a> <a href="https://github.com/search?q=VAC"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="VAC" title="VAC"/></a> <a href="https://github.com/search?q=william%20arzt"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="william arzt" title="william arzt"/></a> <a href="https://github.com/yudshj"><img src="https://avatars.githubusercontent.com/u/16971372?v=4&s=48" width="48" height="48" alt="yudshj" title="yudshj"/></a> <a href="https://github.com/zknicker"><img src="https://avatars.githubusercontent.com/u/1164085?v=4&s=48" width="48" height="48" alt="zknicker" title="zknicker"/></a> <a href="https://github.com/0oAstro"><img src="https://avatars.githubusercontent.com/u/79555780?v=4&s=48" width="48" height="48" alt="0oAstro" title="0oAstro"/></a>
|
||||
<a href="https://github.com/abhaymundhara"><img src="https://avatars.githubusercontent.com/u/62872231?v=4&s=48" width="48" height="48" alt="abhaymundhara" title="abhaymundhara"/></a> <a href="https://github.com/aduk059"><img src="https://avatars.githubusercontent.com/u/257603478?v=4&s=48" width="48" height="48" alt="aduk059" title="aduk059"/></a> <a href="https://github.com/aisling404"><img src="https://avatars.githubusercontent.com/u/211950534?v=4&s=48" width="48" height="48" alt="aisling404" title="aisling404"/></a> <a href="https://github.com/akramcodez"><img src="https://avatars.githubusercontent.com/u/179671552?v=4&s=48" width="48" height="48" alt="akramcodez" title="akramcodez"/></a> <a href="https://github.com/search?q=alejandro%20maza"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="alejandro maza" title="alejandro maza"/></a> <a href="https://github.com/Alex-Alaniz"><img src="https://avatars.githubusercontent.com/u/88956822?v=4&s=48" width="48" height="48" alt="Alex-Alaniz" title="Alex-Alaniz"/></a> <a href="https://github.com/alexanderatallah"><img src="https://avatars.githubusercontent.com/u/1011391?v=4&s=48" width="48" height="48" alt="alexanderatallah" title="alexanderatallah"/></a> <a href="https://github.com/alexstyl"><img src="https://avatars.githubusercontent.com/u/1665273?v=4&s=48" width="48" height="48" alt="alexstyl" title="alexstyl"/></a> <a href="https://github.com/AlexZhangji"><img src="https://avatars.githubusercontent.com/u/3280924?v=4&s=48" width="48" height="48" alt="AlexZhangji" title="AlexZhangji"/></a> <a href="https://github.com/andrewting19"><img src="https://avatars.githubusercontent.com/u/10536704?v=4&s=48" width="48" height="48" alt="andrewting19" title="andrewting19"/></a>
|
||||
<a href="https://github.com/anpoirier"><img src="https://avatars.githubusercontent.com/u/1245729?v=4&s=48" width="48" height="48" alt="anpoirier" title="anpoirier"/></a> <a href="https://github.com/araa47"><img src="https://avatars.githubusercontent.com/u/22760261?v=4&s=48" width="48" height="48" alt="araa47" title="araa47"/></a> <a href="https://github.com/arthyn"><img src="https://avatars.githubusercontent.com/u/5466421?v=4&s=48" width="48" height="48" alt="arthyn" title="arthyn"/></a> <a href="https://github.com/Asleep123"><img src="https://avatars.githubusercontent.com/u/122379135?v=4&s=48" width="48" height="48" alt="Asleep123" title="Asleep123"/></a> <a href="https://github.com/search?q=Ayush%20Ojha"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ayush Ojha" title="Ayush Ojha"/></a> <a href="https://github.com/Ayush10"><img src="https://avatars.githubusercontent.com/u/7945279?v=4&s=48" width="48" height="48" alt="Ayush10" title="Ayush10"/></a> <a href="https://github.com/bguidolim"><img src="https://avatars.githubusercontent.com/u/987360?v=4&s=48" width="48" height="48" alt="bguidolim" title="bguidolim"/></a> <a href="https://github.com/bolismauro"><img src="https://avatars.githubusercontent.com/u/771999?v=4&s=48" width="48" height="48" alt="bolismauro" title="bolismauro"/></a> <a href="https://github.com/caelum0x"><img src="https://avatars.githubusercontent.com/u/130079063?v=4&s=48" width="48" height="48" alt="caelum0x" title="caelum0x"/></a> <a href="https://github.com/championswimmer"><img src="https://avatars.githubusercontent.com/u/1327050?v=4&s=48" width="48" height="48" alt="championswimmer" title="championswimmer"/></a>
|
||||
<a href="https://github.com/chenyuan99"><img src="https://avatars.githubusercontent.com/u/25518100?v=4&s=48" width="48" height="48" alt="chenyuan99" title="chenyuan99"/></a> <a href="https://github.com/Chloe-VP"><img src="https://avatars.githubusercontent.com/u/257371598?v=4&s=48" width="48" height="48" alt="Chloe-VP" title="Chloe-VP"/></a> <a href="https://github.com/search?q=Clawdbot%20Maintainers"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Clawdbot Maintainers" title="Clawdbot Maintainers"/></a> <a href="https://github.com/conhecendoia"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendoia" title="conhecendoia"/></a> <a href="https://github.com/dasilva333"><img src="https://avatars.githubusercontent.com/u/947827?v=4&s=48" width="48" height="48" alt="dasilva333" title="dasilva333"/></a> <a href="https://github.com/David-Marsh-Photo"><img src="https://avatars.githubusercontent.com/u/228404527?v=4&s=48" width="48" height="48" alt="David-Marsh-Photo" title="David-Marsh-Photo"/></a> <a href="https://github.com/deepsoumya617"><img src="https://avatars.githubusercontent.com/u/80877391?v=4&s=48" width="48" height="48" alt="deepsoumya617" title="deepsoumya617"/></a> <a href="https://github.com/search?q=Developer"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Developer" title="Developer"/></a> <a href="https://github.com/search?q=Dimitrios%20Ploutarchos"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Dimitrios Ploutarchos" title="Dimitrios Ploutarchos"/></a> <a href="https://github.com/search?q=Drake%20Thomsen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Drake Thomsen" title="Drake Thomsen"/></a>
|
||||
<a href="https://github.com/dvrshil"><img src="https://avatars.githubusercontent.com/u/81693876?v=4&s=48" width="48" height="48" alt="dvrshil" title="dvrshil"/></a> <a href="https://github.com/dxd5001"><img src="https://avatars.githubusercontent.com/u/1886046?v=4&s=48" width="48" height="48" alt="dxd5001" title="dxd5001"/></a> <a href="https://github.com/dylanneve1"><img src="https://avatars.githubusercontent.com/u/31746704?v=4&s=48" width="48" height="48" alt="dylanneve1" title="dylanneve1"/></a> <a href="https://github.com/search?q=Felix%20Krause"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Felix Krause" title="Felix Krause"/></a> <a href="https://github.com/foeken"><img src="https://avatars.githubusercontent.com/u/13864?v=4&s=48" width="48" height="48" alt="foeken" title="foeken"/></a> <a href="https://github.com/frankekn"><img src="https://avatars.githubusercontent.com/u/4488090?v=4&s=48" width="48" height="48" alt="frankekn" title="frankekn"/></a> <a href="https://github.com/fredheir"><img src="https://avatars.githubusercontent.com/u/3304869?v=4&s=48" width="48" height="48" alt="fredheir" title="fredheir"/></a> <a href="https://github.com/search?q=ganghyun%20kim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ganghyun kim" title="ganghyun kim"/></a> <a href="https://github.com/grrowl"><img src="https://avatars.githubusercontent.com/u/907140?v=4&s=48" width="48" height="48" alt="grrowl" title="grrowl"/></a> <a href="https://github.com/gtsifrikas"><img src="https://avatars.githubusercontent.com/u/8904378?v=4&s=48" width="48" height="48" alt="gtsifrikas" title="gtsifrikas"/></a>
|
||||
<a href="https://github.com/HassanFleyah"><img src="https://avatars.githubusercontent.com/u/228002017?v=4&s=48" width="48" height="48" alt="HassanFleyah" title="HassanFleyah"/></a> <a href="https://github.com/HazAT"><img src="https://avatars.githubusercontent.com/u/363802?v=4&s=48" width="48" height="48" alt="HazAT" title="HazAT"/></a> <a href="https://github.com/hrdwdmrbl"><img src="https://avatars.githubusercontent.com/u/554881?v=4&s=48" width="48" height="48" alt="hrdwdmrbl" title="hrdwdmrbl"/></a> <a href="https://github.com/hugobarauna"><img src="https://avatars.githubusercontent.com/u/2719?v=4&s=48" width="48" height="48" alt="hugobarauna" title="hugobarauna"/></a> <a href="https://github.com/iamEvanYT"><img src="https://avatars.githubusercontent.com/u/47493765?v=4&s=48" width="48" height="48" alt="iamEvanYT" title="iamEvanYT"/></a> <a href="https://github.com/ichbinlucaskim"><img src="https://avatars.githubusercontent.com/u/125564751?v=4&s=48" width="48" height="48" alt="ichbinlucaskim" title="ichbinlucaskim"/></a> <a href="https://github.com/search?q=Jamie%20Openshaw"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jamie Openshaw" title="Jamie Openshaw"/></a> <a href="https://github.com/search?q=Jane"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jane" title="Jane"/></a> <a href="https://github.com/search?q=Jarvis%20Deploy"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jarvis Deploy" title="Jarvis Deploy"/></a> <a href="https://github.com/search?q=Jefferson%20Nunn"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jefferson Nunn" title="Jefferson Nunn"/></a>
|
||||
<a href="https://github.com/jogi47"><img src="https://avatars.githubusercontent.com/u/1710139?v=4&s=48" width="48" height="48" alt="jogi47" title="jogi47"/></a> <a href="https://github.com/kentaro"><img src="https://avatars.githubusercontent.com/u/3458?v=4&s=48" width="48" height="48" alt="kentaro" title="kentaro"/></a> <a href="https://github.com/search?q=Kevin%20Lin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kevin Lin" title="Kevin Lin"/></a> <a href="https://github.com/kira-ariaki"><img src="https://avatars.githubusercontent.com/u/257352493?v=4&s=48" width="48" height="48" alt="kira-ariaki" title="kira-ariaki"/></a> <a href="https://github.com/kitze"><img src="https://avatars.githubusercontent.com/u/1160594?v=4&s=48" width="48" height="48" alt="kitze" title="kitze"/></a> <a href="https://github.com/Kiwitwitter"><img src="https://avatars.githubusercontent.com/u/25277769?v=4&s=48" width="48" height="48" alt="Kiwitwitter" title="Kiwitwitter"/></a> <a href="https://github.com/levifig"><img src="https://avatars.githubusercontent.com/u/1605?v=4&s=48" width="48" height="48" alt="levifig" title="levifig"/></a> <a href="https://github.com/search?q=Lloyd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Lloyd" title="Lloyd"/></a> <a href="https://github.com/loganaden"><img src="https://avatars.githubusercontent.com/u/1688420?v=4&s=48" width="48" height="48" alt="loganaden" title="loganaden"/></a> <a href="https://github.com/longjos"><img src="https://avatars.githubusercontent.com/u/740160?v=4&s=48" width="48" height="48" alt="longjos" title="longjos"/></a>
|
||||
<a href="https://github.com/loukotal"><img src="https://avatars.githubusercontent.com/u/18210858?v=4&s=48" width="48" height="48" alt="loukotal" title="loukotal"/></a> <a href="https://github.com/louzhixian"><img src="https://avatars.githubusercontent.com/u/7994361?v=4&s=48" width="48" height="48" alt="louzhixian" title="louzhixian"/></a> <a href="https://github.com/search?q=mac%20mimi"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="mac mimi" title="mac mimi"/></a> <a href="https://github.com/martinpucik"><img src="https://avatars.githubusercontent.com/u/5503097?v=4&s=48" width="48" height="48" alt="martinpucik" title="martinpucik"/></a> <a href="https://github.com/search?q=Matt%20mini"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Matt mini" title="Matt mini"/></a> <a href="https://github.com/mcaxtr"><img src="https://avatars.githubusercontent.com/u/7562095?v=4&s=48" width="48" height="48" alt="mcaxtr" title="mcaxtr"/></a> <a href="https://github.com/mertcicekci0"><img src="https://avatars.githubusercontent.com/u/179321902?v=4&s=48" width="48" height="48" alt="mertcicekci0" title="mertcicekci0"/></a> <a href="https://github.com/search?q=Miles"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Miles" title="Miles"/></a> <a href="https://github.com/mrdbstn"><img src="https://avatars.githubusercontent.com/u/58957632?v=4&s=48" width="48" height="48" alt="mrdbstn" title="mrdbstn"/></a> <a href="https://github.com/MSch"><img src="https://avatars.githubusercontent.com/u/7475?v=4&s=48" width="48" height="48" alt="MSch" title="MSch"/></a>
|
||||
<a href="https://github.com/search?q=Mustafa%20Tag%20Eldeen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mustafa Tag Eldeen" title="Mustafa Tag Eldeen"/></a> <a href="https://github.com/mylukin"><img src="https://avatars.githubusercontent.com/u/1021019?v=4&s=48" width="48" height="48" alt="mylukin" title="mylukin"/></a> <a href="https://github.com/nathanbosse"><img src="https://avatars.githubusercontent.com/u/4040669?v=4&s=48" width="48" height="48" alt="nathanbosse" title="nathanbosse"/></a> <a href="https://github.com/ndraiman"><img src="https://avatars.githubusercontent.com/u/12609607?v=4&s=48" width="48" height="48" alt="ndraiman" title="ndraiman"/></a> <a href="https://github.com/nexty5870"><img src="https://avatars.githubusercontent.com/u/3869659?v=4&s=48" width="48" height="48" alt="nexty5870" title="nexty5870"/></a> <a href="https://github.com/Noctivoro"><img src="https://avatars.githubusercontent.com/u/183974570?v=4&s=48" width="48" height="48" alt="Noctivoro" title="Noctivoro"/></a> <a href="https://github.com/Omar-Khaleel"><img src="https://avatars.githubusercontent.com/u/240748662?v=4&s=48" width="48" height="48" alt="Omar-Khaleel" title="Omar-Khaleel"/></a> <a href="https://github.com/ozgur-polat"><img src="https://avatars.githubusercontent.com/u/26483942?v=4&s=48" width="48" height="48" alt="ozgur-polat" title="ozgur-polat"/></a> <a href="https://github.com/ppamment"><img src="https://avatars.githubusercontent.com/u/2122919?v=4&s=48" width="48" height="48" alt="ppamment" title="ppamment"/></a> <a href="https://github.com/prathamdby"><img src="https://avatars.githubusercontent.com/u/134331217?v=4&s=48" width="48" height="48" alt="prathamdby" title="prathamdby"/></a>
|
||||
<a href="https://github.com/ptn1411"><img src="https://avatars.githubusercontent.com/u/57529765?v=4&s=48" width="48" height="48" alt="ptn1411" title="ptn1411"/></a> <a href="https://github.com/rafelbev"><img src="https://avatars.githubusercontent.com/u/467120?v=4&s=48" width="48" height="48" alt="rafelbev" title="rafelbev"/></a> <a href="https://github.com/reeltimeapps"><img src="https://avatars.githubusercontent.com/u/637338?v=4&s=48" width="48" height="48" alt="reeltimeapps" title="reeltimeapps"/></a> <a href="https://github.com/RLTCmpe"><img src="https://avatars.githubusercontent.com/u/10762242?v=4&s=48" width="48" height="48" alt="RLTCmpe" title="RLTCmpe"/></a> <a href="https://github.com/search?q=Rony%20Kelner"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rony Kelner" title="Rony Kelner"/></a> <a href="https://github.com/ryancnelson"><img src="https://avatars.githubusercontent.com/u/347171?v=4&s=48" width="48" height="48" alt="ryancnelson" title="ryancnelson"/></a> <a href="https://github.com/search?q=Samrat%20Jha"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Samrat Jha" title="Samrat Jha"/></a> <a href="https://github.com/senoldogann"><img src="https://avatars.githubusercontent.com/u/45736551?v=4&s=48" width="48" height="48" alt="senoldogann" title="senoldogann"/></a> <a href="https://github.com/Seredeep"><img src="https://avatars.githubusercontent.com/u/22802816?v=4&s=48" width="48" height="48" alt="Seredeep" title="Seredeep"/></a> <a href="https://github.com/sergical"><img src="https://avatars.githubusercontent.com/u/3760543?v=4&s=48" width="48" height="48" alt="sergical" title="sergical"/></a>
|
||||
<a href="https://github.com/shiv19"><img src="https://avatars.githubusercontent.com/u/9407019?v=4&s=48" width="48" height="48" alt="shiv19" title="shiv19"/></a> <a href="https://github.com/shiyuanhai"><img src="https://avatars.githubusercontent.com/u/1187370?v=4&s=48" width="48" height="48" alt="shiyuanhai" title="shiyuanhai"/></a> <a href="https://github.com/Shrinija17"><img src="https://avatars.githubusercontent.com/u/199155426?v=4&s=48" width="48" height="48" alt="Shrinija17" title="Shrinija17"/></a> <a href="https://github.com/siraht"><img src="https://avatars.githubusercontent.com/u/73152895?v=4&s=48" width="48" height="48" alt="siraht" title="siraht"/></a> <a href="https://github.com/snopoke"><img src="https://avatars.githubusercontent.com/u/249606?v=4&s=48" width="48" height="48" alt="snopoke" title="snopoke"/></a> <a href="https://github.com/stephenchen2025"><img src="https://avatars.githubusercontent.com/u/218387130?v=4&s=48" width="48" height="48" alt="stephenchen2025" title="stephenchen2025"/></a> <a href="https://github.com/search?q=techboss"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="techboss" title="techboss"/></a> <a href="https://github.com/testingabc321"><img src="https://avatars.githubusercontent.com/u/8577388?v=4&s=48" width="48" height="48" alt="testingabc321" title="testingabc321"/></a> <a href="https://github.com/search?q=The%20Admiral"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="The Admiral" title="The Admiral"/></a> <a href="https://github.com/thesash"><img src="https://avatars.githubusercontent.com/u/1166151?v=4&s=48" width="48" height="48" alt="thesash" title="thesash"/></a>
|
||||
<a href="https://github.com/search?q=Vibe%20Kanban"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Vibe Kanban" title="Vibe Kanban"/></a> <a href="https://github.com/vincentkoc"><img src="https://avatars.githubusercontent.com/u/25068?v=4&s=48" width="48" height="48" alt="vincentkoc" title="vincentkoc"/></a> <a href="https://github.com/voidserf"><img src="https://avatars.githubusercontent.com/u/477673?v=4&s=48" width="48" height="48" alt="voidserf" title="voidserf"/></a> <a href="https://github.com/search?q=Vultr-Clawd%20Admin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Vultr-Clawd Admin" title="Vultr-Clawd Admin"/></a> <a href="https://github.com/search?q=Wimmie"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Wimmie" title="Wimmie"/></a> <a href="https://github.com/search?q=wolfred"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="wolfred" title="wolfred"/></a> <a href="https://github.com/wstock"><img src="https://avatars.githubusercontent.com/u/1394687?v=4&s=48" width="48" height="48" alt="wstock" title="wstock"/></a> <a href="https://github.com/wytheme"><img src="https://avatars.githubusercontent.com/u/5009358?v=4&s=48" width="48" height="48" alt="wytheme" title="wytheme"/></a> <a href="https://github.com/YangHuang2280"><img src="https://avatars.githubusercontent.com/u/201681634?v=4&s=48" width="48" height="48" alt="YangHuang2280" title="YangHuang2280"/></a> <a href="https://github.com/yazinsai"><img src="https://avatars.githubusercontent.com/u/1846034?v=4&s=48" width="48" height="48" alt="yazinsai" title="yazinsai"/></a>
|
||||
<a href="https://github.com/yevhen"><img src="https://avatars.githubusercontent.com/u/107726?v=4&s=48" width="48" height="48" alt="yevhen" title="yevhen"/></a> <a href="https://github.com/YiWang24"><img src="https://avatars.githubusercontent.com/u/176262341?v=4&s=48" width="48" height="48" alt="YiWang24" title="YiWang24"/></a> <a href="https://github.com/search?q=ymat19"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ymat19" title="ymat19"/></a> <a href="https://github.com/search?q=Zach%20Knickerbocker"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Zach Knickerbocker" title="Zach Knickerbocker"/></a> <a href="https://github.com/zackerthescar"><img src="https://avatars.githubusercontent.com/u/38077284?v=4&s=48" width="48" height="48" alt="zackerthescar" title="zackerthescar"/></a> <a href="https://github.com/0xJonHoldsCrypto"><img src="https://avatars.githubusercontent.com/u/81202085?v=4&s=48" width="48" height="48" alt="0xJonHoldsCrypto" title="0xJonHoldsCrypto"/></a> <a href="https://github.com/aaronn"><img src="https://avatars.githubusercontent.com/u/1653630?v=4&s=48" width="48" height="48" alt="aaronn" title="aaronn"/></a> <a href="https://github.com/Alphonse-arianee"><img src="https://avatars.githubusercontent.com/u/254457365?v=4&s=48" width="48" height="48" alt="Alphonse-arianee" title="Alphonse-arianee"/></a> <a href="https://github.com/atalovesyou"><img src="https://avatars.githubusercontent.com/u/3534502?v=4&s=48" width="48" height="48" alt="atalovesyou" title="atalovesyou"/></a> <a href="https://github.com/search?q=Azade"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Azade" title="Azade"/></a>
|
||||
<a href="https://github.com/carlulsoe"><img src="https://avatars.githubusercontent.com/u/34673973?v=4&s=48" width="48" height="48" alt="carlulsoe" title="carlulsoe"/></a> <a href="https://github.com/search?q=ddyo"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ddyo" title="ddyo"/></a> <a href="https://github.com/search?q=Erik"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Erik" title="Erik"/></a> <a href="https://github.com/jiulingyun"><img src="https://avatars.githubusercontent.com/u/126459548?v=4&s=48" width="48" height="48" alt="jiulingyun" title="jiulingyun"/></a> <a href="https://github.com/latitudeki5223"><img src="https://avatars.githubusercontent.com/u/119656367?v=4&s=48" width="48" height="48" alt="latitudeki5223" title="latitudeki5223"/></a> <a href="https://github.com/search?q=Manuel%20Maly"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Manuel Maly" title="Manuel Maly"/></a> <a href="https://github.com/search?q=Mourad%20Boustani"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mourad Boustani" title="Mourad Boustani"/></a> <a href="https://github.com/odrobnik"><img src="https://avatars.githubusercontent.com/u/333270?v=4&s=48" width="48" height="48" alt="odrobnik" title="odrobnik"/></a> <a href="https://github.com/pcty-nextgen-ios-builder"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="pcty-nextgen-ios-builder" title="pcty-nextgen-ios-builder"/></a> <a href="https://github.com/search?q=Quentin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Quentin" title="Quentin"/></a>
|
||||
<a href="https://github.com/search?q=Randy%20Torres"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Randy Torres" title="Randy Torres"/></a> <a href="https://github.com/rhjoh"><img src="https://avatars.githubusercontent.com/u/105699450?v=4&s=48" width="48" height="48" alt="rhjoh" title="rhjoh"/></a> <a href="https://github.com/search?q=Rolf%20Fredheim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rolf Fredheim" title="Rolf Fredheim"/></a> <a href="https://github.com/ronak-guliani"><img src="https://avatars.githubusercontent.com/u/23518228?v=4&s=48" width="48" height="48" alt="ronak-guliani" title="ronak-guliani"/></a> <a href="https://github.com/search?q=William%20Stock"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="William Stock" title="William Stock"/></a>
|
||||
</p>
|
||||
|
||||
@@ -16,6 +16,7 @@ The best way to help the project right now is by sending PRs.
|
||||
|
||||
- Public Internet Exposure
|
||||
- Using OpenClaw in ways that the docs recommend not to
|
||||
- Prompt injection attacks
|
||||
|
||||
## Operational Guidance
|
||||
|
||||
|
||||
339
appcast.xml
339
appcast.xml
@@ -3,231 +3,164 @@
|
||||
<channel>
|
||||
<title>OpenClaw</title>
|
||||
<item>
|
||||
<title>2026.1.29</title>
|
||||
<pubDate>Fri, 30 Jan 2026 06:24:15 +0100</pubDate>
|
||||
<title>2026.2.3</title>
|
||||
<pubDate>Wed, 04 Feb 2026 17:47:10 -0800</pubDate>
|
||||
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
|
||||
<sparkle:version>8345</sparkle:version>
|
||||
<sparkle:shortVersionString>2026.1.29</sparkle:shortVersionString>
|
||||
<sparkle:version>8900</sparkle:version>
|
||||
<sparkle:shortVersionString>2026.2.3</sparkle:shortVersionString>
|
||||
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
||||
<description><![CDATA[<h2>OpenClaw 2026.1.29</h2>
|
||||
Status: stable.
|
||||
<description><![CDATA[<h2>OpenClaw 2026.2.3</h2>
|
||||
<h3>Changes</h3>
|
||||
<ul>
|
||||
<li>Rebrand: rename the npm package/CLI to <code>openclaw</code>, add a <code>openclaw</code> compatibility shim, and move extensions to the <code>@openclaw/*</code> scope.</li>
|
||||
<li>Onboarding: strengthen security warning copy for beta + access control expectations.</li>
|
||||
<li>Onboarding: add Venice API key to non-interactive flow. (#1893) Thanks @jonisjongithub.</li>
|
||||
<li>Config: auto-migrate legacy state/config paths and keep config resolution consistent across legacy filenames.</li>
|
||||
<li>Gateway: warn on hook tokens via query params; document header auth preference. (#2200) Thanks @YuriNachos.</li>
|
||||
<li>Gateway: add dangerous Control UI device auth bypass flag + audit warnings. (#2248)</li>
|
||||
<li>Doctor: warn on gateway exposure without auth. (#2016) Thanks @Alex-Alaniz.</li>
|
||||
<li>Web UI: keep sub-agent announce replies visible in WebChat. (#1977) Thanks @andrescardonas7.</li>
|
||||
<li>Browser: route browser control via gateway/node; remove standalone browser control command and control URL config.</li>
|
||||
<li>Browser: route <code>browser.request</code> via node proxies when available; honor proxy timeouts; derive browser ports from <code>gateway.port</code>.</li>
|
||||
<li>Browser: fall back to URL matching for extension relay target resolution. (#1999) Thanks @jonit-dev.</li>
|
||||
<li>Telegram: allow caption param for media sends. (#1888) Thanks @mguellsegarra.</li>
|
||||
<li>Telegram: support plugin sendPayload channelData (media/buttons) and validate plugin commands. (#1917) Thanks @JoshuaLelon.</li>
|
||||
<li>Telegram: avoid block replies when streaming is disabled. (#1885) Thanks @ivancasco.</li>
|
||||
<li>Telegram: add optional silent send flag (disable notifications). (#2382) Thanks @Suksham-sharma.</li>
|
||||
<li>Telegram: support editing sent messages via message(action="edit"). (#2394) Thanks @marcelomar21.</li>
|
||||
<li>Telegram: support quote replies for message tool and inbound context. (#2900) Thanks @aduk059.</li>
|
||||
<li>Telegram: add sticker receive/send with vision caching. (#2629) Thanks @longjos.</li>
|
||||
<li>Telegram: send sticker pixels to vision models. (#2650)</li>
|
||||
<li>Telegram: keep topic IDs in restart sentinel notifications. (#1807) Thanks @hsrvc.</li>
|
||||
<li>Discord: add configurable privileged gateway intents for presences/members. (#2266) Thanks @kentaro.</li>
|
||||
<li>Slack: clear ack reaction after streamed replies. (#2044) Thanks @fancyboi999.</li>
|
||||
<li>Matrix: switch plugin SDK to @vector-im/matrix-bot-sdk.</li>
|
||||
<li>Tlon: format thread reply IDs as @ud. (#1837) Thanks @wca4a.</li>
|
||||
<li>Tools: add per-sender group tool policies and fix precedence. (#1757) Thanks @adam91holt.</li>
|
||||
<li>Agents: summarize dropped messages during compaction safeguard pruning. (#2509) Thanks @jogi47.</li>
|
||||
<li>Agents: expand cron tool description with full schema docs. (#1988) Thanks @tomascupr.</li>
|
||||
<li>Agents: honor tools.exec.safeBins in exec allowlist checks. (#2281)</li>
|
||||
<li>Memory Search: allow extra paths for memory indexing (ignores symlinks). (#3600) Thanks @kira-ariaki.</li>
|
||||
<li>Skills: add multi-image input support to Nano Banana Pro skill. (#1958) Thanks @tyler6204.</li>
|
||||
<li>Skills: add missing dependency metadata for GitHub, Notion, Slack, Discord. (#1995) Thanks @jackheuberger.</li>
|
||||
<li>Commands: group /help and /commands output with Telegram paging. (#2504) Thanks @hougangdev.</li>
|
||||
<li>Routing: add per-account DM session scope and document multi-account isolation. (#3095) Thanks @jarvis-sam.</li>
|
||||
<li>Routing: precompile session key regexes. (#1697) Thanks @Ray0907.</li>
|
||||
<li>CLI: use Node's module compile cache for faster startup. (#2808) Thanks @pi0.</li>
|
||||
<li>Auth: show copyable Google auth URL after ASCII prompt. (#1787) Thanks @robbyczgw-cla.</li>
|
||||
<li>TUI: avoid width overflow when rendering selection lists. (#1686) Thanks @mossein.</li>
|
||||
<li>macOS: finish OpenClaw app rename for macOS sources, bundle identifiers, and shared kit paths. (#2844) Thanks @fal3.</li>
|
||||
<li>Branding: update launchd labels, mobile bundle IDs, and logging subsystems to bot.molt (legacy bundle ID migrations). Thanks @thewilloftheshadow.</li>
|
||||
<li>macOS: limit project-local <code>node_modules/.bin</code> PATH preference to debug builds (reduce PATH hijacking risk).</li>
|
||||
<li>macOS: keep custom SSH usernames in remote target. (#2046) Thanks @algal.</li>
|
||||
<li>macOS: avoid crash when rendering code blocks by bumping Textual to 0.3.1. (#2033) Thanks @garricn.</li>
|
||||
<li>Update: ignore dist/control-ui for dirty checks and restore after ui builds. (#1976) Thanks @Glucksberg.</li>
|
||||
<li>Build: bundle A2UI assets during build and stop tracking generated bundles. (#2455) Thanks @0oAstro.</li>
|
||||
<li>CI: increase Node heap size for macOS checks. (#1890) Thanks @realZachi.</li>
|
||||
<li>Config: apply config.env before ${VAR} substitution. (#1813) Thanks @spanishflu-est1918.</li>
|
||||
<li>Gateway: prefer newest session metadata when combining stores. (#1823) Thanks @emanuelst.</li>
|
||||
<li>Docs: tighten Fly private deployment steps. (#2289) Thanks @dguido.</li>
|
||||
<li>Docs: add migration guide for moving to a new machine. (#2381)</li>
|
||||
<li>Docs: add Northflank one-click deployment guide. (#2167) Thanks @AdeboyeDN.</li>
|
||||
<li>Docs: add Vercel AI Gateway to providers sidebar. (#1901) Thanks @jerilynzheng.</li>
|
||||
<li>Docs: add Render deployment guide. (#1975) Thanks @anurag.</li>
|
||||
<li>Docs: add Claude Max API Proxy guide. (#1875) Thanks @atalovesyou.</li>
|
||||
<li>Docs: add DigitalOcean deployment guide. (#1870) Thanks @0xJonHoldsCrypto.</li>
|
||||
<li>Docs: add Oracle Cloud (OCI) platform guide + cross-links. (#2333) Thanks @hirefrank.</li>
|
||||
<li>Docs: add Raspberry Pi install guide. (#1871) Thanks @0xJonHoldsCrypto.</li>
|
||||
<li>Docs: add GCP Compute Engine deployment guide. (#1848) Thanks @hougangdev.</li>
|
||||
<li>Docs: add LINE channel guide. Thanks @thewilloftheshadow.</li>
|
||||
<li>Docs: credit both contributors for Control UI refresh. (#1852) Thanks @EnzeD.</li>
|
||||
<li>Docs: keep docs header sticky so navbar stays visible while scrolling. (#2445) Thanks @chenyuan99.</li>
|
||||
<li>Docs: update exe.dev install instructions. (#https://github.com/openclaw/openclaw/pull/3047) Thanks @zackerthescar.</li>
|
||||
</ul>
|
||||
<h3>Breaking</h3>
|
||||
<ul>
|
||||
<li><strong>BREAKING:</strong> Gateway auth mode "none" is removed; gateway now requires token/password (Tailscale Serve identity still allowed).</li>
|
||||
<li>Telegram: remove last <code>@ts-nocheck</code> from <code>bot-handlers.ts</code>, use Grammy types directly, deduplicate <code>StickerMetadata</code>. Zero <code>@ts-nocheck</code> remaining in <code>src/telegram/</code>. (#9206)</li>
|
||||
<li>Telegram: remove <code>@ts-nocheck</code> from <code>bot-message.ts</code>, type deps via <code>Omit<BuildTelegramMessageContextParams></code>, widen <code>allMedia</code> to <code>TelegramMediaRef[]</code>. (#9180)</li>
|
||||
<li>Telegram: remove <code>@ts-nocheck</code> from <code>bot.ts</code>, fix duplicate <code>bot.catch</code> error handler (Grammy overrides), remove dead reaction <code>message_thread_id</code> routing, harden sticker cache guard. (#9077)</li>
|
||||
<li>Onboarding: add Cloudflare AI Gateway provider setup and docs. (#7914) Thanks @roerohan.</li>
|
||||
<li>Onboarding: add Moonshot (.cn) auth choice and keep the China base URL when preserving defaults. (#7180) Thanks @waynelwz.</li>
|
||||
<li>Docs: clarify tmux send-keys for TUI by splitting text and Enter. (#7737) Thanks @Wangnov.</li>
|
||||
<li>Docs: mirror the landing page revamp for zh-CN (features, quickstart, docs directory, network model, credits). (#8994) Thanks @joshp123.</li>
|
||||
<li>Messages: add per-channel and per-account responsePrefix overrides across channels. (#9001) Thanks @mudrii.</li>
|
||||
<li>Cron: add announce delivery mode for isolated jobs (CLI + Control UI) and delivery mode config.</li>
|
||||
<li>Cron: default isolated jobs to announce delivery; accept ISO 8601 <code>schedule.at</code> in tool inputs.</li>
|
||||
<li>Cron: hard-migrate isolated jobs to announce/none delivery; drop legacy post-to-main/payload delivery fields and <code>atMs</code> inputs.</li>
|
||||
<li>Cron: delete one-shot jobs after success by default; add <code>--keep-after-run</code> for CLI.</li>
|
||||
<li>Cron: suppress messaging tools during announce delivery so summaries post consistently.</li>
|
||||
<li>Cron: avoid duplicate deliveries when isolated runs send messages directly.</li>
|
||||
</ul>
|
||||
<h3>Fixes</h3>
|
||||
<ul>
|
||||
<li>Telegram: avoid silent empty replies by tracking normalization skips before fallback. (#3796)</li>
|
||||
<li>Mentions: honor mentionPatterns even when explicit mentions are present. (#3303) Thanks @HirokiKobayashi-R.</li>
|
||||
<li>Discord: restore username directory lookup in target resolution. (#3131) Thanks @bonald.</li>
|
||||
<li>Agents: align MiniMax base URL test expectation with default provider config. (#3131) Thanks @bonald.</li>
|
||||
<li>Agents: prevent retries on oversized image errors and surface size limits. (#2871) Thanks @Suksham-sharma.</li>
|
||||
<li>Agents: inherit provider baseUrl/api for inline models. (#2740) Thanks @lploc94.</li>
|
||||
<li>Memory Search: keep auto provider model defaults and only include remote when configured. (#2576) Thanks @papago2355.</li>
|
||||
<li>Telegram: include AccountId in native command context for multi-agent routing. (#2942) Thanks @Chloe-VP.</li>
|
||||
<li>Telegram: handle video note attachments in media extraction. (#2905) Thanks @mylukin.</li>
|
||||
<li>TTS: read OPENAI_TTS_BASE_URL at runtime instead of module load to honor config.env. (#3341) Thanks @hclsys.</li>
|
||||
<li>macOS: auto-scroll to bottom when sending a new message while scrolled up. (#2471) Thanks @kennyklee.</li>
|
||||
<li>Web UI: auto-expand the chat compose textarea while typing (with sensible max height). (#2950) Thanks @shivamraut101.</li>
|
||||
<li>Gateway: prevent crashes on transient network errors (fetch failures, timeouts, DNS). Added fatal error detection to only exit on truly critical errors. Fixes #2895, #2879, #2873. (#2980) Thanks @elliotsecops.</li>
|
||||
<li>Agents: guard channel tool listActions to avoid plugin crashes. (#2859) Thanks @mbelinky.</li>
|
||||
<li>Discord: stop resolveDiscordTarget from passing directory params into messaging target parsers. Fixes #3167. Thanks @thewilloftheshadow.</li>
|
||||
<li>Discord: avoid resolving bare channel names to user DMs when a username matches. Thanks @thewilloftheshadow.</li>
|
||||
<li>Discord: fix directory config type import for target resolution. Thanks @thewilloftheshadow.</li>
|
||||
<li>Providers: update MiniMax API endpoint and compatibility mode. (#3064) Thanks @hlbbbbbbb.</li>
|
||||
<li>Telegram: treat more network errors as recoverable in polling. (#3013) Thanks @ryancontent.</li>
|
||||
<li>Discord: resolve usernames to user IDs for outbound messages. (#2649) Thanks @nonggialiang.</li>
|
||||
<li>Providers: update Moonshot Kimi model references to kimi-k2.5. (#2762) Thanks @MarvinCui.</li>
|
||||
<li>Gateway: suppress AbortError and transient network errors in unhandled rejections. (#2451) Thanks @Glucksberg.</li>
|
||||
<li>TTS: keep /tts status replies on text-only commands and avoid duplicate block-stream audio. (#2451) Thanks @Glucksberg.</li>
|
||||
<li>Security: pin npm overrides to keep tar@7.5.4 for install toolchains.</li>
|
||||
<li>Security: properly test Windows ACL audit for config includes. (#2403) Thanks @dominicnunez.</li>
|
||||
<li>CLI: recognize versioned Node executables when parsing argv. (#2490) Thanks @David-Marsh-Photo.</li>
|
||||
<li>CLI: avoid prompting for gateway runtime under the spinner. (#2874)</li>
|
||||
<li>BlueBubbles: coalesce inbound URL link preview messages. (#1981) Thanks @tyler6204.</li>
|
||||
<li>Cron: allow payloads containing "heartbeat" in event filter. (#2219) Thanks @dwfinkelstein.</li>
|
||||
<li>CLI: avoid loading config for global help/version while registering plugin commands. (#2212) Thanks @dial481.</li>
|
||||
<li>Agents: include memory.md when bootstrapping memory context. (#2318) Thanks @czekaj.</li>
|
||||
<li>Agents: release session locks on process termination and cover more signals. (#2483) Thanks @janeexai.</li>
|
||||
<li>Agents: skip cooldowned providers during model failover. (#2143) Thanks @YiWang24.</li>
|
||||
<li>Telegram: harden polling + retry behavior for transient network errors and Node 22 transport issues. (#2420) Thanks @techboss.</li>
|
||||
<li>Telegram: ignore non-forum group message_thread_id while preserving DM thread sessions. (#2731) Thanks @dylanneve1.</li>
|
||||
<li>Telegram: wrap reasoning italics per line to avoid raw underscores. (#2181) Thanks @YuriNachos.</li>
|
||||
<li>Telegram: centralize API error logging for delivery and bot calls. (#2492) Thanks @altryne.</li>
|
||||
<li>Voice Call: enforce Twilio webhook signature verification for ngrok URLs; disable ngrok free tier bypass by default.</li>
|
||||
<li>Security: harden Tailscale Serve auth by validating identity via local tailscaled before trusting headers.</li>
|
||||
<li>Media: fix text attachment MIME misclassification with CSV/TSV inference and UTF-16 detection; add XML attribute escaping for file output. (#3628) Thanks @frankekn.</li>
|
||||
<li>Build: align memory-core peer dependency with lockfile.</li>
|
||||
<li>Security: add mDNS discovery mode with minimal default to reduce information disclosure. (#1882) Thanks @orlyjamie.</li>
|
||||
<li>Security: harden URL fetches with DNS pinning to reduce rebinding risk. Thanks Chris Zheng.</li>
|
||||
<li>Web UI: improve WebChat image paste previews and allow image-only sends. (#1925) Thanks @smartprogrammer93.</li>
|
||||
<li>Security: wrap external hook content by default with a per-hook opt-out. (#1827) Thanks @mertcicekci0.</li>
|
||||
<li>Gateway: default auth now fail-closed (token/password required; Tailscale Serve identity remains allowed).</li>
|
||||
<li>Gateway: treat loopback + non-local Host connections as remote unless trusted proxy headers are present.</li>
|
||||
<li>Onboarding: remove unsupported gateway auth "off" choice from onboarding/configure flows and CLI flags.</li>
|
||||
<li>Heartbeat: allow explicit accountId routing for multi-account channels. (#8702) Thanks @lsh411.</li>
|
||||
<li>TUI/Gateway: handle non-streaming finals, refresh history for non-local chat runs, and avoid event gap warnings for targeted tool streams. (#8432) Thanks @gumadeiras.</li>
|
||||
<li>Shell completion: auto-detect and migrate slow dynamic patterns to cached files for faster terminal startup; add completion health checks to doctor/update/onboard.</li>
|
||||
<li>Telegram: honor session model overrides in inline model selection. (#8193) Thanks @gildo.</li>
|
||||
<li>Web UI: fix agent model selection saves for default/non-default agents and wrap long workspace paths. Thanks @Takhoffman.</li>
|
||||
<li>Web UI: resolve header logo path when <code>gateway.controlUi.basePath</code> is set. (#7178) Thanks @Yeom-JinHo.</li>
|
||||
<li>Web UI: apply button styling to the new-messages indicator.</li>
|
||||
<li>Security: keep untrusted channel metadata out of system prompts (Slack/Discord). Thanks @KonstantinMirin.</li>
|
||||
<li>Security: enforce sandboxed media paths for message tool attachments. (#9182) Thanks @victormier.</li>
|
||||
<li>Security: require explicit credentials for gateway URL overrides to prevent credential leakage. (#8113) Thanks @victormier.</li>
|
||||
<li>Security: gate <code>whatsapp_login</code> tool to owner senders and default-deny non-owner contexts. (#8768) Thanks @victormier.</li>
|
||||
<li>Voice call: harden webhook verification with host allowlists/proxy trust and keep ngrok loopback bypass.</li>
|
||||
<li>Voice call: add regression coverage for anonymous inbound caller IDs with allowlist policy. (#8104) Thanks @victormier.</li>
|
||||
<li>Cron: accept epoch timestamps and 0ms durations in CLI <code>--at</code> parsing.</li>
|
||||
<li>Cron: reload store data when the store file is recreated or mtime changes.</li>
|
||||
<li>Cron: deliver announce runs directly, honor delivery mode, and respect wakeMode for summaries. (#8540) Thanks @tyler6204.</li>
|
||||
<li>Telegram: include forward_from_chat metadata in forwarded messages and harden cron delivery target checks. (#8392) Thanks @Glucksberg.</li>
|
||||
<li>macOS: fix cron payload summary rendering and ISO 8601 formatter concurrency safety.</li>
|
||||
</ul>
|
||||
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.1.29/OpenClaw-2026.1.29.zip" length="22458204" type="application/octet-stream" sparkle:edSignature="HqHwZHQyG/CEfBuQnQ/RffJQPKpSbCVrho9C6rgt93S5ek4AH6hUhB3BBKY8sbX1IVFATKK5QZZNE0YPAf7eBw=="/>
|
||||
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.2.3/OpenClaw-2026.2.3.zip" length="22530161" type="application/octet-stream" sparkle:edSignature="7eHUaQC6cx87HWbcaPh9T437+LqfE9VtQBf4p9JBjIyBrqGYxxp9KPvI5unEjg55j9j2djCXhseSMeyyRmvYBg=="/>
|
||||
</item>
|
||||
<item>
|
||||
<title>2026.1.24-1</title>
|
||||
<pubDate>Sun, 25 Jan 2026 14:05:25 +0000</pubDate>
|
||||
<title>2026.2.2</title>
|
||||
<pubDate>Tue, 03 Feb 2026 17:04:17 -0800</pubDate>
|
||||
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
|
||||
<sparkle:version>7952</sparkle:version>
|
||||
<sparkle:shortVersionString>2026.1.24-1</sparkle:shortVersionString>
|
||||
<sparkle:version>8809</sparkle:version>
|
||||
<sparkle:shortVersionString>2026.2.2</sparkle:shortVersionString>
|
||||
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
||||
<description><![CDATA[<h2>OpenClaw 2026.1.24-1</h2>
|
||||
<h3>Fixes</h3>
|
||||
<ul>
|
||||
<li>Packaging: include dist/shared output in npm tarball (fixes missing reasoning-tags import on install).</li>
|
||||
</ul>
|
||||
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.1.24-1/OpenClaw-2026.1.24-1.zip" length="12396699" type="application/octet-stream" sparkle:edSignature="VaEdWIgEJBrZLIp2UmigoQ6vaq4P/jNFXpHYXvXHD5MsATS0CqBl6ugyyxRq+/GbpUqmdgdlht4dTUVbLRw6BA=="/>
|
||||
</item>
|
||||
<item>
|
||||
<title>2026.1.24</title>
|
||||
<pubDate>Sun, 25 Jan 2026 13:31:05 +0000</pubDate>
|
||||
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
|
||||
<sparkle:version>7944</sparkle:version>
|
||||
<sparkle:shortVersionString>2026.1.24</sparkle:shortVersionString>
|
||||
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
||||
<description><![CDATA[<h2>OpenClaw 2026.1.24</h2>
|
||||
<h3>Highlights</h3>
|
||||
<ul>
|
||||
<li>Providers: Ollama discovery + docs; Venice guide upgrades + cross-links. (#1606) Thanks @abhaymundhara. https://docs.openclaw.ai/providers/ollama https://docs.openclaw.ai/providers/venice</li>
|
||||
<li>Channels: LINE plugin (Messaging API) with rich replies + quick replies. (#1630) Thanks @plum-dawg.</li>
|
||||
<li>TTS: Edge fallback (keyless) + <code>/tts</code> auto modes. (#1668, #1667) Thanks @steipete, @sebslight. https://docs.openclaw.ai/tts</li>
|
||||
<li>Exec approvals: approve in-chat via <code>/approve</code> across all channels (including plugins). (#1621) Thanks @czekaj. https://docs.openclaw.ai/tools/exec-approvals https://docs.openclaw.ai/tools/slash-commands</li>
|
||||
<li>Telegram: DM topics as separate sessions + outbound link preview toggle. (#1597, #1700) Thanks @rohannagpal, @zerone0x. https://docs.openclaw.ai/channels/telegram</li>
|
||||
</ul>
|
||||
<description><![CDATA[<h2>OpenClaw 2026.2.2</h2>
|
||||
<h3>Changes</h3>
|
||||
<ul>
|
||||
<li>Channels: add LINE plugin (Messaging API) with rich replies, quick replies, and plugin HTTP registry. (#1630) Thanks @plum-dawg.</li>
|
||||
<li>TTS: add Edge TTS provider fallback, defaulting to keyless Edge with MP3 retry on format failures. (#1668) Thanks @steipete. https://docs.openclaw.ai/tts</li>
|
||||
<li>TTS: add auto mode enum (off/always/inbound/tagged) with per-session <code>/tts</code> override. (#1667) Thanks @sebslight. https://docs.openclaw.ai/tts</li>
|
||||
<li>Telegram: treat DM topics as separate sessions and keep DM history limits stable with thread suffixes. (#1597) Thanks @rohannagpal.</li>
|
||||
<li>Telegram: add <code>channels.telegram.linkPreview</code> to toggle outbound link previews. (#1700) Thanks @zerone0x. https://docs.openclaw.ai/channels/telegram</li>
|
||||
<li>Web search: add Brave freshness filter parameter for time-scoped results. (#1688) Thanks @JonUleis. https://docs.openclaw.ai/tools/web</li>
|
||||
<li>UI: refresh Control UI dashboard design system (typography, colors, spacing). (#1786) Thanks @mousberg.</li>
|
||||
<li>Exec approvals: forward approval prompts to chat with <code>/approve</code> for all channels (including plugins). (#1621) Thanks @czekaj. https://docs.openclaw.ai/tools/exec-approvals https://docs.openclaw.ai/tools/slash-commands</li>
|
||||
<li>Gateway: expose config.patch in the gateway tool with safe partial updates + restart sentinel. (#1653) Thanks @Glucksberg.</li>
|
||||
<li>Diagnostics: add diagnostic flags for targeted debug logs (config + env override). https://docs.openclaw.ai/diagnostics/flags</li>
|
||||
<li>Docs: expand FAQ (migration, scheduling, concurrency, model recommendations, OpenAI subscription auth, Pi sizing, hackable install, docs SSL workaround).</li>
|
||||
<li>Docs: add verbose installer troubleshooting guidance.</li>
|
||||
<li>Docs: add macOS VM guide with local/hosted options + VPS/nodes guidance. (#1693) Thanks @f-trycua.</li>
|
||||
<li>Docs: add Bedrock EC2 instance role setup + IAM steps. (#1625) Thanks @sergical. https://docs.openclaw.ai/bedrock</li>
|
||||
<li>Docs: update Fly.io guide notes.</li>
|
||||
<li>Dev: add prek pre-commit hooks + dependabot config for weekly updates. (#1720) Thanks @dguido.</li>
|
||||
<li>Feishu: add Feishu/Lark plugin support + docs. (#7313) Thanks @jiulingyun (openclaw-cn).</li>
|
||||
<li>Web UI: add Agents dashboard for managing agent files, tools, skills, models, channels, and cron jobs.</li>
|
||||
<li>Memory: implement the opt-in QMD backend for workspace memory. (#3160) Thanks @vignesh07.</li>
|
||||
<li>Security: add healthcheck skill and bootstrap audit guidance. (#7641) Thanks @Takhoffman.</li>
|
||||
<li>Config: allow setting a default subagent thinking level via <code>agents.defaults.subagents.thinking</code> (and per-agent <code>agents.list[].subagents.thinking</code>). (#7372) Thanks @tyler6204.</li>
|
||||
<li>Docs: zh-CN translations seed + polish, pipeline guidance, nav/landing updates, and typo fixes. (#8202, #6995, #6619, #7242, #7303, #7415) Thanks @AaronWander, @taiyi747, @Explorer1092, @rendaoyuan, @joshp123, @lailoo.</li>
|
||||
</ul>
|
||||
<h3>Fixes</h3>
|
||||
<ul>
|
||||
<li>Web UI: fix config/debug layout overflow, scrolling, and code block sizing. (#1715) Thanks @saipreetham589.</li>
|
||||
<li>Web UI: show Stop button during active runs, swap back to New session when idle. (#1664) Thanks @ndbroadbent.</li>
|
||||
<li>Web UI: clear stale disconnect banners on reconnect; allow form saves with unsupported schema paths but block missing schema. (#1707) Thanks @Glucksberg.</li>
|
||||
<li>Web UI: hide internal <code>message_id</code> hints in chat bubbles.</li>
|
||||
<li>Gateway: allow Control UI token-only auth to skip device pairing even when device identity is present (<code>gateway.controlUi.allowInsecureAuth</code>). (#1679) Thanks @steipete.</li>
|
||||
<li>Matrix: decrypt E2EE media attachments with preflight size guard. (#1744) Thanks @araa47.</li>
|
||||
<li>BlueBubbles: route phone-number targets to DMs, avoid leaking routing IDs, and auto-create missing DMs (Private API required). (#1751) Thanks @tyler6204. https://docs.openclaw.ai/channels/bluebubbles</li>
|
||||
<li>BlueBubbles: keep part-index GUIDs in reply tags when short IDs are missing.</li>
|
||||
<li>Signal: repair reaction sends (group/UUID targets + CLI author flags). (#1651) Thanks @vilkasdev.</li>
|
||||
<li>Signal: add configurable signal-cli startup timeout + external daemon mode docs. (#1677) https://docs.openclaw.ai/channels/signal</li>
|
||||
<li>Telegram: set fetch duplex="half" for uploads on Node 22 to avoid sendPhoto failures. (#1684) Thanks @commdata2338.</li>
|
||||
<li>Telegram: use wrapped fetch for long-polling on Node to normalize AbortSignal handling. (#1639)</li>
|
||||
<li>Telegram: honor per-account proxy for outbound API calls. (#1774) Thanks @radek-paclt.</li>
|
||||
<li>Telegram: fall back to text when voice notes are blocked by privacy settings. (#1725) Thanks @foeken.</li>
|
||||
<li>Voice Call: return stream TwiML for outbound conversation calls on initial Twilio webhook. (#1634)</li>
|
||||
<li>Voice Call: serialize Twilio TTS playback and cancel on barge-in to prevent overlap. (#1713) Thanks @dguido.</li>
|
||||
<li>Google Chat: tighten email allowlist matching, typing cleanup, media caps, and onboarding/docs/tests. (#1635) Thanks @iHildy.</li>
|
||||
<li>Google Chat: normalize space targets without double <code>spaces/</code> prefix.</li>
|
||||
<li>Agents: auto-compact on context overflow prompt errors before failing. (#1627) Thanks @rodrigouroz.</li>
|
||||
<li>Agents: use the active auth profile for auto-compaction recovery.</li>
|
||||
<li>Media understanding: skip image understanding when the primary model already supports vision. (#1747) Thanks @tyler6204.</li>
|
||||
<li>Models: default missing custom provider fields so minimal configs are accepted.</li>
|
||||
<li>Messaging: keep newline chunking safe for fenced markdown blocks across channels.</li>
|
||||
<li>TUI: reload history after gateway reconnect to restore session state. (#1663)</li>
|
||||
<li>Heartbeat: normalize target identifiers for consistent routing.</li>
|
||||
<li>Exec: keep approvals for elevated ask unless full mode. (#1616) Thanks @ivancasco.</li>
|
||||
<li>Exec: treat Windows platform labels as Windows for node shell selection. (#1760) Thanks @ymat19.</li>
|
||||
<li>Gateway: include inline config env vars in service install environments. (#1735) Thanks @Seredeep.</li>
|
||||
<li>Gateway: skip Tailscale DNS probing when tailscale.mode is off. (#1671)</li>
|
||||
<li>Gateway: reduce log noise for late invokes + remote node probes; debounce skills refresh. (#1607) Thanks @petter-b.</li>
|
||||
<li>Gateway: clarify Control UI/WebChat auth error hints for missing tokens. (#1690)</li>
|
||||
<li>Gateway: listen on IPv6 loopback when bound to 127.0.0.1 so localhost webhooks work.</li>
|
||||
<li>Gateway: store lock files in the temp directory to avoid stale locks on persistent volumes. (#1676)</li>
|
||||
<li>macOS: default direct-transport <code>ws://</code> URLs to port 18789; document <code>gateway.remote.transport</code>. (#1603) Thanks @ngutman.</li>
|
||||
<li>Tests: cap Vitest workers on CI macOS to reduce timeouts. (#1597) Thanks @rohannagpal.</li>
|
||||
<li>Tests: avoid fake-timer dependency in embedded runner stream mock to reduce CI flakes. (#1597) Thanks @rohannagpal.</li>
|
||||
<li>Tests: increase embedded runner ordering test timeout to reduce CI flakes. (#1597) Thanks @rohannagpal.</li>
|
||||
<li>Security: require operator.approvals for gateway /approve commands. (#1) Thanks @mitsuhiko, @yueyueL.</li>
|
||||
<li>Security: Matrix allowlists now require full MXIDs; ambiguous name resolution no longer grants access. Thanks @MegaManSec.</li>
|
||||
<li>Security: enforce access-group gating for Slack slash commands when channel type lookup fails.</li>
|
||||
<li>Security: require validated shared-secret auth before skipping device identity on gateway connect.</li>
|
||||
<li>Security: guard skill installer downloads with SSRF checks (block private/localhost URLs).</li>
|
||||
<li>Security: harden Windows exec allowlist; block cmd.exe bypass via single &. Thanks @simecek.</li>
|
||||
<li>fix(voice-call): harden inbound allowlist; reject anonymous callers; require Telnyx publicKey for allowlist; token-gate Twilio media streams; cap webhook body size (thanks @simecek)</li>
|
||||
<li>Media understanding: apply SSRF guardrails to provider fetches; allow private baseUrl overrides explicitly.</li>
|
||||
<li>fix(webchat): respect user scroll position during streaming and refresh (#7226) (thanks @marcomarandiz)</li>
|
||||
<li>Telegram: recover from grammY long-poll timed out errors. (#7466) Thanks @macmimi23.</li>
|
||||
<li>Agents: repair malformed tool calls and session transcripts. (#7473) Thanks @justinhuangcode.</li>
|
||||
<li>fix(agents): validate AbortSignal instances before calling AbortSignal.any() (#7277) (thanks @Elarwei001)</li>
|
||||
<li>Media understanding: skip binary media from file text extraction. (#7475) Thanks @AlexZhangji.</li>
|
||||
<li>Onboarding: keep TUI flow exclusive (skip completion prompt + background Web UI seed); completion prompt now handled by install/update.</li>
|
||||
<li>TUI: block onboarding output while TUI is active and restore terminal state on exit.</li>
|
||||
<li>CLI/Zsh completion: cache scripts in state dir and escape option descriptions to avoid invalid option errors.</li>
|
||||
<li>fix(ui): resolve Control UI asset path correctly.</li>
|
||||
<li>fix(ui): refresh agent files after external edits.</li>
|
||||
<li>Docs: finish renaming the QMD memory docs to reference the OpenClaw state dir.</li>
|
||||
<li>Tests: stub SSRF DNS pinning in web auto-reply + Gemini video coverage. (#6619) Thanks @joshp123.</li>
|
||||
</ul>
|
||||
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.1.24/OpenClaw-2026.1.24.zip" length="12396700" type="application/octet-stream" sparkle:edSignature="u+XzKD3YwV8s79gIr7LK4OtDCcmp/b+cjNC6SHav3/1CVJegh02SsBKatrampox32XGx8P2+8c/+fHV+qpkHCA=="/>
|
||||
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.2.2/OpenClaw-2026.2.2.zip" length="22519052" type="application/octet-stream" sparkle:edSignature="a6viD+aS5EfY/RkPIPMfoQQNkJCk6QTdV5WobXFxyYwURskUm8/nXTHVXsCh1c5+0WKUnmlDIyf0i+6IWiavAA=="/>
|
||||
</item>
|
||||
<item>
|
||||
<title>2026.2.1</title>
|
||||
<pubDate>Mon, 02 Feb 2026 03:53:03 -0800</pubDate>
|
||||
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
|
||||
<sparkle:version>8650</sparkle:version>
|
||||
<sparkle:shortVersionString>2026.2.1</sparkle:shortVersionString>
|
||||
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
||||
<description><![CDATA[<h2>OpenClaw 2026.2.1</h2>
|
||||
<h3>Changes</h3>
|
||||
<ul>
|
||||
<li>Docs: onboarding/install/i18n/exec-approvals/Control UI/exe.dev/cacheRetention updates + misc nav/typos. (#3050, #3461, #4064, #4675, #4729, #4763, #5003, #5402, #5446, #5474, #5663, #5689, #5694, #5967, #6270, #6300, #6311, #6416, #6487, #6550, #6789)</li>
|
||||
<li>Telegram: use shared pairing store. (#6127) Thanks @obviyus.</li>
|
||||
<li>Agents: add OpenRouter app attribution headers. Thanks @alexanderatallah.</li>
|
||||
<li>Agents: add system prompt safety guardrails. (#5445) Thanks @joshp123.</li>
|
||||
<li>Agents: update pi-ai to 0.50.9 and rename cacheControlTtl -> cacheRetention (with back-compat mapping).</li>
|
||||
<li>Agents: extend CreateAgentSessionOptions with systemPrompt/skills/contextFiles.</li>
|
||||
<li>Agents: add tool policy conformance snapshot (no runtime behavior change). (#6011)</li>
|
||||
<li>Auth: update MiniMax OAuth hint + portal auth note copy.</li>
|
||||
<li>Discord: inherit thread parent bindings for routing. (#3892) Thanks @aerolalit.</li>
|
||||
<li>Gateway: inject timestamps into agent and chat.send messages. (#3705) Thanks @conroywhitney, @CashWilliams.</li>
|
||||
<li>Gateway: require TLS 1.3 minimum for TLS listeners. (#5970) Thanks @loganaden.</li>
|
||||
<li>Web UI: refine chat layout + extend session active duration.</li>
|
||||
<li>CI: add formal conformance + alias consistency checks. (#5723, #5807)</li>
|
||||
</ul>
|
||||
<h3>Fixes</h3>
|
||||
<ul>
|
||||
<li>Plugins: validate plugin/hook install paths and reject traversal-like names.</li>
|
||||
<li>Telegram: add download timeouts for file fetches. (#6914) Thanks @hclsys.</li>
|
||||
<li>Telegram: enforce thread specs for DM vs forum sends. (#6833) Thanks @obviyus.</li>
|
||||
<li>Streaming: flush block streaming on paragraph boundaries for newline chunking. (#7014)</li>
|
||||
<li>Streaming: stabilize partial streaming filters.</li>
|
||||
<li>Auto-reply: avoid referencing workspace files in /new greeting prompt. (#5706) Thanks @bravostation.</li>
|
||||
<li>Tools: align tool execute adapters/signatures (legacy + parameter order + arg normalization).</li>
|
||||
<li>Tools: treat <code>"*"</code> tool allowlist entries as valid to avoid spurious unknown-entry warnings.</li>
|
||||
<li>Skills: update session-logs paths from .clawdbot to .openclaw. (#4502)</li>
|
||||
<li>Slack: harden media fetch limits and Slack file URL validation. (#6639) Thanks @davidiach.</li>
|
||||
<li>Lint: satisfy curly rule after import sorting. (#6310)</li>
|
||||
<li>Process: resolve Windows <code>spawn()</code> failures for npm-family CLIs by appending <code>.cmd</code> when needed. (#5815) Thanks @thejhinvirtuoso.</li>
|
||||
<li>Discord: resolve PluralKit proxied senders for allowlists and labels. (#5838) Thanks @thewilloftheshadow.</li>
|
||||
<li>Tlon: add timeout to SSE client fetch calls (CWE-400). (#5926)</li>
|
||||
<li>Memory search: L2-normalize local embedding vectors to fix semantic search. (#5332)</li>
|
||||
<li>Agents: align embedded runner + typings with pi-coding-agent API updates (pi 0.51.0).</li>
|
||||
<li>Agents: ensure OpenRouter attribution headers apply in the embedded runner.</li>
|
||||
<li>Agents: cap context window resolution for compaction safeguard. (#6187) Thanks @iamEvanYT.</li>
|
||||
<li>System prompt: resolve overrides and hint using session_status for current date/time. (#1897, #1928, #2108, #3677)</li>
|
||||
<li>Agents: fix Pi prompt template argument syntax. (#6543)</li>
|
||||
<li>Subagents: fix announce failover race (always emit lifecycle end; timeout=0 means no-timeout). (#6621)</li>
|
||||
<li>Teams: gate media auth retries.</li>
|
||||
<li>Telegram: restore draft streaming partials. (#5543) Thanks @obviyus.</li>
|
||||
<li>Onboarding: friendlier Windows onboarding message. (#6242) Thanks @shanselman.</li>
|
||||
<li>TUI: prevent crash when searching with digits in the model selector.</li>
|
||||
<li>Agents: wire before_tool_call plugin hook into tool execution. (#6570, #6660) Thanks @ryancnelson.</li>
|
||||
<li>Browser: secure Chrome extension relay CDP sessions.</li>
|
||||
<li>Docker: use container port for gateway command instead of host port. (#5110) Thanks @mise42.</li>
|
||||
<li>fix(lobster): block arbitrary exec via lobsterPath/cwd injection (GHSA-4mhr-g7xj-cg8j). (#5335) Thanks @vignesh07.</li>
|
||||
<li>Security: sanitize WhatsApp accountId to prevent path traversal. (#4610)</li>
|
||||
<li>Security: restrict MEDIA path extraction to prevent LFI. (#4930)</li>
|
||||
<li>Security: validate message-tool filePath/path against sandbox root. (#6398)</li>
|
||||
<li>Security: block LD*/DYLD* env overrides for host exec. (#4896) Thanks @HassanFleyah.</li>
|
||||
<li>Security: harden web tool content wrapping + file parsing safeguards. (#4058) Thanks @VACInc.</li>
|
||||
<li>Security: enforce Twitch <code>allowFrom</code> allowlist gating (deny non-allowlisted senders). Thanks @MegaManSec.</li>
|
||||
</ul>
|
||||
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.2.1/OpenClaw-2026.2.1.zip" length="22458919" type="application/octet-stream" sparkle:edSignature="kA/8VQlVdtYphcB1iuFrhWczwWKgkVZMfDfQ7T9WD405D8JKTv5CZ1n8lstIVkpk4xog3UhrfaaoTG8Bf8DMAQ=="/>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
|
||||
@@ -21,8 +21,8 @@ android {
|
||||
applicationId = "ai.openclaw.android"
|
||||
minSdk = 31
|
||||
targetSdk = 36
|
||||
versionCode = 202601290
|
||||
versionName = "2026.1.29"
|
||||
versionCode = 202602030
|
||||
versionName = "2026.2.6"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2026.1.29</string>
|
||||
<string>2026.2.6</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>20260129</string>
|
||||
<string>20260202</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoadsInWebContent</key>
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2026.1.29</string>
|
||||
<string>2026.2.6</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>20260129</string>
|
||||
<string>20260202</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -81,8 +81,8 @@ targets:
|
||||
properties:
|
||||
CFBundleDisplayName: OpenClaw
|
||||
CFBundleIconName: AppIcon
|
||||
CFBundleShortVersionString: "2026.1.27-beta.1"
|
||||
CFBundleVersion: "20260126"
|
||||
CFBundleShortVersionString: "2026.2.6"
|
||||
CFBundleVersion: "20260202"
|
||||
UILaunchScreen: {}
|
||||
UIApplicationSceneManifest:
|
||||
UIApplicationSupportsMultipleScenes: false
|
||||
@@ -130,5 +130,5 @@ targets:
|
||||
path: Tests/Info.plist
|
||||
properties:
|
||||
CFBundleDisplayName: OpenClawTests
|
||||
CFBundleShortVersionString: "2026.1.27-beta.1"
|
||||
CFBundleVersion: "20260126"
|
||||
CFBundleShortVersionString: "2026.2.6"
|
||||
CFBundleVersion: "20260202"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "c86f22da7772193c6f161fc9db81747cc00c8b8c96b45f9479de1e65c2c4b17e",
|
||||
"originHash" : "1c9c9d251b760ed3234ecff741a88eb4bf42315ad6f50ac7392b187cf226c16c",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "axorcist",
|
||||
@@ -24,7 +24,7 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/steipete/ElevenLabsKit",
|
||||
"state" : {
|
||||
"revision" : "7e3c948d8340abe3977014f3de020edf221e9269",
|
||||
"revision" : "c8679fbd37416a8780fe43be88a497ff16209e2d",
|
||||
"version" : "0.1.0"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -20,9 +20,11 @@ extension CronJobEditor {
|
||||
self.wakeMode = job.wakeMode
|
||||
|
||||
switch job.schedule {
|
||||
case let .at(atMs):
|
||||
case let .at(at):
|
||||
self.scheduleKind = .at
|
||||
self.atDate = Date(timeIntervalSince1970: TimeInterval(atMs) / 1000)
|
||||
if let date = CronSchedule.parseAtDate(at) {
|
||||
self.atDate = date
|
||||
}
|
||||
case let .every(everyMs, _):
|
||||
self.scheduleKind = .every
|
||||
self.everyText = self.formatDuration(ms: everyMs)
|
||||
@@ -36,19 +38,22 @@ extension CronJobEditor {
|
||||
case let .systemEvent(text):
|
||||
self.payloadKind = .systemEvent
|
||||
self.systemEventText = text
|
||||
case let .agentTurn(message, thinking, timeoutSeconds, deliver, channel, to, bestEffortDeliver):
|
||||
case let .agentTurn(message, thinking, timeoutSeconds, _, _, _, _):
|
||||
self.payloadKind = .agentTurn
|
||||
self.agentMessage = message
|
||||
self.thinking = thinking ?? ""
|
||||
self.timeoutSeconds = timeoutSeconds.map(String.init) ?? ""
|
||||
self.deliver = deliver ?? false
|
||||
let trimmed = (channel ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
self.channel = trimmed.isEmpty ? "last" : trimmed
|
||||
self.to = to ?? ""
|
||||
self.bestEffortDeliver = bestEffortDeliver ?? false
|
||||
}
|
||||
|
||||
self.postPrefix = job.isolation?.postToMainPrefix ?? "Cron"
|
||||
if let delivery = job.delivery {
|
||||
self.deliveryMode = delivery.mode == .announce ? .announce : .none
|
||||
let trimmed = (delivery.channel ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
self.channel = trimmed.isEmpty ? "last" : trimmed
|
||||
self.to = delivery.to ?? ""
|
||||
self.bestEffortDeliver = delivery.bestEffort ?? false
|
||||
} else if self.sessionTarget == .isolated {
|
||||
self.deliveryMode = .announce
|
||||
}
|
||||
}
|
||||
|
||||
func save() {
|
||||
@@ -88,15 +93,29 @@ extension CronJobEditor {
|
||||
}
|
||||
|
||||
if self.sessionTarget == .isolated {
|
||||
let trimmed = self.postPrefix.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
root["isolation"] = [
|
||||
"postToMainPrefix": trimmed.isEmpty ? "Cron" : trimmed,
|
||||
]
|
||||
root["delivery"] = self.buildDelivery()
|
||||
}
|
||||
|
||||
return root.mapValues { AnyCodable($0) }
|
||||
}
|
||||
|
||||
func buildDelivery() -> [String: Any] {
|
||||
let mode = self.deliveryMode == .announce ? "announce" : "none"
|
||||
var delivery: [String: Any] = ["mode": mode]
|
||||
if self.deliveryMode == .announce {
|
||||
let trimmed = self.channel.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
delivery["channel"] = trimmed.isEmpty ? "last" : trimmed
|
||||
let to = self.to.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !to.isEmpty { delivery["to"] = to }
|
||||
if self.bestEffortDeliver {
|
||||
delivery["bestEffort"] = true
|
||||
} else if self.job?.delivery?.bestEffort == true {
|
||||
delivery["bestEffort"] = false
|
||||
}
|
||||
}
|
||||
return delivery
|
||||
}
|
||||
|
||||
func trimmed(_ value: String) -> String {
|
||||
value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
@@ -115,7 +134,7 @@ extension CronJobEditor {
|
||||
func buildSchedule() throws -> [String: Any] {
|
||||
switch self.scheduleKind {
|
||||
case .at:
|
||||
return ["kind": "at", "atMs": Int(self.atDate.timeIntervalSince1970 * 1000)]
|
||||
return ["kind": "at", "at": CronSchedule.formatIsoDate(self.atDate)]
|
||||
case .every:
|
||||
guard let ms = Self.parseDurationMs(self.everyText) else {
|
||||
throw NSError(
|
||||
@@ -209,14 +228,6 @@ extension CronJobEditor {
|
||||
let thinking = self.thinking.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !thinking.isEmpty { payload["thinking"] = thinking }
|
||||
if let n = Int(self.timeoutSeconds), n > 0 { payload["timeoutSeconds"] = n }
|
||||
payload["deliver"] = self.deliver
|
||||
if self.deliver {
|
||||
let trimmed = self.channel.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
payload["channel"] = trimmed.isEmpty ? "last" : trimmed
|
||||
let to = self.to.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !to.isEmpty { payload["to"] = to }
|
||||
payload["bestEffortDeliver"] = self.bestEffortDeliver
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
|
||||
@@ -13,13 +13,12 @@ extension CronJobEditor {
|
||||
|
||||
self.payloadKind = .agentTurn
|
||||
self.agentMessage = "Run diagnostic"
|
||||
self.deliver = true
|
||||
self.deliveryMode = .announce
|
||||
self.channel = "last"
|
||||
self.to = "+15551230000"
|
||||
self.thinking = "low"
|
||||
self.timeoutSeconds = "90"
|
||||
self.bestEffortDeliver = true
|
||||
self.postPrefix = "Cron"
|
||||
|
||||
_ = self.buildAgentTurnPayload()
|
||||
_ = try? self.buildPayload()
|
||||
|
||||
@@ -16,23 +16,20 @@ struct CronJobEditor: View {
|
||||
+ "Use an isolated session for agent turns so your main chat stays clean."
|
||||
static let sessionTargetNote =
|
||||
"Main jobs post a system event into the current main session. "
|
||||
+ "Isolated jobs run OpenClaw in a dedicated session and can deliver results (WhatsApp/Telegram/Discord/etc)."
|
||||
+ "Isolated jobs run OpenClaw in a dedicated session and can announce results to a channel."
|
||||
static let scheduleKindNote =
|
||||
"“At” runs once, “Every” repeats with a duration, “Cron” uses a 5-field Unix expression."
|
||||
static let isolatedPayloadNote =
|
||||
"Isolated jobs always run an agent turn. The result can be delivered to a channel, "
|
||||
+ "and a short summary is posted back to your main chat."
|
||||
"Isolated jobs always run an agent turn. Announce sends a short summary to a channel."
|
||||
static let mainPayloadNote =
|
||||
"System events are injected into the current main session. Agent turns require an isolated session target."
|
||||
static let mainSummaryNote =
|
||||
"Controls the label used when posting the completion summary back to the main session."
|
||||
|
||||
@State var name: String = ""
|
||||
@State var description: String = ""
|
||||
@State var agentId: String = ""
|
||||
@State var enabled: Bool = true
|
||||
@State var sessionTarget: CronSessionTarget = .main
|
||||
@State var wakeMode: CronWakeMode = .nextHeartbeat
|
||||
@State var wakeMode: CronWakeMode = .now
|
||||
@State var deleteAfterRun: Bool = false
|
||||
|
||||
enum ScheduleKind: String, CaseIterable, Identifiable { case at, every, cron; var id: String { rawValue } }
|
||||
@@ -46,13 +43,13 @@ struct CronJobEditor: View {
|
||||
@State var payloadKind: PayloadKind = .systemEvent
|
||||
@State var systemEventText: String = ""
|
||||
@State var agentMessage: String = ""
|
||||
@State var deliver: Bool = false
|
||||
enum DeliveryChoice: String, CaseIterable, Identifiable { case announce, none; var id: String { rawValue } }
|
||||
@State var deliveryMode: DeliveryChoice = .announce
|
||||
@State var channel: String = "last"
|
||||
@State var to: String = ""
|
||||
@State var thinking: String = ""
|
||||
@State var timeoutSeconds: String = ""
|
||||
@State var bestEffortDeliver: Bool = false
|
||||
@State var postPrefix: String = "Cron"
|
||||
|
||||
var channelOptions: [String] {
|
||||
let ordered = self.channelsStore.orderedChannelIds()
|
||||
@@ -122,8 +119,8 @@ struct CronJobEditor: View {
|
||||
GridRow {
|
||||
self.gridLabel("Wake mode")
|
||||
Picker("", selection: self.$wakeMode) {
|
||||
Text("next-heartbeat").tag(CronWakeMode.nextHeartbeat)
|
||||
Text("now").tag(CronWakeMode.now)
|
||||
Text("next-heartbeat").tag(CronWakeMode.nextHeartbeat)
|
||||
}
|
||||
.labelsHidden()
|
||||
.pickerStyle(.segmented)
|
||||
@@ -248,27 +245,6 @@ struct CronJobEditor: View {
|
||||
}
|
||||
}
|
||||
|
||||
if self.sessionTarget == .isolated {
|
||||
GroupBox("Main session summary") {
|
||||
Grid(alignment: .leadingFirstTextBaseline, horizontalSpacing: 14, verticalSpacing: 10) {
|
||||
GridRow {
|
||||
self.gridLabel("Prefix")
|
||||
TextField("Cron", text: self.$postPrefix)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
GridRow {
|
||||
Color.clear
|
||||
.frame(width: self.labelColumnWidth, height: 1)
|
||||
Text(
|
||||
Self.mainSummaryNote)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.vertical, 2)
|
||||
@@ -340,13 +316,17 @@ struct CronJobEditor: View {
|
||||
.frame(width: 180, alignment: .leading)
|
||||
}
|
||||
GridRow {
|
||||
self.gridLabel("Deliver")
|
||||
Toggle("Deliver result to a channel", isOn: self.$deliver)
|
||||
.toggleStyle(.switch)
|
||||
self.gridLabel("Delivery")
|
||||
Picker("", selection: self.$deliveryMode) {
|
||||
Text("Announce summary").tag(DeliveryChoice.announce)
|
||||
Text("None").tag(DeliveryChoice.none)
|
||||
}
|
||||
.labelsHidden()
|
||||
.pickerStyle(.segmented)
|
||||
}
|
||||
}
|
||||
|
||||
if self.deliver {
|
||||
if self.deliveryMode == .announce {
|
||||
Grid(alignment: .leadingFirstTextBaseline, horizontalSpacing: 14, verticalSpacing: 10) {
|
||||
GridRow {
|
||||
self.gridLabel("Channel")
|
||||
@@ -367,7 +347,7 @@ struct CronJobEditor: View {
|
||||
}
|
||||
GridRow {
|
||||
self.gridLabel("Best-effort")
|
||||
Toggle("Do not fail the job if delivery fails", isOn: self.$bestEffortDeliver)
|
||||
Toggle("Do not fail the job if announce fails", isOn: self.$bestEffortDeliver)
|
||||
.toggleStyle(.switch)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,12 +14,26 @@ enum CronWakeMode: String, CaseIterable, Identifiable, Codable {
|
||||
var id: String { self.rawValue }
|
||||
}
|
||||
|
||||
enum CronDeliveryMode: String, CaseIterable, Identifiable, Codable {
|
||||
case none
|
||||
case announce
|
||||
|
||||
var id: String { self.rawValue }
|
||||
}
|
||||
|
||||
struct CronDelivery: Codable, Equatable {
|
||||
var mode: CronDeliveryMode
|
||||
var channel: String?
|
||||
var to: String?
|
||||
var bestEffort: Bool?
|
||||
}
|
||||
|
||||
enum CronSchedule: Codable, Equatable {
|
||||
case at(atMs: Int)
|
||||
case at(at: String)
|
||||
case every(everyMs: Int, anchorMs: Int?)
|
||||
case cron(expr: String, tz: String?)
|
||||
|
||||
enum CodingKeys: String, CodingKey { case kind, atMs, everyMs, anchorMs, expr, tz }
|
||||
enum CodingKeys: String, CodingKey { case kind, at, atMs, everyMs, anchorMs, expr, tz }
|
||||
|
||||
var kind: String {
|
||||
switch self {
|
||||
@@ -34,7 +48,21 @@ enum CronSchedule: Codable, Equatable {
|
||||
let kind = try container.decode(String.self, forKey: .kind)
|
||||
switch kind {
|
||||
case "at":
|
||||
self = try .at(atMs: container.decode(Int.self, forKey: .atMs))
|
||||
if let at = try container.decodeIfPresent(String.self, forKey: .at),
|
||||
!at.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
{
|
||||
self = .at(at: at)
|
||||
return
|
||||
}
|
||||
if let atMs = try container.decodeIfPresent(Int.self, forKey: .atMs) {
|
||||
let date = Date(timeIntervalSince1970: TimeInterval(atMs) / 1000)
|
||||
self = .at(at: Self.formatIsoDate(date))
|
||||
return
|
||||
}
|
||||
throw DecodingError.dataCorruptedError(
|
||||
forKey: .at,
|
||||
in: container,
|
||||
debugDescription: "Missing schedule.at")
|
||||
case "every":
|
||||
self = try .every(
|
||||
everyMs: container.decode(Int.self, forKey: .everyMs),
|
||||
@@ -55,8 +83,8 @@ enum CronSchedule: Codable, Equatable {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(self.kind, forKey: .kind)
|
||||
switch self {
|
||||
case let .at(atMs):
|
||||
try container.encode(atMs, forKey: .atMs)
|
||||
case let .at(at):
|
||||
try container.encode(at, forKey: .at)
|
||||
case let .every(everyMs, anchorMs):
|
||||
try container.encode(everyMs, forKey: .everyMs)
|
||||
try container.encodeIfPresent(anchorMs, forKey: .anchorMs)
|
||||
@@ -65,6 +93,25 @@ enum CronSchedule: Codable, Equatable {
|
||||
try container.encodeIfPresent(tz, forKey: .tz)
|
||||
}
|
||||
}
|
||||
|
||||
static func parseAtDate(_ value: String) -> Date? {
|
||||
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if trimmed.isEmpty { return nil }
|
||||
if let date = makeIsoFormatter(withFractional: true).date(from: trimmed) { return date }
|
||||
return makeIsoFormatter(withFractional: false).date(from: trimmed)
|
||||
}
|
||||
|
||||
static func formatIsoDate(_ date: Date) -> String {
|
||||
makeIsoFormatter(withFractional: false).string(from: date)
|
||||
}
|
||||
|
||||
private static func makeIsoFormatter(withFractional: Bool) -> ISO8601DateFormatter {
|
||||
let formatter = ISO8601DateFormatter()
|
||||
formatter.formatOptions = withFractional
|
||||
? [.withInternetDateTime, .withFractionalSeconds]
|
||||
: [.withInternetDateTime]
|
||||
return formatter
|
||||
}
|
||||
}
|
||||
|
||||
enum CronPayload: Codable, Equatable {
|
||||
@@ -131,10 +178,6 @@ enum CronPayload: Codable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
struct CronIsolation: Codable, Equatable {
|
||||
var postToMainPrefix: String?
|
||||
}
|
||||
|
||||
struct CronJobState: Codable, Equatable {
|
||||
var nextRunAtMs: Int?
|
||||
var runningAtMs: Int?
|
||||
@@ -157,7 +200,7 @@ struct CronJob: Identifiable, Codable, Equatable {
|
||||
let sessionTarget: CronSessionTarget
|
||||
let wakeMode: CronWakeMode
|
||||
let payload: CronPayload
|
||||
let isolation: CronIsolation?
|
||||
let delivery: CronDelivery?
|
||||
let state: CronJobState
|
||||
|
||||
var displayName: String {
|
||||
|
||||
@@ -17,9 +17,11 @@ extension CronSettings {
|
||||
|
||||
func scheduleSummary(_ schedule: CronSchedule) -> String {
|
||||
switch schedule {
|
||||
case let .at(atMs):
|
||||
let date = Date(timeIntervalSince1970: TimeInterval(atMs) / 1000)
|
||||
return "at \(date.formatted(date: .abbreviated, time: .standard))"
|
||||
case let .at(at):
|
||||
if let date = CronSchedule.parseAtDate(at) {
|
||||
return "at \(date.formatted(date: .abbreviated, time: .standard))"
|
||||
}
|
||||
return "at \(at)"
|
||||
case let .every(everyMs, _):
|
||||
return "every \(self.formatDuration(ms: everyMs))"
|
||||
case let .cron(expr, tz):
|
||||
|
||||
@@ -128,7 +128,7 @@ extension CronSettings {
|
||||
.foregroundStyle(.orange)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
self.payloadSummary(job.payload)
|
||||
self.payloadSummary(job)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(10)
|
||||
@@ -205,8 +205,9 @@ extension CronSettings {
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
|
||||
func payloadSummary(_ payload: CronPayload) -> some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
func payloadSummary(_ job: CronJob) -> some View {
|
||||
let payload = job.payload
|
||||
return VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Payload")
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundStyle(.secondary)
|
||||
@@ -215,7 +216,7 @@ extension CronSettings {
|
||||
Text(text)
|
||||
.font(.callout)
|
||||
.textSelection(.enabled)
|
||||
case let .agentTurn(message, thinking, timeoutSeconds, deliver, provider, to, _):
|
||||
case let .agentTurn(message, thinking, timeoutSeconds, _, _, _, _):
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(message)
|
||||
.font(.callout)
|
||||
@@ -223,10 +224,19 @@ extension CronSettings {
|
||||
HStack(spacing: 8) {
|
||||
if let thinking, !thinking.isEmpty { StatusPill(text: "think \(thinking)", tint: .secondary) }
|
||||
if let timeoutSeconds { StatusPill(text: "\(timeoutSeconds)s", tint: .secondary) }
|
||||
if deliver ?? false {
|
||||
StatusPill(text: "deliver", tint: .secondary)
|
||||
if let provider, !provider.isEmpty { StatusPill(text: provider, tint: .secondary) }
|
||||
if let to, !to.isEmpty { StatusPill(text: to, tint: .secondary) }
|
||||
if job.sessionTarget == .isolated {
|
||||
let delivery = job.delivery
|
||||
if let delivery {
|
||||
if delivery.mode == .announce {
|
||||
StatusPill(text: "announce", tint: .secondary)
|
||||
if let channel = delivery.channel, !channel.isEmpty {
|
||||
StatusPill(text: channel, tint: .secondary)
|
||||
}
|
||||
if let to = delivery.to, !to.isEmpty { StatusPill(text: to, tint: .secondary) }
|
||||
} else {
|
||||
StatusPill(text: "no delivery", tint: .secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,11 +21,11 @@ struct CronSettings_Previews: PreviewProvider {
|
||||
message: "Summarize inbox",
|
||||
thinking: "low",
|
||||
timeoutSeconds: 600,
|
||||
deliver: true,
|
||||
channel: "last",
|
||||
deliver: nil,
|
||||
channel: nil,
|
||||
to: nil,
|
||||
bestEffortDeliver: true),
|
||||
isolation: CronIsolation(postToMainPrefix: "Cron"),
|
||||
bestEffortDeliver: nil),
|
||||
delivery: CronDelivery(mode: .announce, channel: "last", to: nil, bestEffort: true),
|
||||
state: CronJobState(
|
||||
nextRunAtMs: Int(Date().addingTimeInterval(3600).timeIntervalSince1970 * 1000),
|
||||
runningAtMs: nil,
|
||||
@@ -75,11 +75,11 @@ extension CronSettings {
|
||||
message: "Summarize",
|
||||
thinking: "low",
|
||||
timeoutSeconds: 120,
|
||||
deliver: true,
|
||||
channel: "whatsapp",
|
||||
to: "+15551234567",
|
||||
bestEffortDeliver: true),
|
||||
isolation: CronIsolation(postToMainPrefix: "[cron] "),
|
||||
deliver: nil,
|
||||
channel: nil,
|
||||
to: nil,
|
||||
bestEffortDeliver: nil),
|
||||
delivery: CronDelivery(mode: .announce, channel: "whatsapp", to: "+15551234567", bestEffort: true),
|
||||
state: CronJobState(
|
||||
nextRunAtMs: 1_700_000_200_000,
|
||||
runningAtMs: nil,
|
||||
@@ -111,7 +111,7 @@ extension CronSettings {
|
||||
_ = view.detailCard(job)
|
||||
_ = view.runHistoryCard(job)
|
||||
_ = view.runRow(run)
|
||||
_ = view.payloadSummary(job.payload)
|
||||
_ = view.payloadSummary(job)
|
||||
_ = view.scheduleSummary(job.schedule)
|
||||
_ = view.statusTint(job.state.lastStatus)
|
||||
_ = view.nextRunLabel(Date())
|
||||
|
||||
@@ -335,7 +335,7 @@ extension OnboardingView {
|
||||
.multilineTextAlignment(.center)
|
||||
.frame(maxWidth: 540)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
Text("OpenClaw supports any model — we strongly recommend Opus 4.5 for the best experience.")
|
||||
Text("OpenClaw supports any model — we strongly recommend Opus 4.6 for the best experience.")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2026.1.29</string>
|
||||
<string>2026.2.6</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>202601290</string>
|
||||
<string>202602020</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>OpenClaw</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -169,7 +169,7 @@ extension SessionRow {
|
||||
systemSent: true,
|
||||
abortedLastRun: true,
|
||||
tokens: SessionTokenStats(input: 5000, output: 1200, total: 6200, contextTokens: 200_000),
|
||||
model: "claude-opus-4-5"),
|
||||
model: "claude-opus-4-6"),
|
||||
SessionRow(
|
||||
id: "global",
|
||||
key: "global",
|
||||
@@ -242,7 +242,7 @@ struct SessionStoreSnapshot {
|
||||
|
||||
@MainActor
|
||||
enum SessionLoader {
|
||||
static let fallbackModel = "claude-opus-4-5"
|
||||
static let fallbackModel = "claude-opus-4-6"
|
||||
static let fallbackContextTokens = 200_000
|
||||
|
||||
static let defaultStorePath = standardize(
|
||||
|
||||
@@ -222,9 +222,9 @@ enum WideAreaGatewayDiscovery {
|
||||
process.executableURL = URL(fileURLWithPath: path)
|
||||
process.arguments = args
|
||||
let outPipe = Pipe()
|
||||
let errPipe = Pipe()
|
||||
process.standardOutput = outPipe
|
||||
process.standardError = errPipe
|
||||
// Avoid stderr pipe backpressure; we don't consume it.
|
||||
process.standardError = FileHandle.nullDevice
|
||||
|
||||
do {
|
||||
try process.run()
|
||||
|
||||
@@ -589,20 +589,24 @@ public struct AgentIdentityResult: Codable, Sendable {
|
||||
public let agentid: String
|
||||
public let name: String?
|
||||
public let avatar: String?
|
||||
public let emoji: String?
|
||||
|
||||
public init(
|
||||
agentid: String,
|
||||
name: String?,
|
||||
avatar: String?
|
||||
avatar: String?,
|
||||
emoji: String?
|
||||
) {
|
||||
self.agentid = agentid
|
||||
self.name = name
|
||||
self.avatar = avatar
|
||||
self.emoji = emoji
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case agentid = "agentId"
|
||||
case name
|
||||
case avatar
|
||||
case emoji
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1115,6 +1119,35 @@ public struct SessionsCompactParams: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsUsageParams: Codable, Sendable {
|
||||
public let key: String?
|
||||
public let startdate: String?
|
||||
public let enddate: String?
|
||||
public let limit: Int?
|
||||
public let includecontextweight: Bool?
|
||||
|
||||
public init(
|
||||
key: String?,
|
||||
startdate: String?,
|
||||
enddate: String?,
|
||||
limit: Int?,
|
||||
includecontextweight: Bool?
|
||||
) {
|
||||
self.key = key
|
||||
self.startdate = startdate
|
||||
self.enddate = enddate
|
||||
self.limit = limit
|
||||
self.includecontextweight = includecontextweight
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case key
|
||||
case startdate = "startDate"
|
||||
case enddate = "endDate"
|
||||
case limit
|
||||
case includecontextweight = "includeContextWeight"
|
||||
}
|
||||
}
|
||||
|
||||
public struct ConfigGetParams: Codable, Sendable {
|
||||
}
|
||||
|
||||
@@ -1556,6 +1589,291 @@ public struct AgentSummary: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct AgentsCreateParams: Codable, Sendable {
|
||||
public let name: String
|
||||
public let workspace: String
|
||||
public let emoji: String?
|
||||
public let avatar: String?
|
||||
|
||||
public init(
|
||||
name: String,
|
||||
workspace: String,
|
||||
emoji: String?,
|
||||
avatar: String?
|
||||
) {
|
||||
self.name = name
|
||||
self.workspace = workspace
|
||||
self.emoji = emoji
|
||||
self.avatar = avatar
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case name
|
||||
case workspace
|
||||
case emoji
|
||||
case avatar
|
||||
}
|
||||
}
|
||||
|
||||
public struct AgentsCreateResult: Codable, Sendable {
|
||||
public let ok: Bool
|
||||
public let agentid: String
|
||||
public let name: String
|
||||
public let workspace: String
|
||||
|
||||
public init(
|
||||
ok: Bool,
|
||||
agentid: String,
|
||||
name: String,
|
||||
workspace: String
|
||||
) {
|
||||
self.ok = ok
|
||||
self.agentid = agentid
|
||||
self.name = name
|
||||
self.workspace = workspace
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case ok
|
||||
case agentid = "agentId"
|
||||
case name
|
||||
case workspace
|
||||
}
|
||||
}
|
||||
|
||||
public struct AgentsUpdateParams: Codable, Sendable {
|
||||
public let agentid: String
|
||||
public let name: String?
|
||||
public let workspace: String?
|
||||
public let model: String?
|
||||
public let avatar: String?
|
||||
|
||||
public init(
|
||||
agentid: String,
|
||||
name: String?,
|
||||
workspace: String?,
|
||||
model: String?,
|
||||
avatar: String?
|
||||
) {
|
||||
self.agentid = agentid
|
||||
self.name = name
|
||||
self.workspace = workspace
|
||||
self.model = model
|
||||
self.avatar = avatar
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case agentid = "agentId"
|
||||
case name
|
||||
case workspace
|
||||
case model
|
||||
case avatar
|
||||
}
|
||||
}
|
||||
|
||||
public struct AgentsUpdateResult: Codable, Sendable {
|
||||
public let ok: Bool
|
||||
public let agentid: String
|
||||
|
||||
public init(
|
||||
ok: Bool,
|
||||
agentid: String
|
||||
) {
|
||||
self.ok = ok
|
||||
self.agentid = agentid
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case ok
|
||||
case agentid = "agentId"
|
||||
}
|
||||
}
|
||||
|
||||
public struct AgentsDeleteParams: Codable, Sendable {
|
||||
public let agentid: String
|
||||
public let deletefiles: Bool?
|
||||
|
||||
public init(
|
||||
agentid: String,
|
||||
deletefiles: Bool?
|
||||
) {
|
||||
self.agentid = agentid
|
||||
self.deletefiles = deletefiles
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case agentid = "agentId"
|
||||
case deletefiles = "deleteFiles"
|
||||
}
|
||||
}
|
||||
|
||||
public struct AgentsDeleteResult: Codable, Sendable {
|
||||
public let ok: Bool
|
||||
public let agentid: String
|
||||
public let removedbindings: Int
|
||||
|
||||
public init(
|
||||
ok: Bool,
|
||||
agentid: String,
|
||||
removedbindings: Int
|
||||
) {
|
||||
self.ok = ok
|
||||
self.agentid = agentid
|
||||
self.removedbindings = removedbindings
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case ok
|
||||
case agentid = "agentId"
|
||||
case removedbindings = "removedBindings"
|
||||
}
|
||||
}
|
||||
|
||||
public struct AgentsFileEntry: Codable, Sendable {
|
||||
public let name: String
|
||||
public let path: String
|
||||
public let missing: Bool
|
||||
public let size: Int?
|
||||
public let updatedatms: Int?
|
||||
public let content: String?
|
||||
|
||||
public init(
|
||||
name: String,
|
||||
path: String,
|
||||
missing: Bool,
|
||||
size: Int?,
|
||||
updatedatms: Int?,
|
||||
content: String?
|
||||
) {
|
||||
self.name = name
|
||||
self.path = path
|
||||
self.missing = missing
|
||||
self.size = size
|
||||
self.updatedatms = updatedatms
|
||||
self.content = content
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case name
|
||||
case path
|
||||
case missing
|
||||
case size
|
||||
case updatedatms = "updatedAtMs"
|
||||
case content
|
||||
}
|
||||
}
|
||||
|
||||
public struct AgentsFilesListParams: Codable, Sendable {
|
||||
public let agentid: String
|
||||
|
||||
public init(
|
||||
agentid: String
|
||||
) {
|
||||
self.agentid = agentid
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case agentid = "agentId"
|
||||
}
|
||||
}
|
||||
|
||||
public struct AgentsFilesListResult: Codable, Sendable {
|
||||
public let agentid: String
|
||||
public let workspace: String
|
||||
public let files: [AgentsFileEntry]
|
||||
|
||||
public init(
|
||||
agentid: String,
|
||||
workspace: String,
|
||||
files: [AgentsFileEntry]
|
||||
) {
|
||||
self.agentid = agentid
|
||||
self.workspace = workspace
|
||||
self.files = files
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case agentid = "agentId"
|
||||
case workspace
|
||||
case files
|
||||
}
|
||||
}
|
||||
|
||||
public struct AgentsFilesGetParams: Codable, Sendable {
|
||||
public let agentid: String
|
||||
public let name: String
|
||||
|
||||
public init(
|
||||
agentid: String,
|
||||
name: String
|
||||
) {
|
||||
self.agentid = agentid
|
||||
self.name = name
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case agentid = "agentId"
|
||||
case name
|
||||
}
|
||||
}
|
||||
|
||||
public struct AgentsFilesGetResult: Codable, Sendable {
|
||||
public let agentid: String
|
||||
public let workspace: String
|
||||
public let file: AgentsFileEntry
|
||||
|
||||
public init(
|
||||
agentid: String,
|
||||
workspace: String,
|
||||
file: AgentsFileEntry
|
||||
) {
|
||||
self.agentid = agentid
|
||||
self.workspace = workspace
|
||||
self.file = file
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case agentid = "agentId"
|
||||
case workspace
|
||||
case file
|
||||
}
|
||||
}
|
||||
|
||||
public struct AgentsFilesSetParams: Codable, Sendable {
|
||||
public let agentid: String
|
||||
public let name: String
|
||||
public let content: String
|
||||
|
||||
public init(
|
||||
agentid: String,
|
||||
name: String,
|
||||
content: String
|
||||
) {
|
||||
self.agentid = agentid
|
||||
self.name = name
|
||||
self.content = content
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case agentid = "agentId"
|
||||
case name
|
||||
case content
|
||||
}
|
||||
}
|
||||
|
||||
public struct AgentsFilesSetResult: Codable, Sendable {
|
||||
public let ok: Bool
|
||||
public let agentid: String
|
||||
public let workspace: String
|
||||
public let file: AgentsFileEntry
|
||||
|
||||
public init(
|
||||
ok: Bool,
|
||||
agentid: String,
|
||||
workspace: String,
|
||||
file: AgentsFileEntry
|
||||
) {
|
||||
self.ok = ok
|
||||
self.agentid = agentid
|
||||
self.workspace = workspace
|
||||
self.file = file
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case ok
|
||||
case agentid = "agentId"
|
||||
case workspace
|
||||
case file
|
||||
}
|
||||
}
|
||||
|
||||
public struct AgentsListParams: Codable, Sendable {
|
||||
}
|
||||
|
||||
@@ -1630,6 +1948,16 @@ public struct ModelsListResult: Codable, Sendable {
|
||||
}
|
||||
|
||||
public struct SkillsStatusParams: Codable, Sendable {
|
||||
public let agentid: String?
|
||||
|
||||
public init(
|
||||
agentid: String?
|
||||
) {
|
||||
self.agentid = agentid
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case agentid = "agentId"
|
||||
}
|
||||
}
|
||||
|
||||
public struct SkillsBinsParams: Codable, Sendable {
|
||||
@@ -1707,7 +2035,7 @@ public struct CronJob: Codable, Sendable {
|
||||
public let sessiontarget: AnyCodable
|
||||
public let wakemode: AnyCodable
|
||||
public let payload: AnyCodable
|
||||
public let isolation: [String: AnyCodable]?
|
||||
public let delivery: [String: AnyCodable]?
|
||||
public let state: [String: AnyCodable]
|
||||
|
||||
public init(
|
||||
@@ -1723,7 +2051,7 @@ public struct CronJob: Codable, Sendable {
|
||||
sessiontarget: AnyCodable,
|
||||
wakemode: AnyCodable,
|
||||
payload: AnyCodable,
|
||||
isolation: [String: AnyCodable]?,
|
||||
delivery: [String: AnyCodable]?,
|
||||
state: [String: AnyCodable]
|
||||
) {
|
||||
self.id = id
|
||||
@@ -1738,7 +2066,7 @@ public struct CronJob: Codable, Sendable {
|
||||
self.sessiontarget = sessiontarget
|
||||
self.wakemode = wakemode
|
||||
self.payload = payload
|
||||
self.isolation = isolation
|
||||
self.delivery = delivery
|
||||
self.state = state
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
@@ -1754,7 +2082,7 @@ public struct CronJob: Codable, Sendable {
|
||||
case sessiontarget = "sessionTarget"
|
||||
case wakemode = "wakeMode"
|
||||
case payload
|
||||
case isolation
|
||||
case delivery
|
||||
case state
|
||||
}
|
||||
}
|
||||
@@ -1785,7 +2113,7 @@ public struct CronAddParams: Codable, Sendable {
|
||||
public let sessiontarget: AnyCodable
|
||||
public let wakemode: AnyCodable
|
||||
public let payload: AnyCodable
|
||||
public let isolation: [String: AnyCodable]?
|
||||
public let delivery: [String: AnyCodable]?
|
||||
|
||||
public init(
|
||||
name: String,
|
||||
@@ -1797,7 +2125,7 @@ public struct CronAddParams: Codable, Sendable {
|
||||
sessiontarget: AnyCodable,
|
||||
wakemode: AnyCodable,
|
||||
payload: AnyCodable,
|
||||
isolation: [String: AnyCodable]?
|
||||
delivery: [String: AnyCodable]?
|
||||
) {
|
||||
self.name = name
|
||||
self.agentid = agentid
|
||||
@@ -1808,7 +2136,7 @@ public struct CronAddParams: Codable, Sendable {
|
||||
self.sessiontarget = sessiontarget
|
||||
self.wakemode = wakemode
|
||||
self.payload = payload
|
||||
self.isolation = isolation
|
||||
self.delivery = delivery
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case name
|
||||
@@ -1820,7 +2148,7 @@ public struct CronAddParams: Codable, Sendable {
|
||||
case sessiontarget = "sessionTarget"
|
||||
case wakemode = "wakeMode"
|
||||
case payload
|
||||
case isolation
|
||||
case delivery
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1831,6 +2159,8 @@ public struct CronRunLogEntry: Codable, Sendable {
|
||||
public let status: AnyCodable?
|
||||
public let error: String?
|
||||
public let summary: String?
|
||||
public let sessionid: String?
|
||||
public let sessionkey: String?
|
||||
public let runatms: Int?
|
||||
public let durationms: Int?
|
||||
public let nextrunatms: Int?
|
||||
@@ -1842,6 +2172,8 @@ public struct CronRunLogEntry: Codable, Sendable {
|
||||
status: AnyCodable?,
|
||||
error: String?,
|
||||
summary: String?,
|
||||
sessionid: String?,
|
||||
sessionkey: String?,
|
||||
runatms: Int?,
|
||||
durationms: Int?,
|
||||
nextrunatms: Int?
|
||||
@@ -1852,6 +2184,8 @@ public struct CronRunLogEntry: Codable, Sendable {
|
||||
self.status = status
|
||||
self.error = error
|
||||
self.summary = summary
|
||||
self.sessionid = sessionid
|
||||
self.sessionkey = sessionkey
|
||||
self.runatms = runatms
|
||||
self.durationms = durationms
|
||||
self.nextrunatms = nextrunatms
|
||||
@@ -1863,6 +2197,8 @@ public struct CronRunLogEntry: Codable, Sendable {
|
||||
case status
|
||||
case error
|
||||
case summary
|
||||
case sessionid = "sessionId"
|
||||
case sessionkey = "sessionKey"
|
||||
case runatms = "runAtMs"
|
||||
case durationms = "durationMs"
|
||||
case nextrunatms = "nextRunAtMs"
|
||||
|
||||
@@ -40,11 +40,11 @@ struct CronJobEditorSmokeTests {
|
||||
message: "Summarize the last day",
|
||||
thinking: "low",
|
||||
timeoutSeconds: 120,
|
||||
deliver: true,
|
||||
channel: "whatsapp",
|
||||
to: "+15551234567",
|
||||
bestEffortDeliver: true),
|
||||
isolation: CronIsolation(postToMainPrefix: "Cron"),
|
||||
deliver: nil,
|
||||
channel: nil,
|
||||
to: nil,
|
||||
bestEffortDeliver: nil),
|
||||
delivery: CronDelivery(mode: .announce, channel: "whatsapp", to: "+15551234567", bestEffort: true),
|
||||
state: CronJobState(
|
||||
nextRunAtMs: 1_700_000_100_000,
|
||||
runningAtMs: nil,
|
||||
|
||||
@@ -5,12 +5,24 @@ import Testing
|
||||
@Suite
|
||||
struct CronModelsTests {
|
||||
@Test func scheduleAtEncodesAndDecodes() throws {
|
||||
let schedule = CronSchedule.at(atMs: 123)
|
||||
let schedule = CronSchedule.at(at: "2026-02-03T18:00:00Z")
|
||||
let data = try JSONEncoder().encode(schedule)
|
||||
let decoded = try JSONDecoder().decode(CronSchedule.self, from: data)
|
||||
#expect(decoded == schedule)
|
||||
}
|
||||
|
||||
@Test func scheduleAtDecodesLegacyAtMs() throws {
|
||||
let json = """
|
||||
{"kind":"at","atMs":1700000000000}
|
||||
"""
|
||||
let decoded = try JSONDecoder().decode(CronSchedule.self, from: Data(json.utf8))
|
||||
if case let .at(at) = decoded {
|
||||
#expect(at.hasPrefix("2023-"))
|
||||
} else {
|
||||
#expect(Bool(false))
|
||||
}
|
||||
}
|
||||
|
||||
@Test func scheduleEveryEncodesAndDecodesWithAnchor() throws {
|
||||
let schedule = CronSchedule.every(everyMs: 5000, anchorMs: 10000)
|
||||
let data = try JSONEncoder().encode(schedule)
|
||||
@@ -49,11 +61,11 @@ struct CronModelsTests {
|
||||
deleteAfterRun: true,
|
||||
createdAtMs: 0,
|
||||
updatedAtMs: 0,
|
||||
schedule: .at(atMs: 1_700_000_000_000),
|
||||
schedule: .at(at: "2026-02-03T18:00:00Z"),
|
||||
sessionTarget: .main,
|
||||
wakeMode: .now,
|
||||
payload: .systemEvent(text: "ping"),
|
||||
isolation: nil,
|
||||
delivery: nil,
|
||||
state: CronJobState())
|
||||
let data = try JSONEncoder().encode(job)
|
||||
let decoded = try JSONDecoder().decode(CronJob.self, from: data)
|
||||
@@ -62,7 +74,7 @@ struct CronModelsTests {
|
||||
|
||||
@Test func scheduleDecodeRejectsUnknownKind() {
|
||||
let json = """
|
||||
{"kind":"wat","atMs":1}
|
||||
{"kind":"wat","at":"2026-02-03T18:00:00Z"}
|
||||
"""
|
||||
#expect(throws: DecodingError.self) {
|
||||
_ = try JSONDecoder().decode(CronSchedule.self, from: Data(json.utf8))
|
||||
@@ -88,11 +100,11 @@ struct CronModelsTests {
|
||||
deleteAfterRun: nil,
|
||||
createdAtMs: 0,
|
||||
updatedAtMs: 0,
|
||||
schedule: .at(atMs: 0),
|
||||
schedule: .at(at: "2026-02-03T18:00:00Z"),
|
||||
sessionTarget: .main,
|
||||
wakeMode: .now,
|
||||
payload: .systemEvent(text: "hi"),
|
||||
isolation: nil,
|
||||
delivery: nil,
|
||||
state: CronJobState())
|
||||
#expect(base.displayName == "hello")
|
||||
|
||||
@@ -111,11 +123,11 @@ struct CronModelsTests {
|
||||
deleteAfterRun: nil,
|
||||
createdAtMs: 0,
|
||||
updatedAtMs: 0,
|
||||
schedule: .at(atMs: 0),
|
||||
schedule: .at(at: "2026-02-03T18:00:00Z"),
|
||||
sessionTarget: .main,
|
||||
wakeMode: .now,
|
||||
payload: .systemEvent(text: "hi"),
|
||||
isolation: nil,
|
||||
delivery: nil,
|
||||
state: CronJobState(
|
||||
nextRunAtMs: 1_700_000_000_000,
|
||||
runningAtMs: nil,
|
||||
|
||||
@@ -23,7 +23,7 @@ struct MenuSessionsInjectorTests {
|
||||
let injector = MenuSessionsInjector()
|
||||
injector.setTestingControlChannelConnected(true)
|
||||
|
||||
let defaults = SessionDefaults(model: "anthropic/claude-opus-4-5", contextTokens: 200_000)
|
||||
let defaults = SessionDefaults(model: "anthropic/claude-opus-4-6", contextTokens: 200_000)
|
||||
let rows = [
|
||||
SessionRow(
|
||||
id: "main",
|
||||
@@ -41,7 +41,7 @@ struct MenuSessionsInjectorTests {
|
||||
systemSent: false,
|
||||
abortedLastRun: false,
|
||||
tokens: SessionTokenStats(input: 10, output: 20, total: 30, contextTokens: 200_000),
|
||||
model: "claude-opus-4-5"),
|
||||
model: "claude-opus-4-6"),
|
||||
SessionRow(
|
||||
id: "discord:group:alpha",
|
||||
key: "discord:group:alpha",
|
||||
@@ -58,7 +58,7 @@ struct MenuSessionsInjectorTests {
|
||||
systemSent: true,
|
||||
abortedLastRun: true,
|
||||
tokens: SessionTokenStats(input: 50, output: 50, total: 100, contextTokens: 200_000),
|
||||
model: "claude-opus-4-5"),
|
||||
model: "claude-opus-4-6"),
|
||||
]
|
||||
let snapshot = SessionStoreSnapshot(
|
||||
storePath: "/tmp/sessions.json",
|
||||
|
||||
@@ -23,7 +23,7 @@ struct SettingsViewSmokeTests {
|
||||
sessionTarget: .main,
|
||||
wakeMode: .now,
|
||||
payload: .systemEvent(text: "ping"),
|
||||
isolation: nil,
|
||||
delivery: nil,
|
||||
state: CronJobState(
|
||||
nextRunAtMs: 1_700_000_200_000,
|
||||
runningAtMs: nil,
|
||||
@@ -48,11 +48,11 @@ struct SettingsViewSmokeTests {
|
||||
message: "hello",
|
||||
thinking: "low",
|
||||
timeoutSeconds: 30,
|
||||
deliver: true,
|
||||
channel: "sms",
|
||||
to: "+15551234567",
|
||||
bestEffortDeliver: true),
|
||||
isolation: CronIsolation(postToMainPrefix: "[cron] "),
|
||||
deliver: nil,
|
||||
channel: nil,
|
||||
to: nil,
|
||||
bestEffortDeliver: nil),
|
||||
delivery: CronDelivery(mode: .announce, channel: "sms", to: "+15551234567", bestEffort: true),
|
||||
state: CronJobState(
|
||||
nextRunAtMs: nil,
|
||||
runningAtMs: nil,
|
||||
|
||||
@@ -416,7 +416,9 @@ public actor GatewayChannelActor {
|
||||
guard let self else { return }
|
||||
await self.watchTicks()
|
||||
}
|
||||
await self.pushHandler?(.snapshot(ok))
|
||||
if let pushHandler = self.pushHandler {
|
||||
Task { await pushHandler(.snapshot(ok)) }
|
||||
}
|
||||
}
|
||||
|
||||
private func listen() {
|
||||
|
||||
@@ -11,10 +11,12 @@ private struct NodeInvokeRequestPayload: Codable, Sendable {
|
||||
var idempotencyKey: String?
|
||||
}
|
||||
|
||||
|
||||
public actor GatewayNodeSession {
|
||||
private let logger = Logger(subsystem: "ai.openclaw", category: "node.gateway")
|
||||
private let decoder = JSONDecoder()
|
||||
private let encoder = JSONEncoder()
|
||||
private static let defaultInvokeTimeoutMs = 30_000
|
||||
private var channel: GatewayChannelActor?
|
||||
private var activeURL: URL?
|
||||
private var activeToken: String?
|
||||
@@ -23,34 +25,78 @@ public actor GatewayNodeSession {
|
||||
private var onConnected: (@Sendable () async -> Void)?
|
||||
private var onDisconnected: (@Sendable (String) async -> Void)?
|
||||
private var onInvoke: (@Sendable (BridgeInvokeRequest) async -> BridgeInvokeResponse)?
|
||||
private var hasNotifiedConnected = false
|
||||
private var snapshotReceived = false
|
||||
private var snapshotWaiters: [CheckedContinuation<Bool, Never>] = []
|
||||
|
||||
static func invokeWithTimeout(
|
||||
request: BridgeInvokeRequest,
|
||||
timeoutMs: Int?,
|
||||
onInvoke: @escaping @Sendable (BridgeInvokeRequest) async -> BridgeInvokeResponse
|
||||
) async -> BridgeInvokeResponse {
|
||||
let timeout = max(0, timeoutMs ?? 0)
|
||||
let timeoutLogger = Logger(subsystem: "ai.openclaw", category: "node.gateway")
|
||||
let timeout: Int = {
|
||||
if let timeoutMs { return max(0, timeoutMs) }
|
||||
return Self.defaultInvokeTimeoutMs
|
||||
}()
|
||||
guard timeout > 0 else {
|
||||
return await onInvoke(request)
|
||||
}
|
||||
|
||||
return await withTaskGroup(of: BridgeInvokeResponse.self) { group in
|
||||
group.addTask { await onInvoke(request) }
|
||||
group.addTask {
|
||||
// Use an explicit latch so timeouts win even if onInvoke blocks (e.g., permission prompts).
|
||||
final class InvokeLatch: @unchecked Sendable {
|
||||
private let lock = NSLock()
|
||||
private var continuation: CheckedContinuation<BridgeInvokeResponse, Never>?
|
||||
private var resumed = false
|
||||
|
||||
func setContinuation(_ continuation: CheckedContinuation<BridgeInvokeResponse, Never>) {
|
||||
self.lock.lock()
|
||||
defer { self.lock.unlock() }
|
||||
self.continuation = continuation
|
||||
}
|
||||
|
||||
func resume(_ response: BridgeInvokeResponse) {
|
||||
let cont: CheckedContinuation<BridgeInvokeResponse, Never>?
|
||||
self.lock.lock()
|
||||
if self.resumed {
|
||||
self.lock.unlock()
|
||||
return
|
||||
}
|
||||
self.resumed = true
|
||||
cont = self.continuation
|
||||
self.continuation = nil
|
||||
self.lock.unlock()
|
||||
cont?.resume(returning: response)
|
||||
}
|
||||
}
|
||||
|
||||
let latch = InvokeLatch()
|
||||
var onInvokeTask: Task<Void, Never>?
|
||||
var timeoutTask: Task<Void, Never>?
|
||||
defer {
|
||||
onInvokeTask?.cancel()
|
||||
timeoutTask?.cancel()
|
||||
}
|
||||
let response = await withCheckedContinuation { (cont: CheckedContinuation<BridgeInvokeResponse, Never>) in
|
||||
latch.setContinuation(cont)
|
||||
onInvokeTask = Task.detached {
|
||||
let result = await onInvoke(request)
|
||||
latch.resume(result)
|
||||
}
|
||||
timeoutTask = Task.detached {
|
||||
try? await Task.sleep(nanoseconds: UInt64(timeout) * 1_000_000)
|
||||
return BridgeInvokeResponse(
|
||||
timeoutLogger.info("node invoke timeout fired id=\(request.id, privacy: .public)")
|
||||
latch.resume(BridgeInvokeResponse(
|
||||
id: request.id,
|
||||
ok: false,
|
||||
error: OpenClawNodeError(
|
||||
code: .unavailable,
|
||||
message: "node invoke timed out")
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
let first = await group.next()!
|
||||
group.cancelAll()
|
||||
return first
|
||||
}
|
||||
timeoutLogger.info("node invoke race resolved id=\(request.id, privacy: .public) ok=\(response.ok, privacy: .public)")
|
||||
return response
|
||||
}
|
||||
private var serverEventSubscribers: [UUID: AsyncStream<EventFrame>.Continuation] = [:]
|
||||
private var canvasHostUrl: String?
|
||||
@@ -78,6 +124,7 @@ public actor GatewayNodeSession {
|
||||
self.onInvoke = onInvoke
|
||||
|
||||
if shouldReconnect {
|
||||
self.resetConnectionState()
|
||||
if let existing = self.channel {
|
||||
await existing.shutdown()
|
||||
}
|
||||
@@ -107,7 +154,8 @@ public actor GatewayNodeSession {
|
||||
|
||||
do {
|
||||
try await channel.connect()
|
||||
await onConnected()
|
||||
_ = await self.waitForSnapshot(timeoutMs: 500)
|
||||
await self.notifyConnectedIfNeeded()
|
||||
} catch {
|
||||
await onDisconnected(error.localizedDescription)
|
||||
throw error
|
||||
@@ -120,6 +168,7 @@ public actor GatewayNodeSession {
|
||||
self.activeURL = nil
|
||||
self.activeToken = nil
|
||||
self.activePassword = nil
|
||||
self.resetConnectionState()
|
||||
}
|
||||
|
||||
public func currentCanvasHostUrl() -> String? {
|
||||
@@ -179,7 +228,8 @@ public actor GatewayNodeSession {
|
||||
case let .snapshot(ok):
|
||||
let raw = ok.canvashosturl?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
self.canvasHostUrl = (raw?.isEmpty == false) ? raw : nil
|
||||
await self.onConnected?()
|
||||
self.markSnapshotReceived()
|
||||
await self.notifyConnectedIfNeeded()
|
||||
case let .event(evt):
|
||||
await self.handleEvent(evt)
|
||||
default:
|
||||
@@ -187,28 +237,98 @@ public actor GatewayNodeSession {
|
||||
}
|
||||
}
|
||||
|
||||
private func resetConnectionState() {
|
||||
self.hasNotifiedConnected = false
|
||||
self.snapshotReceived = false
|
||||
if !self.snapshotWaiters.isEmpty {
|
||||
let waiters = self.snapshotWaiters
|
||||
self.snapshotWaiters.removeAll()
|
||||
for waiter in waiters {
|
||||
waiter.resume(returning: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func markSnapshotReceived() {
|
||||
self.snapshotReceived = true
|
||||
if !self.snapshotWaiters.isEmpty {
|
||||
let waiters = self.snapshotWaiters
|
||||
self.snapshotWaiters.removeAll()
|
||||
for waiter in waiters {
|
||||
waiter.resume(returning: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func waitForSnapshot(timeoutMs: Int) async -> Bool {
|
||||
if self.snapshotReceived { return true }
|
||||
let clamped = max(0, timeoutMs)
|
||||
return await withCheckedContinuation { cont in
|
||||
self.snapshotWaiters.append(cont)
|
||||
Task { [weak self] in
|
||||
guard let self else { return }
|
||||
try? await Task.sleep(nanoseconds: UInt64(clamped) * 1_000_000)
|
||||
await self.timeoutSnapshotWaiters()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func timeoutSnapshotWaiters() {
|
||||
guard !self.snapshotReceived else { return }
|
||||
if !self.snapshotWaiters.isEmpty {
|
||||
let waiters = self.snapshotWaiters
|
||||
self.snapshotWaiters.removeAll()
|
||||
for waiter in waiters {
|
||||
waiter.resume(returning: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func notifyConnectedIfNeeded() async {
|
||||
guard !self.hasNotifiedConnected else { return }
|
||||
self.hasNotifiedConnected = true
|
||||
await self.onConnected?()
|
||||
}
|
||||
|
||||
private func handleEvent(_ evt: EventFrame) async {
|
||||
self.broadcastServerEvent(evt)
|
||||
guard evt.event == "node.invoke.request" else { return }
|
||||
self.logger.info("node invoke request received")
|
||||
guard let payload = evt.payload else { return }
|
||||
do {
|
||||
let data = try self.encoder.encode(payload)
|
||||
let request = try self.decoder.decode(NodeInvokeRequestPayload.self, from: data)
|
||||
let request = try self.decodeInvokeRequest(from: payload)
|
||||
let timeoutLabel = request.timeoutMs.map(String.init) ?? "none"
|
||||
self.logger.info("node invoke request decoded id=\(request.id, privacy: .public) command=\(request.command, privacy: .public) timeoutMs=\(timeoutLabel, privacy: .public)")
|
||||
guard let onInvoke else { return }
|
||||
let req = BridgeInvokeRequest(id: request.id, command: request.command, paramsJSON: request.paramsJSON)
|
||||
self.logger.info("node invoke executing id=\(request.id, privacy: .public)")
|
||||
let response = await Self.invokeWithTimeout(
|
||||
request: req,
|
||||
timeoutMs: request.timeoutMs,
|
||||
onInvoke: onInvoke
|
||||
)
|
||||
self.logger.info("node invoke completed id=\(request.id, privacy: .public) ok=\(response.ok, privacy: .public)")
|
||||
await self.sendInvokeResult(request: request, response: response)
|
||||
} catch {
|
||||
self.logger.error("node invoke decode failed: \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
private func decodeInvokeRequest(from payload: OpenClawProtocol.AnyCodable) throws -> NodeInvokeRequestPayload {
|
||||
do {
|
||||
let data = try self.encoder.encode(payload)
|
||||
return try self.decoder.decode(NodeInvokeRequestPayload.self, from: data)
|
||||
} catch {
|
||||
if let raw = payload.value as? String, let data = raw.data(using: .utf8) {
|
||||
return try self.decoder.decode(NodeInvokeRequestPayload.self, from: data)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private func sendInvokeResult(request: NodeInvokeRequestPayload, response: BridgeInvokeResponse) async {
|
||||
guard let channel = self.channel else { return }
|
||||
self.logger.info("node invoke result sending id=\(request.id, privacy: .public) ok=\(response.ok, privacy: .public)")
|
||||
var params: [String: AnyCodable] = [
|
||||
"id": AnyCodable(request.id),
|
||||
"nodeId": AnyCodable(request.nodeId),
|
||||
@@ -226,7 +346,7 @@ public actor GatewayNodeSession {
|
||||
do {
|
||||
try await channel.send(method: "node.invoke.result", params: params)
|
||||
} catch {
|
||||
self.logger.error("node invoke result failed: \(error.localizedDescription, privacy: .public)")
|
||||
self.logger.error("node invoke result failed id=\(request.id, privacy: .public) error=\(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -589,20 +589,24 @@ public struct AgentIdentityResult: Codable, Sendable {
|
||||
public let agentid: String
|
||||
public let name: String?
|
||||
public let avatar: String?
|
||||
public let emoji: String?
|
||||
|
||||
public init(
|
||||
agentid: String,
|
||||
name: String?,
|
||||
avatar: String?
|
||||
avatar: String?,
|
||||
emoji: String?
|
||||
) {
|
||||
self.agentid = agentid
|
||||
self.name = name
|
||||
self.avatar = avatar
|
||||
self.emoji = emoji
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case agentid = "agentId"
|
||||
case name
|
||||
case avatar
|
||||
case emoji
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1115,6 +1119,35 @@ public struct SessionsCompactParams: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsUsageParams: Codable, Sendable {
|
||||
public let key: String?
|
||||
public let startdate: String?
|
||||
public let enddate: String?
|
||||
public let limit: Int?
|
||||
public let includecontextweight: Bool?
|
||||
|
||||
public init(
|
||||
key: String?,
|
||||
startdate: String?,
|
||||
enddate: String?,
|
||||
limit: Int?,
|
||||
includecontextweight: Bool?
|
||||
) {
|
||||
self.key = key
|
||||
self.startdate = startdate
|
||||
self.enddate = enddate
|
||||
self.limit = limit
|
||||
self.includecontextweight = includecontextweight
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case key
|
||||
case startdate = "startDate"
|
||||
case enddate = "endDate"
|
||||
case limit
|
||||
case includecontextweight = "includeContextWeight"
|
||||
}
|
||||
}
|
||||
|
||||
public struct ConfigGetParams: Codable, Sendable {
|
||||
}
|
||||
|
||||
@@ -1556,6 +1589,291 @@ public struct AgentSummary: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct AgentsCreateParams: Codable, Sendable {
|
||||
public let name: String
|
||||
public let workspace: String
|
||||
public let emoji: String?
|
||||
public let avatar: String?
|
||||
|
||||
public init(
|
||||
name: String,
|
||||
workspace: String,
|
||||
emoji: String?,
|
||||
avatar: String?
|
||||
) {
|
||||
self.name = name
|
||||
self.workspace = workspace
|
||||
self.emoji = emoji
|
||||
self.avatar = avatar
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case name
|
||||
case workspace
|
||||
case emoji
|
||||
case avatar
|
||||
}
|
||||
}
|
||||
|
||||
public struct AgentsCreateResult: Codable, Sendable {
|
||||
public let ok: Bool
|
||||
public let agentid: String
|
||||
public let name: String
|
||||
public let workspace: String
|
||||
|
||||
public init(
|
||||
ok: Bool,
|
||||
agentid: String,
|
||||
name: String,
|
||||
workspace: String
|
||||
) {
|
||||
self.ok = ok
|
||||
self.agentid = agentid
|
||||
self.name = name
|
||||
self.workspace = workspace
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case ok
|
||||
case agentid = "agentId"
|
||||
case name
|
||||
case workspace
|
||||
}
|
||||
}
|
||||
|
||||
public struct AgentsUpdateParams: Codable, Sendable {
|
||||
public let agentid: String
|
||||
public let name: String?
|
||||
public let workspace: String?
|
||||
public let model: String?
|
||||
public let avatar: String?
|
||||
|
||||
public init(
|
||||
agentid: String,
|
||||
name: String?,
|
||||
workspace: String?,
|
||||
model: String?,
|
||||
avatar: String?
|
||||
) {
|
||||
self.agentid = agentid
|
||||
self.name = name
|
||||
self.workspace = workspace
|
||||
self.model = model
|
||||
self.avatar = avatar
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case agentid = "agentId"
|
||||
case name
|
||||
case workspace
|
||||
case model
|
||||
case avatar
|
||||
}
|
||||
}
|
||||
|
||||
public struct AgentsUpdateResult: Codable, Sendable {
|
||||
public let ok: Bool
|
||||
public let agentid: String
|
||||
|
||||
public init(
|
||||
ok: Bool,
|
||||
agentid: String
|
||||
) {
|
||||
self.ok = ok
|
||||
self.agentid = agentid
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case ok
|
||||
case agentid = "agentId"
|
||||
}
|
||||
}
|
||||
|
||||
public struct AgentsDeleteParams: Codable, Sendable {
|
||||
public let agentid: String
|
||||
public let deletefiles: Bool?
|
||||
|
||||
public init(
|
||||
agentid: String,
|
||||
deletefiles: Bool?
|
||||
) {
|
||||
self.agentid = agentid
|
||||
self.deletefiles = deletefiles
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case agentid = "agentId"
|
||||
case deletefiles = "deleteFiles"
|
||||
}
|
||||
}
|
||||
|
||||
public struct AgentsDeleteResult: Codable, Sendable {
|
||||
public let ok: Bool
|
||||
public let agentid: String
|
||||
public let removedbindings: Int
|
||||
|
||||
public init(
|
||||
ok: Bool,
|
||||
agentid: String,
|
||||
removedbindings: Int
|
||||
) {
|
||||
self.ok = ok
|
||||
self.agentid = agentid
|
||||
self.removedbindings = removedbindings
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case ok
|
||||
case agentid = "agentId"
|
||||
case removedbindings = "removedBindings"
|
||||
}
|
||||
}
|
||||
|
||||
public struct AgentsFileEntry: Codable, Sendable {
|
||||
public let name: String
|
||||
public let path: String
|
||||
public let missing: Bool
|
||||
public let size: Int?
|
||||
public let updatedatms: Int?
|
||||
public let content: String?
|
||||
|
||||
public init(
|
||||
name: String,
|
||||
path: String,
|
||||
missing: Bool,
|
||||
size: Int?,
|
||||
updatedatms: Int?,
|
||||
content: String?
|
||||
) {
|
||||
self.name = name
|
||||
self.path = path
|
||||
self.missing = missing
|
||||
self.size = size
|
||||
self.updatedatms = updatedatms
|
||||
self.content = content
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case name
|
||||
case path
|
||||
case missing
|
||||
case size
|
||||
case updatedatms = "updatedAtMs"
|
||||
case content
|
||||
}
|
||||
}
|
||||
|
||||
public struct AgentsFilesListParams: Codable, Sendable {
|
||||
public let agentid: String
|
||||
|
||||
public init(
|
||||
agentid: String
|
||||
) {
|
||||
self.agentid = agentid
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case agentid = "agentId"
|
||||
}
|
||||
}
|
||||
|
||||
public struct AgentsFilesListResult: Codable, Sendable {
|
||||
public let agentid: String
|
||||
public let workspace: String
|
||||
public let files: [AgentsFileEntry]
|
||||
|
||||
public init(
|
||||
agentid: String,
|
||||
workspace: String,
|
||||
files: [AgentsFileEntry]
|
||||
) {
|
||||
self.agentid = agentid
|
||||
self.workspace = workspace
|
||||
self.files = files
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case agentid = "agentId"
|
||||
case workspace
|
||||
case files
|
||||
}
|
||||
}
|
||||
|
||||
public struct AgentsFilesGetParams: Codable, Sendable {
|
||||
public let agentid: String
|
||||
public let name: String
|
||||
|
||||
public init(
|
||||
agentid: String,
|
||||
name: String
|
||||
) {
|
||||
self.agentid = agentid
|
||||
self.name = name
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case agentid = "agentId"
|
||||
case name
|
||||
}
|
||||
}
|
||||
|
||||
public struct AgentsFilesGetResult: Codable, Sendable {
|
||||
public let agentid: String
|
||||
public let workspace: String
|
||||
public let file: AgentsFileEntry
|
||||
|
||||
public init(
|
||||
agentid: String,
|
||||
workspace: String,
|
||||
file: AgentsFileEntry
|
||||
) {
|
||||
self.agentid = agentid
|
||||
self.workspace = workspace
|
||||
self.file = file
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case agentid = "agentId"
|
||||
case workspace
|
||||
case file
|
||||
}
|
||||
}
|
||||
|
||||
public struct AgentsFilesSetParams: Codable, Sendable {
|
||||
public let agentid: String
|
||||
public let name: String
|
||||
public let content: String
|
||||
|
||||
public init(
|
||||
agentid: String,
|
||||
name: String,
|
||||
content: String
|
||||
) {
|
||||
self.agentid = agentid
|
||||
self.name = name
|
||||
self.content = content
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case agentid = "agentId"
|
||||
case name
|
||||
case content
|
||||
}
|
||||
}
|
||||
|
||||
public struct AgentsFilesSetResult: Codable, Sendable {
|
||||
public let ok: Bool
|
||||
public let agentid: String
|
||||
public let workspace: String
|
||||
public let file: AgentsFileEntry
|
||||
|
||||
public init(
|
||||
ok: Bool,
|
||||
agentid: String,
|
||||
workspace: String,
|
||||
file: AgentsFileEntry
|
||||
) {
|
||||
self.ok = ok
|
||||
self.agentid = agentid
|
||||
self.workspace = workspace
|
||||
self.file = file
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case ok
|
||||
case agentid = "agentId"
|
||||
case workspace
|
||||
case file
|
||||
}
|
||||
}
|
||||
|
||||
public struct AgentsListParams: Codable, Sendable {
|
||||
}
|
||||
|
||||
@@ -1630,6 +1948,16 @@ public struct ModelsListResult: Codable, Sendable {
|
||||
}
|
||||
|
||||
public struct SkillsStatusParams: Codable, Sendable {
|
||||
public let agentid: String?
|
||||
|
||||
public init(
|
||||
agentid: String?
|
||||
) {
|
||||
self.agentid = agentid
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case agentid = "agentId"
|
||||
}
|
||||
}
|
||||
|
||||
public struct SkillsBinsParams: Codable, Sendable {
|
||||
@@ -1707,7 +2035,7 @@ public struct CronJob: Codable, Sendable {
|
||||
public let sessiontarget: AnyCodable
|
||||
public let wakemode: AnyCodable
|
||||
public let payload: AnyCodable
|
||||
public let isolation: [String: AnyCodable]?
|
||||
public let delivery: [String: AnyCodable]?
|
||||
public let state: [String: AnyCodable]
|
||||
|
||||
public init(
|
||||
@@ -1723,7 +2051,7 @@ public struct CronJob: Codable, Sendable {
|
||||
sessiontarget: AnyCodable,
|
||||
wakemode: AnyCodable,
|
||||
payload: AnyCodable,
|
||||
isolation: [String: AnyCodable]?,
|
||||
delivery: [String: AnyCodable]?,
|
||||
state: [String: AnyCodable]
|
||||
) {
|
||||
self.id = id
|
||||
@@ -1738,7 +2066,7 @@ public struct CronJob: Codable, Sendable {
|
||||
self.sessiontarget = sessiontarget
|
||||
self.wakemode = wakemode
|
||||
self.payload = payload
|
||||
self.isolation = isolation
|
||||
self.delivery = delivery
|
||||
self.state = state
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
@@ -1754,7 +2082,7 @@ public struct CronJob: Codable, Sendable {
|
||||
case sessiontarget = "sessionTarget"
|
||||
case wakemode = "wakeMode"
|
||||
case payload
|
||||
case isolation
|
||||
case delivery
|
||||
case state
|
||||
}
|
||||
}
|
||||
@@ -1785,7 +2113,7 @@ public struct CronAddParams: Codable, Sendable {
|
||||
public let sessiontarget: AnyCodable
|
||||
public let wakemode: AnyCodable
|
||||
public let payload: AnyCodable
|
||||
public let isolation: [String: AnyCodable]?
|
||||
public let delivery: [String: AnyCodable]?
|
||||
|
||||
public init(
|
||||
name: String,
|
||||
@@ -1797,7 +2125,7 @@ public struct CronAddParams: Codable, Sendable {
|
||||
sessiontarget: AnyCodable,
|
||||
wakemode: AnyCodable,
|
||||
payload: AnyCodable,
|
||||
isolation: [String: AnyCodable]?
|
||||
delivery: [String: AnyCodable]?
|
||||
) {
|
||||
self.name = name
|
||||
self.agentid = agentid
|
||||
@@ -1808,7 +2136,7 @@ public struct CronAddParams: Codable, Sendable {
|
||||
self.sessiontarget = sessiontarget
|
||||
self.wakemode = wakemode
|
||||
self.payload = payload
|
||||
self.isolation = isolation
|
||||
self.delivery = delivery
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case name
|
||||
@@ -1820,7 +2148,7 @@ public struct CronAddParams: Codable, Sendable {
|
||||
case sessiontarget = "sessionTarget"
|
||||
case wakemode = "wakeMode"
|
||||
case payload
|
||||
case isolation
|
||||
case delivery
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1831,6 +2159,8 @@ public struct CronRunLogEntry: Codable, Sendable {
|
||||
public let status: AnyCodable?
|
||||
public let error: String?
|
||||
public let summary: String?
|
||||
public let sessionid: String?
|
||||
public let sessionkey: String?
|
||||
public let runatms: Int?
|
||||
public let durationms: Int?
|
||||
public let nextrunatms: Int?
|
||||
@@ -1842,6 +2172,8 @@ public struct CronRunLogEntry: Codable, Sendable {
|
||||
status: AnyCodable?,
|
||||
error: String?,
|
||||
summary: String?,
|
||||
sessionid: String?,
|
||||
sessionkey: String?,
|
||||
runatms: Int?,
|
||||
durationms: Int?,
|
||||
nextrunatms: Int?
|
||||
@@ -1852,6 +2184,8 @@ public struct CronRunLogEntry: Codable, Sendable {
|
||||
self.status = status
|
||||
self.error = error
|
||||
self.summary = summary
|
||||
self.sessionid = sessionid
|
||||
self.sessionkey = sessionkey
|
||||
self.runatms = runatms
|
||||
self.durationms = durationms
|
||||
self.nextrunatms = nextrunatms
|
||||
@@ -1863,6 +2197,8 @@ public struct CronRunLogEntry: Codable, Sendable {
|
||||
case status
|
||||
case error
|
||||
case summary
|
||||
case sessionid = "sessionId"
|
||||
case sessionkey = "sessionKey"
|
||||
case runatms = "runAtMs"
|
||||
case durationms = "durationMs"
|
||||
case nextrunatms = "nextRunAtMs"
|
||||
|
||||
@@ -32,7 +32,6 @@ if (modalElement && Array.isArray(modalElement.styles)) {
|
||||
modalElement.styles = [...modalElement.styles, modalStyles];
|
||||
}
|
||||
|
||||
const empty = Object.freeze({});
|
||||
const emptyClasses = () => ({});
|
||||
const textHintStyles = () => ({ h1: {}, h2: {}, h3: {}, h4: {}, h5: {}, body: {}, caption: {} });
|
||||
|
||||
@@ -160,7 +159,7 @@ class OpenClawA2UIHost extends LitElement {
|
||||
};
|
||||
|
||||
#processor = v0_8.Data.createSignalA2uiMessageProcessor();
|
||||
#themeProvider = new ContextProvider(this, {
|
||||
themeProvider = new ContextProvider(this, {
|
||||
context: themeContext,
|
||||
initialValue: openclawTheme,
|
||||
});
|
||||
@@ -318,8 +317,8 @@ class OpenClawA2UIHost extends LitElement {
|
||||
|
||||
#handleActionStatus(evt) {
|
||||
const detail = evt?.detail ?? null;
|
||||
if (!detail || typeof detail.id !== "string") return;
|
||||
if (!this.pendingAction || this.pendingAction.id !== detail.id) return;
|
||||
if (!detail || typeof detail.id !== "string") {return;}
|
||||
if (!this.pendingAction || this.pendingAction.id !== detail.id) {return;}
|
||||
|
||||
if (detail.ok) {
|
||||
this.pendingAction = { ...this.pendingAction, phase: "sent", sentAt: Date.now() };
|
||||
@@ -362,7 +361,7 @@ class OpenClawA2UIHost extends LitElement {
|
||||
for (const item of ctxItems) {
|
||||
const key = item?.key;
|
||||
const value = item?.value ?? null;
|
||||
if (!key || !value) continue;
|
||||
if (!key || !value) {continue;}
|
||||
|
||||
if (typeof value.path === "string") {
|
||||
const resolved = sourceNode
|
||||
|
||||
@@ -24,7 +24,7 @@ services:
|
||||
"--bind",
|
||||
"${OPENCLAW_GATEWAY_BIND:-lan}",
|
||||
"--port",
|
||||
"${OPENCLAW_GATEWAY_PORT:-18789}"
|
||||
"18789",
|
||||
]
|
||||
|
||||
openclaw-cli:
|
||||
@@ -32,6 +32,7 @@ services:
|
||||
environment:
|
||||
HOME: /home/node
|
||||
TERM: xterm-256color
|
||||
OPENCLAW_GATEWAY_TOKEN: ${OPENCLAW_GATEWAY_TOKEN}
|
||||
BROWSER: echo
|
||||
CLAUDE_AI_SESSION_KEY: ${CLAUDE_AI_SESSION_KEY}
|
||||
CLAUDE_WEB_SESSION_KEY: ${CLAUDE_WEB_SESSION_KEY}
|
||||
|
||||
@@ -191,12 +191,12 @@ docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli onboard --no-install-d
|
||||
echo ""
|
||||
echo "==> Provider setup (optional)"
|
||||
echo "WhatsApp (QR):"
|
||||
echo " ${COMPOSE_HINT} run --rm openclaw-cli providers login"
|
||||
echo " ${COMPOSE_HINT} run --rm openclaw-cli channels login"
|
||||
echo "Telegram (bot token):"
|
||||
echo " ${COMPOSE_HINT} run --rm openclaw-cli providers add --provider telegram --token <token>"
|
||||
echo " ${COMPOSE_HINT} run --rm openclaw-cli channels add --channel telegram --token <token>"
|
||||
echo "Discord (bot token):"
|
||||
echo " ${COMPOSE_HINT} run --rm openclaw-cli providers add --provider discord --token <token>"
|
||||
echo "Docs: https://docs.openclaw.ai/providers"
|
||||
echo " ${COMPOSE_HINT} run --rm openclaw-cli channels add --channel discord --token <token>"
|
||||
echo "Docs: https://docs.openclaw.ai/channels"
|
||||
|
||||
echo ""
|
||||
echo "==> Starting gateway"
|
||||
|
||||
15
docs.acp.md
15
docs.acp.md
@@ -84,9 +84,12 @@ To target a specific Gateway or agent:
|
||||
"command": "openclaw",
|
||||
"args": [
|
||||
"acp",
|
||||
"--url", "wss://gateway-host:18789",
|
||||
"--token", "<token>",
|
||||
"--session", "agent:design:main"
|
||||
"--url",
|
||||
"wss://gateway-host:18789",
|
||||
"--token",
|
||||
"<token>",
|
||||
"--session",
|
||||
"agent:design:main"
|
||||
],
|
||||
"env": {}
|
||||
}
|
||||
@@ -112,7 +115,7 @@ By default each ACP session is mapped to a dedicated Gateway session key:
|
||||
|
||||
You can override or reuse sessions in two ways:
|
||||
|
||||
1) CLI defaults
|
||||
1. CLI defaults
|
||||
|
||||
```bash
|
||||
openclaw acp --session agent:main:main
|
||||
@@ -120,7 +123,7 @@ openclaw acp --session-label "support inbox"
|
||||
openclaw acp --reset-session
|
||||
```
|
||||
|
||||
2) ACP metadata per session
|
||||
2. ACP metadata per session
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -185,7 +188,7 @@ updates. Terminal Gateway states map to ACP `done` with stop reasons:
|
||||
## Testing
|
||||
|
||||
- Unit: `src/acp/session.test.ts` covers run id lifecycle.
|
||||
- Full gate: `pnpm lint && pnpm build && pnpm test && pnpm docs:build`.
|
||||
- Full gate: `pnpm build && pnpm check && pnpm test && pnpm docs:build`.
|
||||
|
||||
## Related Docs
|
||||
|
||||
|
||||
31
docs/.i18n/README.md
Normal file
31
docs/.i18n/README.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# OpenClaw docs i18n assets
|
||||
|
||||
This folder stores **generated** and **config** files for documentation translations.
|
||||
|
||||
## Files
|
||||
|
||||
- `glossary.<lang>.json` — preferred term mappings (used in prompt guidance).
|
||||
- `<lang>.tm.jsonl` — translation memory (cache) keyed by workflow + model + text hash.
|
||||
|
||||
## Glossary format
|
||||
|
||||
`glossary.<lang>.json` is an array of entries:
|
||||
|
||||
```json
|
||||
{
|
||||
"source": "troubleshooting",
|
||||
"target": "故障排除",
|
||||
"ignore_case": true,
|
||||
"whole_word": false
|
||||
}
|
||||
```
|
||||
|
||||
Fields:
|
||||
|
||||
- `source`: English (or source) phrase to prefer.
|
||||
- `target`: preferred translation output.
|
||||
|
||||
## Notes
|
||||
|
||||
- Glossary entries are passed to the model as **prompt guidance** (no deterministic rewrites).
|
||||
- The translation memory is updated by `scripts/docs-i18n`.
|
||||
210
docs/.i18n/glossary.zh-CN.json
Normal file
210
docs/.i18n/glossary.zh-CN.json
Normal file
@@ -0,0 +1,210 @@
|
||||
[
|
||||
{
|
||||
"source": "OpenClaw",
|
||||
"target": "OpenClaw"
|
||||
},
|
||||
{
|
||||
"source": "Gateway",
|
||||
"target": "Gateway 网关"
|
||||
},
|
||||
{
|
||||
"source": "Pi",
|
||||
"target": "Pi"
|
||||
},
|
||||
{
|
||||
"source": "Skills",
|
||||
"target": "Skills"
|
||||
},
|
||||
{
|
||||
"source": "Skills config",
|
||||
"target": "Skills 配置"
|
||||
},
|
||||
{
|
||||
"source": "Skills Config",
|
||||
"target": "Skills 配置"
|
||||
},
|
||||
{
|
||||
"source": "local loopback",
|
||||
"target": "local loopback"
|
||||
},
|
||||
{
|
||||
"source": "Tailscale",
|
||||
"target": "Tailscale"
|
||||
},
|
||||
{
|
||||
"source": "Getting Started",
|
||||
"target": "入门指南"
|
||||
},
|
||||
{
|
||||
"source": "Getting started",
|
||||
"target": "入门指南"
|
||||
},
|
||||
{
|
||||
"source": "Quick start",
|
||||
"target": "快速开始"
|
||||
},
|
||||
{
|
||||
"source": "Quick Start",
|
||||
"target": "快速开始"
|
||||
},
|
||||
{
|
||||
"source": "Docs directory",
|
||||
"target": "文档目录"
|
||||
},
|
||||
{
|
||||
"source": "Credits",
|
||||
"target": "致谢"
|
||||
},
|
||||
{
|
||||
"source": "Features",
|
||||
"target": "功能"
|
||||
},
|
||||
{
|
||||
"source": "DMs",
|
||||
"target": "私信"
|
||||
},
|
||||
{
|
||||
"source": "DM",
|
||||
"target": "私信"
|
||||
},
|
||||
{
|
||||
"source": "sandbox",
|
||||
"target": "沙箱"
|
||||
},
|
||||
{
|
||||
"source": "Sandbox",
|
||||
"target": "沙箱"
|
||||
},
|
||||
{
|
||||
"source": "sandboxing",
|
||||
"target": "沙箱隔离"
|
||||
},
|
||||
{
|
||||
"source": "Sandboxing",
|
||||
"target": "沙箱隔离"
|
||||
},
|
||||
{
|
||||
"source": "sandboxed",
|
||||
"target": "沙箱隔离"
|
||||
},
|
||||
{
|
||||
"source": "Sandboxed",
|
||||
"target": "沙箱隔离"
|
||||
},
|
||||
{
|
||||
"source": "Sandboxing note",
|
||||
"target": "沙箱注意事项"
|
||||
},
|
||||
{
|
||||
"source": "Companion apps",
|
||||
"target": "配套应用"
|
||||
},
|
||||
{
|
||||
"source": "expected keys",
|
||||
"target": "预期键名"
|
||||
},
|
||||
{
|
||||
"source": "block streaming",
|
||||
"target": "分块流式传输"
|
||||
},
|
||||
{
|
||||
"source": "Block streaming",
|
||||
"target": "分块流式传输"
|
||||
},
|
||||
{
|
||||
"source": "Discovery + transports",
|
||||
"target": "设备发现 + 传输协议"
|
||||
},
|
||||
{
|
||||
"source": "Discovery",
|
||||
"target": "设备发现"
|
||||
},
|
||||
{
|
||||
"source": "Network model",
|
||||
"target": "网络模型"
|
||||
},
|
||||
{
|
||||
"source": "for full details",
|
||||
"target": "了解详情"
|
||||
},
|
||||
{
|
||||
"source": "First 60 seconds",
|
||||
"target": "最初的六十秒"
|
||||
},
|
||||
{
|
||||
"source": "Auth: where it lives (important)",
|
||||
"target": "凭证:存储位置(重要)"
|
||||
},
|
||||
{
|
||||
"source": "agent",
|
||||
"target": "智能体"
|
||||
},
|
||||
{
|
||||
"source": "channel",
|
||||
"target": "渠道"
|
||||
},
|
||||
{
|
||||
"source": "session",
|
||||
"target": "会话"
|
||||
},
|
||||
{
|
||||
"source": "provider",
|
||||
"target": "提供商"
|
||||
},
|
||||
{
|
||||
"source": "model",
|
||||
"target": "模型"
|
||||
},
|
||||
{
|
||||
"source": "tool",
|
||||
"target": "工具"
|
||||
},
|
||||
{
|
||||
"source": "CLI",
|
||||
"target": "CLI"
|
||||
},
|
||||
{
|
||||
"source": "install sanity",
|
||||
"target": "安装完整性检查"
|
||||
},
|
||||
{
|
||||
"source": "get unstuck",
|
||||
"target": "解决问题"
|
||||
},
|
||||
{
|
||||
"source": "troubleshooting",
|
||||
"target": "故障排除"
|
||||
},
|
||||
{
|
||||
"source": "FAQ",
|
||||
"target": "常见问题"
|
||||
},
|
||||
{
|
||||
"source": "onboarding",
|
||||
"target": "新手引导"
|
||||
},
|
||||
{
|
||||
"source": "Onboarding",
|
||||
"target": "新手引导"
|
||||
},
|
||||
{
|
||||
"source": "wizard",
|
||||
"target": "向导"
|
||||
},
|
||||
{
|
||||
"source": "environment variables",
|
||||
"target": "环境变量"
|
||||
},
|
||||
{
|
||||
"source": "environment variable",
|
||||
"target": "环境变量"
|
||||
},
|
||||
{
|
||||
"source": "env vars",
|
||||
"target": "环境变量"
|
||||
},
|
||||
{
|
||||
"source": "env var",
|
||||
"target": "环境变量"
|
||||
}
|
||||
]
|
||||
1329
docs/.i18n/zh-CN.tm.jsonl
Normal file
1329
docs/.i18n/zh-CN.tm.jsonl
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,53 +0,0 @@
|
||||
title: "OpenClaw Docs"
|
||||
description: "A TypeScript/Node gateway + macOS/iOS/Android companions for WhatsApp (web) and Telegram (bot)."
|
||||
markdown: kramdown
|
||||
highlighter: rouge
|
||||
|
||||
# Keep GitHub Pages' default page URLs (e.g. /gateway.html). Many docs links
|
||||
# are written as relative *.md links and are rewritten during the build.
|
||||
|
||||
plugins:
|
||||
- jekyll-relative-links
|
||||
|
||||
relative_links:
|
||||
enabled: true
|
||||
collections: true
|
||||
|
||||
defaults:
|
||||
- scope:
|
||||
path: ""
|
||||
values:
|
||||
layout: default
|
||||
|
||||
nav:
|
||||
- title: "Home"
|
||||
url: "/"
|
||||
- title: "OpenClaw Assistant"
|
||||
url: "/start/openclaw/"
|
||||
- title: "Gateway"
|
||||
url: "/gateway/"
|
||||
- title: "Remote"
|
||||
url: "/gateway/remote/"
|
||||
- title: "Discovery"
|
||||
url: "/gateway/discovery/"
|
||||
- title: "Configuration"
|
||||
url: "/gateway/configuration/"
|
||||
- title: "WebChat"
|
||||
url: "/web/webchat/"
|
||||
- title: "macOS App"
|
||||
url: "/platforms/macos/"
|
||||
- title: "iOS App"
|
||||
url: "/platforms/ios/"
|
||||
- title: "Android App"
|
||||
url: "/platforms/android/"
|
||||
- title: "Telegram"
|
||||
url: "/channels/telegram/"
|
||||
- title: "Security"
|
||||
url: "/gateway/security/"
|
||||
- title: "Troubleshooting"
|
||||
url: "/gateway/troubleshooting/"
|
||||
|
||||
kramdown:
|
||||
input: GFM
|
||||
hard_wrap: false
|
||||
syntax_highlighter: rouge
|
||||
@@ -1,145 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-theme="auto">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<title>
|
||||
{% if page.url == "/" %}{{ site.title }}{% else %}{{ page.title | default: page.path | split: "/" | last | replace: ".md", "" }} · {{ site.title }}{% endif %}
|
||||
</title>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Pixelify+Sans:wght@400..700&family=Fragment+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script>
|
||||
(() => {
|
||||
try {
|
||||
const stored = localStorage.getItem("openclaw:theme");
|
||||
if (stored === "light" || stored === "dark") document.documentElement.dataset.theme = stored;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="{{ "/assets/terminal.css" | relative_url }}" />
|
||||
<link rel="stylesheet" href="{{ "/assets/markdown.css" | relative_url }}" />
|
||||
<script defer src="{{ "/assets/theme.js" | relative_url }}"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<a class="skip-link" href="#content">Skip to content</a>
|
||||
|
||||
<header class="shell">
|
||||
<div class="shell__frame" role="banner">
|
||||
<div class="shell__titlebar">
|
||||
<div class="brand" aria-label="OpenClaw docs terminal">
|
||||
<img
|
||||
class="brand__logo"
|
||||
src="{{ "/assets/pixel-lobster.svg" | relative_url }}"
|
||||
alt=""
|
||||
width="40"
|
||||
height="40"
|
||||
decoding="async"
|
||||
/>
|
||||
<div class="brand__text">
|
||||
<div class="brand__name">OpenClaw</div>
|
||||
<div class="brand__hint">docs // lobster terminal</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="titlebar__actions">
|
||||
<a class="titlebar__cta" href="https://github.com/openclaw/openclaw">
|
||||
<span class="titlebar__cta-label">GitHub</span>
|
||||
<span class="titlebar__cta-meta">repo</span>
|
||||
</a>
|
||||
<a class="titlebar__cta titlebar__cta--accent" href="https://github.com/openclaw/openclaw/releases/latest">
|
||||
<span class="titlebar__cta-label">Download</span>
|
||||
<span class="titlebar__cta-meta">latest</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shell__nav" aria-label="Primary">
|
||||
<nav class="nav">
|
||||
{% assign nav = site.nav | default: empty %}
|
||||
{% for item in nav %}
|
||||
{% assign item_url = item.url | relative_url %}
|
||||
<a class="nav__link" href="{{ item_url }}">
|
||||
<span class="nav__chev">›</span>{{ item.title }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
|
||||
<div class="shell__status" aria-hidden="true">
|
||||
<span class="status__dot"></span>
|
||||
<span class="status__text">
|
||||
{% if page.url == "/" %}
|
||||
ready
|
||||
{% else %}
|
||||
viewing: {{ page.path }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main id="content" class="content" role="main">
|
||||
<div class="terminal">
|
||||
<div class="terminal__prompt" aria-hidden="true">
|
||||
<span class="prompt__user">openclaw</span>@<span class="prompt__host">openclaw</span>:<span class="prompt__path">~/docs</span>$<span class="prompt__cmd">
|
||||
{% if page.url == "/" %}cat index.md{% else %}less {{ page.path }}{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% if page.summary %}
|
||||
<p class="terminal__summary">{{ page.summary }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if page.read_when %}
|
||||
<details class="terminal__meta">
|
||||
<summary>Read when…</summary>
|
||||
<ul>
|
||||
{% for hint in page.read_when %}
|
||||
<li>{{ hint }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</details>
|
||||
{% endif %}
|
||||
|
||||
<article class="markdown">
|
||||
{{ content }}
|
||||
</article>
|
||||
|
||||
<footer class="terminal__footer" role="contentinfo">
|
||||
<div class="footer__line">
|
||||
<span class="footer__sig">openclaw.ai</span>
|
||||
<span class="footer__sep">·</span>
|
||||
<a href="https://github.com/openclaw/openclaw">source</a>
|
||||
<span class="footer__sep">·</span>
|
||||
<a href="https://github.com/openclaw/openclaw/releases">releases</a>
|
||||
</div>
|
||||
<div class="footer__hint" aria-hidden="true">
|
||||
tip: press <kbd>F2</kbd> (Mac: <kbd>fn</kbd>+<kbd>F2</kbd>) to flip
|
||||
the universe
|
||||
</div>
|
||||
<div class="footer__actions">
|
||||
<button
|
||||
class="theme-toggle"
|
||||
type="button"
|
||||
data-theme-toggle
|
||||
aria-label="Toggle theme (F2; on Mac: fn+F2)"
|
||||
aria-pressed="false"
|
||||
>
|
||||
<span class="theme-toggle__key">F2</span>
|
||||
<span class="theme-toggle__label" data-theme-label>theme</span>
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
BIN
docs/assets/macos-onboarding/01-macos-warning.jpeg
Normal file
BIN
docs/assets/macos-onboarding/01-macos-warning.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 155 KiB |
BIN
docs/assets/macos-onboarding/02-local-networks.jpeg
Normal file
BIN
docs/assets/macos-onboarding/02-local-networks.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 162 KiB |
BIN
docs/assets/macos-onboarding/03-security-notice.png
Normal file
BIN
docs/assets/macos-onboarding/03-security-notice.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 312 KiB |
BIN
docs/assets/macos-onboarding/04-choose-gateway.png
Normal file
BIN
docs/assets/macos-onboarding/04-choose-gateway.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 312 KiB |
BIN
docs/assets/macos-onboarding/05-permissions.png
Normal file
BIN
docs/assets/macos-onboarding/05-permissions.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 362 KiB |
@@ -1,179 +0,0 @@
|
||||
.markdown {
|
||||
margin-top: 18px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.mdx-content > h1:first-of-type {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.markdown h1,
|
||||
.markdown h2,
|
||||
.markdown h3,
|
||||
.markdown h4 {
|
||||
font-family: var(--font-pixel);
|
||||
letter-spacing: 0.04em;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.markdown h1 {
|
||||
font-size: clamp(28px, 4vw, 44px);
|
||||
margin: 26px 0 10px;
|
||||
}
|
||||
|
||||
.markdown h2 {
|
||||
font-size: 22px;
|
||||
margin: 26px 0 10px;
|
||||
}
|
||||
|
||||
.markdown h3 {
|
||||
font-size: 18px;
|
||||
margin: 22px 0 8px;
|
||||
}
|
||||
|
||||
.markdown p {
|
||||
margin: 0 0 14px;
|
||||
}
|
||||
|
||||
.markdown a {
|
||||
color: var(--link);
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dotted color-mix(in oklab, var(--link) 65%, transparent);
|
||||
}
|
||||
|
||||
.markdown a:hover {
|
||||
color: var(--link2);
|
||||
border-bottom-color: color-mix(in oklab, var(--link2) 75%, transparent);
|
||||
}
|
||||
|
||||
.markdown hr {
|
||||
border: 0;
|
||||
height: 1px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
color-mix(in oklab, var(--frame-border) 30%, transparent),
|
||||
transparent
|
||||
);
|
||||
margin: 26px 0;
|
||||
}
|
||||
|
||||
.markdown blockquote {
|
||||
margin: 18px 0;
|
||||
padding: 14px 14px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: color-mix(in oklab, var(--panel) 70%, transparent);
|
||||
border-left: 6px solid color-mix(in oklab, var(--accent) 60%, transparent);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.markdown ul,
|
||||
.markdown ol {
|
||||
margin: 0 0 14px 22px;
|
||||
}
|
||||
|
||||
.markdown li {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.markdown img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 12px;
|
||||
border: 1px solid color-mix(in oklab, var(--frame-border) 20%, transparent);
|
||||
box-shadow: 0 12px 0 -8px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.showcase-link {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.showcase-preview {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 100%;
|
||||
width: min(420px, 80vw);
|
||||
padding: 8px;
|
||||
border-radius: 14px;
|
||||
background: color-mix(in oklab, var(--panel) 92%, transparent);
|
||||
border: 1px solid color-mix(in oklab, var(--frame-border) 30%, transparent);
|
||||
box-shadow: 0 18px 40px -18px rgba(0, 0, 0, 0.55);
|
||||
transform: translate(-50%, 10px) scale(0.98);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
z-index: 20;
|
||||
transition: opacity 0.18s ease, transform 0.18s ease, visibility 0.18s ease;
|
||||
}
|
||||
|
||||
.showcase-preview img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 10px;
|
||||
border: 1px solid color-mix(in oklab, var(--frame-border) 25%, transparent);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.showcase-link:hover .showcase-preview,
|
||||
.showcase-link:focus-within .showcase-preview {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translate(-50%, 6px) scale(1);
|
||||
}
|
||||
|
||||
@media (hover: none) {
|
||||
.showcase-preview {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown code {
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.95em;
|
||||
padding: 0.15em 0.35em;
|
||||
border-radius: 8px;
|
||||
background: color-mix(in oklab, var(--panel) 70%, var(--code-bg));
|
||||
border: 1px solid color-mix(in oklab, var(--frame-border) 18%, transparent);
|
||||
}
|
||||
|
||||
.markdown pre {
|
||||
background: var(--code-bg);
|
||||
color: var(--code-fg);
|
||||
padding: 14px 14px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid color-mix(in oklab, var(--frame-border) 18%, transparent);
|
||||
overflow-x: auto;
|
||||
box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--code-accent) 12%, transparent);
|
||||
}
|
||||
|
||||
.markdown pre code {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.markdown table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 16px 0 22px;
|
||||
border: 1px solid color-mix(in oklab, var(--frame-border) 20%, transparent);
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.markdown th,
|
||||
.markdown td {
|
||||
padding: 10px 10px;
|
||||
border-bottom: 1px solid color-mix(in oklab, var(--frame-border) 15%, transparent);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.markdown th {
|
||||
background: color-mix(in oklab, var(--panel2) 85%, transparent);
|
||||
font-family: var(--font-pixel);
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
@@ -1,473 +0,0 @@
|
||||
:root {
|
||||
--font-body: "Fragment Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--font-pixel: "Pixelify Sans", system-ui, sans-serif;
|
||||
--radius: 14px;
|
||||
--radius-sm: 10px;
|
||||
--border: 2px;
|
||||
--shadow-px: 0 0 0 var(--border) var(--frame-border), 0 12px 0 -4px rgba(0, 0, 0, 0.25);
|
||||
--scanline-size: 6px;
|
||||
--scanline-opacity: 0.08;
|
||||
}
|
||||
|
||||
html[data-theme="light"],
|
||||
html[data-theme="auto"] {
|
||||
--bg0: #fbf4e7;
|
||||
--bg1: #fffaf0;
|
||||
--panel: #fffdf8;
|
||||
--panel2: #fff6e5;
|
||||
--text: #10221c;
|
||||
--muted: #3e5a50;
|
||||
--faint: #6b7f77;
|
||||
--link: #0f6b4c;
|
||||
--link2: #ff4f40;
|
||||
--accent: #ff4f40;
|
||||
--accent2: #00b88a;
|
||||
--frame-border: #1b2e27;
|
||||
--code-bg: #0b1713;
|
||||
--code-fg: #eafff6;
|
||||
--code-accent: #67ff9b;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] {
|
||||
--bg0: #0b1a22;
|
||||
--bg1: #0a1720;
|
||||
--panel: #0e231f;
|
||||
--panel2: #102a24;
|
||||
--text: #c9eadc;
|
||||
--muted: #8ab8aa;
|
||||
--faint: #699b8d;
|
||||
--link: #6fe8c7;
|
||||
--link2: #ff7b63;
|
||||
--accent: #ff4f40;
|
||||
--accent2: #5fdfa2;
|
||||
--frame-border: #6fbfa8;
|
||||
--code-bg: #091814;
|
||||
--code-fg: #d7f5e8;
|
||||
--code-accent: #5fdfa2;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html[data-theme="auto"] {
|
||||
--bg0: #0b1a22;
|
||||
--bg1: #0a1720;
|
||||
--panel: #0e231f;
|
||||
--panel2: #102a24;
|
||||
--text: #c9eadc;
|
||||
--muted: #8ab8aa;
|
||||
--faint: #699b8d;
|
||||
--link: #6fe8c7;
|
||||
--link2: #ff7b63;
|
||||
--accent: #ff4f40;
|
||||
--accent2: #5fdfa2;
|
||||
--frame-border: #6fbfa8;
|
||||
--code-bg: #091814;
|
||||
--code-fg: #d7f5e8;
|
||||
--code-accent: #5fdfa2;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--font-body);
|
||||
color: var(--text);
|
||||
background:
|
||||
radial-gradient(1100px 700px at 20% -10%, color-mix(in oklab, var(--accent) 18%, transparent), transparent 55%),
|
||||
radial-gradient(900px 600px at 95% 10%, color-mix(in oklab, var(--accent2) 14%, transparent), transparent 60%),
|
||||
radial-gradient(900px 600px at 50% 120%, color-mix(in oklab, var(--link) 10%, transparent), transparent 55%),
|
||||
linear-gradient(180deg, var(--bg0), var(--bg1));
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body::before,
|
||||
body::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
z-index: 10;
|
||||
padding: 10px 12px;
|
||||
border-radius: 999px;
|
||||
background: var(--panel);
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
transform: translateY(-130%);
|
||||
box-shadow: var(--shadow-px);
|
||||
}
|
||||
|
||||
.skip-link:focus {
|
||||
transform: translateY(0);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.shell {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
padding: 22px 16px 10px;
|
||||
}
|
||||
|
||||
.shell__frame {
|
||||
max-width: 1120px;
|
||||
margin: 0 auto;
|
||||
border-radius: var(--radius);
|
||||
background:
|
||||
linear-gradient(180deg, color-mix(in oklab, var(--panel2) 88%, transparent), color-mix(in oklab, var(--panel) 92%, transparent));
|
||||
box-shadow: var(--shadow-px);
|
||||
border: var(--border) solid var(--frame-border);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.shell__titlebar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 14px 14px 12px;
|
||||
background:
|
||||
linear-gradient(90deg, color-mix(in oklab, var(--accent) 26%, transparent), transparent 42%),
|
||||
linear-gradient(180deg, color-mix(in oklab, var(--panel) 90%, #000 10%), color-mix(in oklab, var(--panel2) 90%, #000 10%));
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.brand__logo {
|
||||
flex: 0 0 auto;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
image-rendering: pixelated;
|
||||
filter: drop-shadow(0 2px 0 color-mix(in oklab, var(--frame-border) 80%, transparent));
|
||||
}
|
||||
|
||||
.brand__text {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.brand__name {
|
||||
font-family: var(--font-pixel);
|
||||
letter-spacing: 0.14em;
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
line-height: 1.1;
|
||||
text-shadow: 0 1px 0 color-mix(in oklab, var(--frame-border) 55%, transparent);
|
||||
}
|
||||
|
||||
.brand__hint {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.titlebar__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.titlebar__cta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px 8px 10px;
|
||||
border-radius: 12px;
|
||||
background:
|
||||
linear-gradient(140deg, color-mix(in oklab, var(--accent) 10%, transparent), transparent 60%),
|
||||
color-mix(in oklab, var(--panel) 92%, transparent);
|
||||
border: var(--border) solid color-mix(in oklab, var(--frame-border) 80%, transparent);
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
box-shadow:
|
||||
0 6px 0 -3px rgba(0, 0, 0, 0.25),
|
||||
inset 0 0 0 1px color-mix(in oklab, var(--panel2) 55%, transparent);
|
||||
}
|
||||
|
||||
.titlebar__cta:hover {
|
||||
border-color: color-mix(in oklab, var(--accent2) 45%, transparent);
|
||||
box-shadow:
|
||||
0 0 0 2px color-mix(in oklab, var(--accent2) 30%, transparent),
|
||||
0 6px 0 -3px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.titlebar__cta:active {
|
||||
transform: translateY(1px);
|
||||
box-shadow: 0 4px 0 -3px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.titlebar__cta:focus-visible {
|
||||
outline: 3px solid color-mix(in oklab, var(--accent2) 60%, transparent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.titlebar__cta--accent {
|
||||
background:
|
||||
linear-gradient(120deg, color-mix(in oklab, var(--accent) 22%, transparent), transparent 70%),
|
||||
color-mix(in oklab, var(--panel) 88%, transparent);
|
||||
border-color: color-mix(in oklab, var(--accent) 60%, var(--frame-border));
|
||||
}
|
||||
|
||||
.titlebar__cta-label {
|
||||
font-family: var(--font-pixel);
|
||||
letter-spacing: 0.12em;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.titlebar__cta-meta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 20px;
|
||||
padding: 0 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
background: var(--code-bg);
|
||||
color: var(--code-accent);
|
||||
border: 1px solid color-mix(in oklab, var(--code-accent) 30%, transparent);
|
||||
box-shadow: inset 0 0 12px color-mix(in oklab, var(--code-accent) 25%, transparent);
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 9px 10px;
|
||||
border-radius: 12px;
|
||||
background: color-mix(in oklab, var(--panel) 92%, transparent);
|
||||
border: var(--border) solid var(--frame-border);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
box-shadow: 0 6px 0 -3px rgba(0, 0, 0, 0.25);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.theme-toggle:active {
|
||||
transform: translateY(1px);
|
||||
box-shadow: 0 4px 0 -3px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.theme-toggle__key {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 28px;
|
||||
border-radius: 9px;
|
||||
background: var(--code-bg);
|
||||
color: var(--code-accent);
|
||||
border: 1px solid color-mix(in oklab, var(--code-accent) 30%, transparent);
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.06em;
|
||||
text-shadow: 0 0 14px color-mix(in oklab, var(--code-accent) 55%, transparent);
|
||||
}
|
||||
|
||||
.theme-toggle__label {
|
||||
font-family: var(--font-pixel);
|
||||
letter-spacing: 0.12em;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.theme-toggle:focus-visible {
|
||||
outline: 3px solid color-mix(in oklab, var(--accent2) 60%, transparent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.shell__nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 10px 14px 12px;
|
||||
border-top: 1px solid color-mix(in oklab, var(--frame-border) 25%, transparent);
|
||||
background: linear-gradient(180deg, transparent, color-mix(in oklab, var(--panel2) 78%, transparent));
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.nav__link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
text-decoration: none;
|
||||
color: var(--text);
|
||||
background: color-mix(in oklab, var(--panel) 85%, transparent);
|
||||
border: 1px solid color-mix(in oklab, var(--frame-border) 20%, transparent);
|
||||
}
|
||||
|
||||
.nav__link:hover {
|
||||
border-color: color-mix(in oklab, var(--accent2) 45%, transparent);
|
||||
box-shadow: 0 0 0 2px color-mix(in oklab, var(--accent2) 30%, transparent);
|
||||
}
|
||||
|
||||
.nav__chev {
|
||||
color: var(--accent);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.shell__status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status__dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
background: radial-gradient(circle at 30% 30%, var(--accent2), color-mix(in oklab, var(--accent2) 30%, #000));
|
||||
box-shadow: 0 0 0 2px color-mix(in oklab, var(--accent2) 18%, transparent), 0 0 18px color-mix(in oklab, var(--accent2) 50%, transparent);
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 18px 16px 48px;
|
||||
}
|
||||
|
||||
.terminal {
|
||||
position: relative;
|
||||
max-width: 1120px;
|
||||
margin: 0 auto;
|
||||
border-radius: var(--radius);
|
||||
border: var(--border) solid var(--frame-border);
|
||||
background: linear-gradient(180deg, color-mix(in oklab, var(--panel) 92%, transparent), color-mix(in oklab, var(--panel2) 86%, transparent));
|
||||
box-shadow: var(--shadow-px);
|
||||
padding: 18px 16px 16px;
|
||||
}
|
||||
|
||||
.terminal__prompt {
|
||||
display: block;
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--code-bg);
|
||||
color: var(--code-fg);
|
||||
border: 1px solid color-mix(in oklab, var(--frame-border) 18%, transparent);
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.prompt__user {
|
||||
color: var(--code-accent);
|
||||
text-shadow: 0 0 16px color-mix(in oklab, var(--code-accent) 52%, transparent);
|
||||
}
|
||||
|
||||
.prompt__host {
|
||||
color: color-mix(in oklab, var(--code-fg) 92%, var(--accent2));
|
||||
}
|
||||
|
||||
.prompt__path {
|
||||
color: color-mix(in oklab, var(--code-fg) 78%, var(--link));
|
||||
}
|
||||
|
||||
.prompt__cmd {
|
||||
margin-left: 8px;
|
||||
color: color-mix(in oklab, var(--code-fg) 90%, var(--accent));
|
||||
}
|
||||
|
||||
.terminal__summary {
|
||||
margin: 12px 2px 0;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.terminal__meta {
|
||||
margin: 12px 2px 0;
|
||||
padding: 12px 12px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px dashed color-mix(in oklab, var(--frame-border) 25%, transparent);
|
||||
background: color-mix(in oklab, var(--panel) 76%, transparent);
|
||||
color: var(--faint);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.terminal__meta summary {
|
||||
cursor: pointer;
|
||||
font-family: var(--font-pixel);
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.terminal__meta ul {
|
||||
margin: 10px 0 0 18px;
|
||||
}
|
||||
|
||||
.terminal__footer {
|
||||
margin-top: 22px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid color-mix(in oklab, var(--frame-border) 20%, transparent);
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.footer__actions {
|
||||
margin-top: 14px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.terminal__footer a {
|
||||
color: var(--link);
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dotted color-mix(in oklab, var(--link) 55%, transparent);
|
||||
}
|
||||
|
||||
.terminal__footer a:hover {
|
||||
color: var(--link2);
|
||||
border-bottom-color: color-mix(in oklab, var(--link2) 75%, transparent);
|
||||
}
|
||||
|
||||
.footer__hint {
|
||||
margin-top: 8px;
|
||||
color: var(--faint);
|
||||
}
|
||||
|
||||
kbd {
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.9em;
|
||||
padding: 2px 6px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid color-mix(in oklab, var(--frame-border) 18%, transparent);
|
||||
background: color-mix(in oklab, var(--panel) 65%, transparent);
|
||||
box-shadow: 0 6px 0 -4px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
@supports not (color: color-mix(in oklab, black, white)) {
|
||||
body::before,
|
||||
body::after {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
const THEME_STORAGE_KEY = "openclaw:theme";
|
||||
|
||||
function safeGet(key) {
|
||||
try {
|
||||
return localStorage.getItem(key);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function safeSet(key, value) {
|
||||
try {
|
||||
localStorage.setItem(key, value);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
function preferredTheme() {
|
||||
const stored = safeGet(THEME_STORAGE_KEY);
|
||||
if (stored === "light" || stored === "dark") return stored;
|
||||
return window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||
}
|
||||
|
||||
function applyTheme(theme) {
|
||||
document.documentElement.dataset.theme = theme;
|
||||
|
||||
const toggle = document.querySelector("[data-theme-toggle]");
|
||||
const label = document.querySelector("[data-theme-label]");
|
||||
|
||||
if (toggle instanceof HTMLButtonElement) toggle.setAttribute("aria-pressed", theme === "dark" ? "true" : "false");
|
||||
if (label) label.textContent = theme === "dark" ? "dark" : "light";
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
const current = document.documentElement.dataset.theme === "dark" ? "dark" : "light";
|
||||
const next = current === "dark" ? "light" : "dark";
|
||||
safeSet(THEME_STORAGE_KEY, next);
|
||||
applyTheme(next);
|
||||
}
|
||||
|
||||
applyTheme(preferredTheme());
|
||||
|
||||
document.addEventListener("click", (event) => {
|
||||
const target = event.target;
|
||||
const button = target instanceof Element ? target.closest("[data-theme-toggle]") : null;
|
||||
if (button) toggleTheme();
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "F2") {
|
||||
event.preventDefault();
|
||||
toggleTheme();
|
||||
}
|
||||
});
|
||||
@@ -3,7 +3,9 @@ summary: "Monitor OAuth expiry for model providers"
|
||||
read_when:
|
||||
- Setting up auth expiry monitoring or alerts
|
||||
- Automating Claude Code / Codex OAuth refresh checks
|
||||
title: "Auth Monitoring"
|
||||
---
|
||||
|
||||
# Auth monitoring
|
||||
|
||||
OpenClaw exposes OAuth expiry health via `openclaw models status`. Use that for
|
||||
@@ -16,6 +18,7 @@ openclaw models status --check
|
||||
```
|
||||
|
||||
Exit codes:
|
||||
|
||||
- `0`: OK
|
||||
- `1`: expired or missing credentials
|
||||
- `2`: expiring soon (within 24h)
|
||||
|
||||
@@ -4,7 +4,9 @@ read_when:
|
||||
- Scheduling background jobs or wakeups
|
||||
- Wiring automation that should run with or alongside heartbeats
|
||||
- Deciding between heartbeat and cron for scheduled tasks
|
||||
title: "Cron Jobs"
|
||||
---
|
||||
|
||||
# Cron jobs (Gateway scheduler)
|
||||
|
||||
> **Cron vs Heartbeat?** See [Cron vs Heartbeat](/automation/cron-vs-heartbeat) for guidance on when to use each.
|
||||
@@ -12,52 +14,104 @@ read_when:
|
||||
Cron is the Gateway’s built-in scheduler. It persists jobs, wakes the agent at
|
||||
the right time, and can optionally deliver output back to a chat.
|
||||
|
||||
If you want *“run this every morning”* or *“poke the agent in 20 minutes”*,
|
||||
If you want _“run this every morning”_ or _“poke the agent in 20 minutes”_,
|
||||
cron is the mechanism.
|
||||
|
||||
Troubleshooting: [/automation/troubleshooting](/automation/troubleshooting)
|
||||
|
||||
## TL;DR
|
||||
|
||||
- Cron runs **inside the Gateway** (not inside the model).
|
||||
- Jobs persist under `~/.openclaw/cron/` so restarts don’t lose schedules.
|
||||
- Two execution styles:
|
||||
- **Main session**: enqueue a system event, then run on the next heartbeat.
|
||||
- **Isolated**: run a dedicated agent turn in `cron:<jobId>`, optionally deliver output.
|
||||
- **Isolated**: run a dedicated agent turn in `cron:<jobId>`, with delivery (announce by default or none).
|
||||
- Wakeups are first-class: a job can request “wake now” vs “next heartbeat”.
|
||||
|
||||
## Quick start (actionable)
|
||||
|
||||
Create a one-shot reminder, verify it exists, and run it immediately:
|
||||
|
||||
```bash
|
||||
openclaw cron add \
|
||||
--name "Reminder" \
|
||||
--at "2026-02-01T16:00:00Z" \
|
||||
--session main \
|
||||
--system-event "Reminder: check the cron docs draft" \
|
||||
--wake now \
|
||||
--delete-after-run
|
||||
|
||||
openclaw cron list
|
||||
openclaw cron run <job-id>
|
||||
openclaw cron runs --id <job-id>
|
||||
```
|
||||
|
||||
Schedule a recurring isolated job with delivery:
|
||||
|
||||
```bash
|
||||
openclaw cron add \
|
||||
--name "Morning brief" \
|
||||
--cron "0 7 * * *" \
|
||||
--tz "America/Los_Angeles" \
|
||||
--session isolated \
|
||||
--message "Summarize overnight updates." \
|
||||
--announce \
|
||||
--channel slack \
|
||||
--to "channel:C1234567890"
|
||||
```
|
||||
|
||||
## Tool-call equivalents (Gateway cron tool)
|
||||
|
||||
For the canonical JSON shapes and examples, see [JSON schema for tool calls](/automation/cron-jobs#json-schema-for-tool-calls).
|
||||
|
||||
## Where cron jobs are stored
|
||||
|
||||
Cron jobs are persisted on the Gateway host at `~/.openclaw/cron/jobs.json` by default.
|
||||
The Gateway loads the file into memory and writes it back on changes, so manual edits
|
||||
are only safe when the Gateway is stopped. Prefer `openclaw cron add/edit` or the cron
|
||||
tool call API for changes.
|
||||
|
||||
## Beginner-friendly overview
|
||||
|
||||
Think of a cron job as: **when** to run + **what** to do.
|
||||
|
||||
1) **Choose a schedule**
|
||||
1. **Choose a schedule**
|
||||
- One-shot reminder → `schedule.kind = "at"` (CLI: `--at`)
|
||||
- Repeating job → `schedule.kind = "every"` or `schedule.kind = "cron"`
|
||||
- If your ISO timestamp omits a timezone, it is treated as **UTC**.
|
||||
|
||||
2) **Choose where it runs**
|
||||
2. **Choose where it runs**
|
||||
- `sessionTarget: "main"` → run during the next heartbeat with main context.
|
||||
- `sessionTarget: "isolated"` → run a dedicated agent turn in `cron:<jobId>`.
|
||||
|
||||
3) **Choose the payload**
|
||||
3. **Choose the payload**
|
||||
- Main session → `payload.kind = "systemEvent"`
|
||||
- Isolated session → `payload.kind = "agentTurn"`
|
||||
|
||||
Optional: `deleteAfterRun: true` removes successful one-shot jobs from the store.
|
||||
Optional: one-shot jobs (`schedule.kind = "at"`) delete after success by default. Set
|
||||
`deleteAfterRun: false` to keep them (they will disable after success).
|
||||
|
||||
## Concepts
|
||||
|
||||
### Jobs
|
||||
|
||||
A cron job is a stored record with:
|
||||
|
||||
- a **schedule** (when it should run),
|
||||
- a **payload** (what it should do),
|
||||
- optional **delivery** (where output should be sent).
|
||||
- optional **delivery mode** (announce or none).
|
||||
- optional **agent binding** (`agentId`): run the job under a specific agent; if
|
||||
missing or unknown, the gateway falls back to the default agent.
|
||||
|
||||
Jobs are identified by a stable `jobId` (used by CLI/Gateway APIs).
|
||||
In agent tool calls, `jobId` is canonical; legacy `id` is accepted for compatibility.
|
||||
Jobs can optionally auto-delete after a successful one-shot run via `deleteAfterRun: true`.
|
||||
One-shot jobs auto-delete after success by default; set `deleteAfterRun: false` to keep them.
|
||||
|
||||
### Schedules
|
||||
|
||||
Cron supports three schedule kinds:
|
||||
- `at`: one-shot timestamp (ms since epoch). Gateway accepts ISO 8601 and coerces to UTC.
|
||||
|
||||
- `at`: one-shot timestamp via `schedule.at` (ISO 8601).
|
||||
- `every`: fixed interval (ms).
|
||||
- `cron`: 5-field cron expression with optional IANA timezone.
|
||||
|
||||
@@ -67,49 +121,81 @@ local timezone is used.
|
||||
### Main vs isolated execution
|
||||
|
||||
#### Main session jobs (system events)
|
||||
|
||||
Main jobs enqueue a system event and optionally wake the heartbeat runner.
|
||||
They must use `payload.kind = "systemEvent"`.
|
||||
|
||||
- `wakeMode: "next-heartbeat"` (default): event waits for the next scheduled heartbeat.
|
||||
- `wakeMode: "now"`: event triggers an immediate heartbeat run.
|
||||
- `wakeMode: "now"` (default): event triggers an immediate heartbeat run.
|
||||
- `wakeMode: "next-heartbeat"`: event waits for the next scheduled heartbeat.
|
||||
|
||||
This is the best fit when you want the normal heartbeat prompt + main-session context.
|
||||
See [Heartbeat](/gateway/heartbeat).
|
||||
|
||||
#### Isolated jobs (dedicated cron sessions)
|
||||
|
||||
Isolated jobs run a dedicated agent turn in session `cron:<jobId>`.
|
||||
|
||||
Key behaviors:
|
||||
|
||||
- Prompt is prefixed with `[cron:<jobId> <job name>]` for traceability.
|
||||
- Each run starts a **fresh session id** (no prior conversation carry-over).
|
||||
- A summary is posted to the main session (prefix `Cron`, configurable).
|
||||
- `wakeMode: "now"` triggers an immediate heartbeat after posting the summary.
|
||||
- If `payload.deliver: true`, output is delivered to a channel; otherwise it stays internal.
|
||||
- Default behavior: if `delivery` is omitted, isolated jobs announce a summary (`delivery.mode = "announce"`).
|
||||
- `delivery.mode` (isolated-only) chooses what happens:
|
||||
- `announce`: deliver a summary to the target channel and post a brief summary to the main session.
|
||||
- `none`: internal only (no delivery, no main-session summary).
|
||||
- `wakeMode` controls when the main-session summary posts:
|
||||
- `now`: immediate heartbeat.
|
||||
- `next-heartbeat`: waits for the next scheduled heartbeat.
|
||||
|
||||
Use isolated jobs for noisy, frequent, or "background chores" that shouldn't spam
|
||||
your main chat history.
|
||||
|
||||
### Payload shapes (what runs)
|
||||
|
||||
Two payload kinds are supported:
|
||||
|
||||
- `systemEvent`: main-session only, routed through the heartbeat prompt.
|
||||
- `agentTurn`: isolated-session only, runs a dedicated agent turn.
|
||||
|
||||
Common `agentTurn` fields:
|
||||
|
||||
- `message`: required text prompt.
|
||||
- `model` / `thinking`: optional overrides (see below).
|
||||
- `timeoutSeconds`: optional timeout override.
|
||||
- `deliver`: `true` to send output to a channel target.
|
||||
- `channel`: `last` or a specific channel.
|
||||
- `to`: channel-specific target (phone/chat/channel id).
|
||||
- `bestEffortDeliver`: avoid failing the job if delivery fails.
|
||||
|
||||
Isolation options (only for `session=isolated`):
|
||||
- `postToMainPrefix` (CLI: `--post-prefix`): prefix for the system event in main.
|
||||
- `postToMainMode`: `summary` (default) or `full`.
|
||||
- `postToMainMaxChars`: max chars when `postToMainMode=full` (default 8000).
|
||||
Delivery config (isolated jobs only):
|
||||
|
||||
- `delivery.mode`: `none` | `announce`.
|
||||
- `delivery.channel`: `last` or a specific channel.
|
||||
- `delivery.to`: channel-specific target (phone/chat/channel id).
|
||||
- `delivery.bestEffort`: avoid failing the job if announce delivery fails.
|
||||
|
||||
Announce delivery suppresses messaging tool sends for the run; use `delivery.channel`/`delivery.to`
|
||||
to target the chat instead. When `delivery.mode = "none"`, no summary is posted to the main session.
|
||||
|
||||
If `delivery` is omitted for isolated jobs, OpenClaw defaults to `announce`.
|
||||
|
||||
#### Announce delivery flow
|
||||
|
||||
When `delivery.mode = "announce"`, cron delivers directly via the outbound channel adapters.
|
||||
The main agent is not spun up to craft or forward the message.
|
||||
|
||||
Behavior details:
|
||||
|
||||
- Content: delivery uses the isolated run's outbound payloads (text/media) with normal chunking and
|
||||
channel formatting.
|
||||
- Heartbeat-only responses (`HEARTBEAT_OK` with no real content) are not delivered.
|
||||
- If the isolated run already sent a message to the same target via the message tool, delivery is
|
||||
skipped to avoid duplicates.
|
||||
- Missing or invalid delivery targets fail the job unless `delivery.bestEffort = true`.
|
||||
- A short summary is posted to the main session only when `delivery.mode = "announce"`.
|
||||
- The main-session summary respects `wakeMode`: `now` triggers an immediate heartbeat and
|
||||
`next-heartbeat` waits for the next scheduled heartbeat.
|
||||
|
||||
### Model and thinking overrides
|
||||
|
||||
Isolated jobs (`agentTurn`) can override the model and thinking level:
|
||||
|
||||
- `model`: Provider/model string (e.g., `anthropic/claude-sonnet-4-20250514`) or alias (e.g., `opus`)
|
||||
- `thinking`: Thinking level (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`; GPT-5.2 + Codex models only)
|
||||
|
||||
@@ -118,28 +204,31 @@ session model. We recommend model overrides only for isolated jobs to avoid
|
||||
unexpected context shifts.
|
||||
|
||||
Resolution priority:
|
||||
|
||||
1. Job payload override (highest)
|
||||
2. Hook-specific defaults (e.g., `hooks.gmail.model`)
|
||||
3. Agent config default
|
||||
|
||||
### Delivery (channel + target)
|
||||
Isolated jobs can deliver output to a channel. The job payload can specify:
|
||||
- `channel`: `whatsapp` / `telegram` / `discord` / `slack` / `mattermost` (plugin) / `signal` / `imessage` / `last`
|
||||
- `to`: channel-specific recipient target
|
||||
|
||||
If `channel` or `to` is omitted, cron can fall back to the main session’s “last route”
|
||||
(the last place the agent replied).
|
||||
Isolated jobs can deliver output to a channel via the top-level `delivery` config:
|
||||
|
||||
Delivery notes:
|
||||
- If `to` is set, cron auto-delivers the agent’s final output even if `deliver` is omitted.
|
||||
- Use `deliver: true` when you want last-route delivery without an explicit `to`.
|
||||
- Use `deliver: false` to keep output internal even if a `to` is present.
|
||||
- `delivery.mode`: `announce` (deliver a summary) or `none`.
|
||||
- `delivery.channel`: `whatsapp` / `telegram` / `discord` / `slack` / `mattermost` (plugin) / `signal` / `imessage` / `last`.
|
||||
- `delivery.to`: channel-specific recipient target.
|
||||
|
||||
Delivery config is only valid for isolated jobs (`sessionTarget: "isolated"`).
|
||||
|
||||
If `delivery.channel` or `delivery.to` is omitted, cron can fall back to the main session’s
|
||||
“last route” (the last place the agent replied).
|
||||
|
||||
Target format reminders:
|
||||
|
||||
- Slack/Discord/Mattermost (plugin) targets should use explicit prefixes (e.g. `channel:<id>`, `user:<id>`) to avoid ambiguity.
|
||||
- Telegram topics should use the `:topic:` form (see below).
|
||||
|
||||
#### Telegram delivery targets (topics / forum threads)
|
||||
|
||||
Telegram supports forum topics via `message_thread_id`. For cron delivery, you can encode
|
||||
the topic/thread into the `to` field:
|
||||
|
||||
@@ -148,9 +237,90 @@ the topic/thread into the `to` field:
|
||||
- `-1001234567890:123` (shorthand: numeric suffix)
|
||||
|
||||
Prefixed targets like `telegram:...` / `telegram:group:...` are also accepted:
|
||||
|
||||
- `telegram:group:-1001234567890:topic:123`
|
||||
|
||||
## JSON schema for tool calls
|
||||
|
||||
Use these shapes when calling Gateway `cron.*` tools directly (agent tool calls or RPC).
|
||||
CLI flags accept human durations like `20m`, but tool calls should use an ISO 8601 string
|
||||
for `schedule.at` and milliseconds for `schedule.everyMs`.
|
||||
|
||||
### cron.add params
|
||||
|
||||
One-shot, main session job (system event):
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Reminder",
|
||||
"schedule": { "kind": "at", "at": "2026-02-01T16:00:00Z" },
|
||||
"sessionTarget": "main",
|
||||
"wakeMode": "now",
|
||||
"payload": { "kind": "systemEvent", "text": "Reminder text" },
|
||||
"deleteAfterRun": true
|
||||
}
|
||||
```
|
||||
|
||||
Recurring, isolated job with delivery:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Morning brief",
|
||||
"schedule": { "kind": "cron", "expr": "0 7 * * *", "tz": "America/Los_Angeles" },
|
||||
"sessionTarget": "isolated",
|
||||
"wakeMode": "next-heartbeat",
|
||||
"payload": {
|
||||
"kind": "agentTurn",
|
||||
"message": "Summarize overnight updates."
|
||||
},
|
||||
"delivery": {
|
||||
"mode": "announce",
|
||||
"channel": "slack",
|
||||
"to": "channel:C1234567890",
|
||||
"bestEffort": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `schedule.kind`: `at` (`at`), `every` (`everyMs`), or `cron` (`expr`, optional `tz`).
|
||||
- `schedule.at` accepts ISO 8601 (timezone optional; treated as UTC when omitted).
|
||||
- `everyMs` is milliseconds.
|
||||
- `sessionTarget` must be `"main"` or `"isolated"` and must match `payload.kind`.
|
||||
- Optional fields: `agentId`, `description`, `enabled`, `deleteAfterRun` (defaults to true for `at`),
|
||||
`delivery`.
|
||||
- `wakeMode` defaults to `"now"` when omitted.
|
||||
|
||||
### cron.update params
|
||||
|
||||
```json
|
||||
{
|
||||
"jobId": "job-123",
|
||||
"patch": {
|
||||
"enabled": false,
|
||||
"schedule": { "kind": "every", "everyMs": 3600000 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `jobId` is canonical; `id` is accepted for compatibility.
|
||||
- Use `agentId: null` in the patch to clear an agent binding.
|
||||
|
||||
### cron.run and cron.remove params
|
||||
|
||||
```json
|
||||
{ "jobId": "job-123", "mode": "force" }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "jobId": "job-123" }
|
||||
```
|
||||
|
||||
## Storage & history
|
||||
|
||||
- Job store: `~/.openclaw/cron/jobs.json` (Gateway-managed JSON).
|
||||
- Run history: `~/.openclaw/cron/runs/<jobId>.jsonl` (JSONL, auto-pruned).
|
||||
- Override store path: `cron.store` in config.
|
||||
@@ -162,18 +332,20 @@ Prefixed targets like `telegram:...` / `telegram:group:...` are also accepted:
|
||||
cron: {
|
||||
enabled: true, // default true
|
||||
store: "~/.openclaw/cron/jobs.json",
|
||||
maxConcurrentRuns: 1 // default 1
|
||||
}
|
||||
maxConcurrentRuns: 1, // default 1
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Disable cron entirely:
|
||||
|
||||
- `cron.enabled: false` (config)
|
||||
- `OPENCLAW_SKIP_CRON=1` (env)
|
||||
|
||||
## CLI quickstart
|
||||
|
||||
One-shot reminder (UTC ISO, auto-delete after success):
|
||||
|
||||
```bash
|
||||
openclaw cron add \
|
||||
--name "Send reminder" \
|
||||
@@ -185,6 +357,7 @@ openclaw cron add \
|
||||
```
|
||||
|
||||
One-shot reminder (main session, wake immediately):
|
||||
|
||||
```bash
|
||||
openclaw cron add \
|
||||
--name "Calendar check" \
|
||||
@@ -194,7 +367,8 @@ openclaw cron add \
|
||||
--wake now
|
||||
```
|
||||
|
||||
Recurring isolated job (deliver to WhatsApp):
|
||||
Recurring isolated job (announce to WhatsApp):
|
||||
|
||||
```bash
|
||||
openclaw cron add \
|
||||
--name "Morning status" \
|
||||
@@ -202,12 +376,13 @@ openclaw cron add \
|
||||
--tz "America/Los_Angeles" \
|
||||
--session isolated \
|
||||
--message "Summarize inbox + calendar for today." \
|
||||
--deliver \
|
||||
--announce \
|
||||
--channel whatsapp \
|
||||
--to "+15551234567"
|
||||
```
|
||||
|
||||
Recurring isolated job (deliver to a Telegram topic):
|
||||
|
||||
```bash
|
||||
openclaw cron add \
|
||||
--name "Nightly summary (topic)" \
|
||||
@@ -215,12 +390,13 @@ openclaw cron add \
|
||||
--tz "America/Los_Angeles" \
|
||||
--session isolated \
|
||||
--message "Summarize today; send to the nightly topic." \
|
||||
--deliver \
|
||||
--announce \
|
||||
--channel telegram \
|
||||
--to "-1001234567890:topic:123"
|
||||
```
|
||||
|
||||
Isolated job with model and thinking override:
|
||||
|
||||
```bash
|
||||
openclaw cron add \
|
||||
--name "Deep analysis" \
|
||||
@@ -230,11 +406,13 @@ openclaw cron add \
|
||||
--message "Weekly deep analysis of project progress." \
|
||||
--model "opus" \
|
||||
--thinking high \
|
||||
--deliver \
|
||||
--announce \
|
||||
--channel whatsapp \
|
||||
--to "+15551234567"
|
||||
```
|
||||
|
||||
Agent selection (multi-agent setups):
|
||||
|
||||
```bash
|
||||
# Pin a job to agent "ops" (falls back to default if that agent is missing)
|
||||
openclaw cron add --name "Ops sweep" --cron "0 6 * * *" --session isolated --message "Check ops queue" --agent ops
|
||||
@@ -243,14 +421,16 @@ openclaw cron add --name "Ops sweep" --cron "0 6 * * *" --session isolated --mes
|
||||
openclaw cron edit <jobId> --agent ops
|
||||
openclaw cron edit <jobId> --clear-agent
|
||||
```
|
||||
```
|
||||
|
||||
Manual run (debug):
|
||||
Manual run (force is the default, use `--due` to only run when due):
|
||||
|
||||
```bash
|
||||
openclaw cron run <jobId> --force
|
||||
openclaw cron run <jobId>
|
||||
openclaw cron run <jobId> --due
|
||||
```
|
||||
|
||||
Edit an existing job (patch fields):
|
||||
|
||||
```bash
|
||||
openclaw cron edit <jobId> \
|
||||
--message "Updated prompt" \
|
||||
@@ -259,28 +439,40 @@ openclaw cron edit <jobId> \
|
||||
```
|
||||
|
||||
Run history:
|
||||
|
||||
```bash
|
||||
openclaw cron runs --id <jobId> --limit 50
|
||||
```
|
||||
|
||||
Immediate system event without creating a job:
|
||||
|
||||
```bash
|
||||
openclaw system event --mode now --text "Next heartbeat: check battery."
|
||||
```
|
||||
|
||||
## Gateway API surface
|
||||
|
||||
- `cron.list`, `cron.status`, `cron.add`, `cron.update`, `cron.remove`
|
||||
- `cron.run` (force or due), `cron.runs`
|
||||
For immediate system events without a job, use [`openclaw system event`](/cli/system).
|
||||
For immediate system events without a job, use [`openclaw system event`](/cli/system).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### “Nothing runs”
|
||||
|
||||
- Check cron is enabled: `cron.enabled` and `OPENCLAW_SKIP_CRON`.
|
||||
- Check the Gateway is running continuously (cron runs inside the Gateway process).
|
||||
- For `cron` schedules: confirm timezone (`--tz`) vs the host timezone.
|
||||
|
||||
### A recurring job keeps delaying after failures
|
||||
|
||||
- OpenClaw applies exponential retry backoff for recurring jobs after consecutive errors:
|
||||
30s, 1m, 5m, 15m, then 60m between retries.
|
||||
- Backoff resets automatically after the next successful run.
|
||||
- One-shot (`at`) jobs disable after a terminal run (`ok`, `error`, or `skipped`) and do not retry.
|
||||
|
||||
### Telegram delivers to the wrong place
|
||||
|
||||
- For forum topics, use `-100…:topic:<id>` so it’s explicit and unambiguous.
|
||||
- If you see `telegram:...` prefixes in logs or stored “last route” targets, that’s normal;
|
||||
cron delivery accepts them and still parses topic IDs correctly.
|
||||
|
||||
@@ -4,21 +4,23 @@ read_when:
|
||||
- Deciding how to schedule recurring tasks
|
||||
- Setting up background monitoring or notifications
|
||||
- Optimizing token usage for periodic checks
|
||||
title: "Cron vs Heartbeat"
|
||||
---
|
||||
|
||||
# Cron vs Heartbeat: When to Use Each
|
||||
|
||||
Both heartbeats and cron jobs let you run tasks on a schedule. This guide helps you choose the right mechanism for your use case.
|
||||
|
||||
## Quick Decision Guide
|
||||
|
||||
| Use Case | Recommended | Why |
|
||||
|----------|-------------|-----|
|
||||
| Check inbox every 30 min | Heartbeat | Batches with other checks, context-aware |
|
||||
| Send daily report at 9am sharp | Cron (isolated) | Exact timing needed |
|
||||
| Monitor calendar for upcoming events | Heartbeat | Natural fit for periodic awareness |
|
||||
| Run weekly deep analysis | Cron (isolated) | Standalone task, can use different model |
|
||||
| Remind me in 20 minutes | Cron (main, `--at`) | One-shot with precise timing |
|
||||
| Background project health check | Heartbeat | Piggybacks on existing cycle |
|
||||
| Use Case | Recommended | Why |
|
||||
| ------------------------------------ | ------------------- | ---------------------------------------- |
|
||||
| Check inbox every 30 min | Heartbeat | Batches with other checks, context-aware |
|
||||
| Send daily report at 9am sharp | Cron (isolated) | Exact timing needed |
|
||||
| Monitor calendar for upcoming events | Heartbeat | Natural fit for periodic awareness |
|
||||
| Run weekly deep analysis | Cron (isolated) | Standalone task, can use different model |
|
||||
| Remind me in 20 minutes | Cron (main, `--at`) | One-shot with precise timing |
|
||||
| Background project health check | Heartbeat | Piggybacks on existing cycle |
|
||||
|
||||
## Heartbeat: Periodic Awareness
|
||||
|
||||
@@ -59,12 +61,12 @@ The agent reads this on each heartbeat and handles all items in one turn.
|
||||
agents: {
|
||||
defaults: {
|
||||
heartbeat: {
|
||||
every: "30m", // interval
|
||||
target: "last", // where to deliver alerts
|
||||
activeHours: { start: "08:00", end: "22:00" } // optional
|
||||
}
|
||||
}
|
||||
}
|
||||
every: "30m", // interval
|
||||
target: "last", // where to deliver alerts
|
||||
activeHours: { start: "08:00", end: "22:00" }, // optional
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@@ -88,7 +90,8 @@ Cron jobs run at **exact times** and can run in isolated sessions without affect
|
||||
- **Exact timing**: 5-field cron expressions with timezone support.
|
||||
- **Session isolation**: Runs in `cron:<jobId>` without polluting main history.
|
||||
- **Model overrides**: Use a cheaper or more powerful model per job.
|
||||
- **Delivery control**: Can deliver directly to a channel; still posts a summary to main by default (configurable).
|
||||
- **Delivery control**: Isolated jobs default to `announce` (summary); choose `none` as needed.
|
||||
- **Immediate delivery**: Announce mode posts directly without waiting for heartbeat.
|
||||
- **No agent context needed**: Runs even if main session is idle or compacted.
|
||||
- **One-shot support**: `--at` for precise future timestamps.
|
||||
|
||||
@@ -102,12 +105,12 @@ openclaw cron add \
|
||||
--session isolated \
|
||||
--message "Generate today's briefing: weather, calendar, top emails, news summary." \
|
||||
--model opus \
|
||||
--deliver \
|
||||
--announce \
|
||||
--channel whatsapp \
|
||||
--to "+15551234567"
|
||||
```
|
||||
|
||||
This runs at exactly 7:00 AM New York time, uses Opus for quality, and delivers directly to WhatsApp.
|
||||
This runs at exactly 7:00 AM New York time, uses Opus for quality, and announces a summary directly to WhatsApp.
|
||||
|
||||
### Cron example: One-shot reminder
|
||||
|
||||
@@ -157,8 +160,10 @@ The most efficient setup uses **both**:
|
||||
### Example: Efficient automation setup
|
||||
|
||||
**HEARTBEAT.md** (checked every 30 min):
|
||||
|
||||
```md
|
||||
# Heartbeat checklist
|
||||
|
||||
- Scan inbox for urgent emails
|
||||
- Check calendar for events in next 2h
|
||||
- Review any pending tasks
|
||||
@@ -166,9 +171,10 @@ The most efficient setup uses **both**:
|
||||
```
|
||||
|
||||
**Cron jobs** (precise timing):
|
||||
|
||||
```bash
|
||||
# Daily morning briefing at 7am
|
||||
openclaw cron add --name "Morning brief" --cron "0 7 * * *" --session isolated --message "..." --deliver
|
||||
openclaw cron add --name "Morning brief" --cron "0 7 * * *" --session isolated --message "..." --announce
|
||||
|
||||
# Weekly project review on Mondays at 9am
|
||||
openclaw cron add --name "Weekly review" --cron "0 9 * * 1" --session isolated --message "..." --model opus
|
||||
@@ -177,7 +183,6 @@ openclaw cron add --name "Weekly review" --cron "0 9 * * 1" --session isolated -
|
||||
openclaw cron add --name "Call back" --at "2h" --session main --system-event "Call back the client" --wake now
|
||||
```
|
||||
|
||||
|
||||
## Lobster: Deterministic workflows with approvals
|
||||
|
||||
Lobster is the workflow runtime for **multi-step tool pipelines** that need deterministic execution and explicit approvals.
|
||||
@@ -191,8 +196,8 @@ Use it when the task is more than a single agent turn, and you want a resumable
|
||||
|
||||
### How it pairs with heartbeat and cron
|
||||
|
||||
- **Heartbeat/cron** decide *when* a run happens.
|
||||
- **Lobster** defines *what steps* happen once the run starts.
|
||||
- **Heartbeat/cron** decide _when_ a run happens.
|
||||
- **Lobster** defines _what steps_ happen once the run starts.
|
||||
|
||||
For scheduled workflows, use cron or heartbeat to trigger an agent turn that calls Lobster.
|
||||
For ad-hoc workflows, call Lobster directly.
|
||||
@@ -210,17 +215,18 @@ See [Lobster](/tools/lobster) for full usage and examples.
|
||||
|
||||
Both heartbeat and cron can interact with the main session, but differently:
|
||||
|
||||
| | Heartbeat | Cron (main) | Cron (isolated) |
|
||||
|---|---|---|---|
|
||||
| Session | Main | Main (via system event) | `cron:<jobId>` |
|
||||
| History | Shared | Shared | Fresh each run |
|
||||
| Context | Full | Full | None (starts clean) |
|
||||
| Model | Main session model | Main session model | Can override |
|
||||
| Output | Delivered if not `HEARTBEAT_OK` | Heartbeat prompt + event | Summary posted to main |
|
||||
| | Heartbeat | Cron (main) | Cron (isolated) |
|
||||
| ------- | ------------------------------- | ------------------------ | -------------------------- |
|
||||
| Session | Main | Main (via system event) | `cron:<jobId>` |
|
||||
| History | Shared | Shared | Fresh each run |
|
||||
| Context | Full | Full | None (starts clean) |
|
||||
| Model | Main session model | Main session model | Can override |
|
||||
| Output | Delivered if not `HEARTBEAT_OK` | Heartbeat prompt + event | Announce summary (default) |
|
||||
|
||||
### When to use main session cron
|
||||
|
||||
Use `--session main` with `--system-event` when you want:
|
||||
|
||||
- The reminder/event to appear in main session context
|
||||
- The agent to handle it during the next heartbeat with full context
|
||||
- No separate isolated run
|
||||
@@ -237,9 +243,10 @@ openclaw cron add \
|
||||
### When to use isolated cron
|
||||
|
||||
Use `--session isolated` when you want:
|
||||
|
||||
- A clean slate without prior context
|
||||
- Different model or thinking settings
|
||||
- Output delivered directly to a channel (summary still posts to main by default)
|
||||
- Announce summaries directly to a channel
|
||||
- History that doesn't clutter main session
|
||||
|
||||
```bash
|
||||
@@ -250,18 +257,19 @@ openclaw cron add \
|
||||
--message "Weekly codebase analysis..." \
|
||||
--model opus \
|
||||
--thinking high \
|
||||
--deliver
|
||||
--announce
|
||||
```
|
||||
|
||||
## Cost Considerations
|
||||
|
||||
| Mechanism | Cost Profile |
|
||||
|-----------|--------------|
|
||||
| Heartbeat | One turn every N minutes; scales with HEARTBEAT.md size |
|
||||
| Cron (main) | Adds event to next heartbeat (no isolated turn) |
|
||||
| Cron (isolated) | Full agent turn per job; can use cheaper model |
|
||||
| Mechanism | Cost Profile |
|
||||
| --------------- | ------------------------------------------------------- |
|
||||
| Heartbeat | One turn every N minutes; scales with HEARTBEAT.md size |
|
||||
| Cron (main) | Adds event to next heartbeat (no isolated turn) |
|
||||
| Cron (isolated) | Full agent turn per job; can use cheaper model |
|
||||
|
||||
**Tips**:
|
||||
|
||||
- Keep `HEARTBEAT.md` small to minimize token overhead.
|
||||
- Batch similar checks into heartbeat instead of multiple cron jobs.
|
||||
- Use `target: "none"` on heartbeat if you only want internal processing.
|
||||
|
||||
@@ -3,6 +3,7 @@ summary: "Gmail Pub/Sub push wired into OpenClaw webhooks via gogcli"
|
||||
read_when:
|
||||
- Wiring Gmail inbox triggers to OpenClaw
|
||||
- Setting up Pub/Sub push for agent wake
|
||||
title: "Gmail PubSub"
|
||||
---
|
||||
|
||||
# Gmail Pub/Sub -> OpenClaw
|
||||
@@ -26,8 +27,8 @@ Example hook config (enable Gmail preset mapping):
|
||||
enabled: true,
|
||||
token: "OPENCLAW_HOOK_TOKEN",
|
||||
path: "/hooks",
|
||||
presets: ["gmail"]
|
||||
}
|
||||
presets: ["gmail"],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@@ -47,15 +48,14 @@ that sets `deliver` + optional `channel`/`to`:
|
||||
wakeMode: "now",
|
||||
name: "Gmail",
|
||||
sessionKey: "hook:gmail:{{messages[0].id}}",
|
||||
messageTemplate:
|
||||
"New email from {{messages[0].from}}\nSubject: {{messages[0].subject}}\n{{messages[0].snippet}}\n{{messages[0].body}}",
|
||||
messageTemplate: "New email from {{messages[0].from}}\nSubject: {{messages[0].subject}}\n{{messages[0].snippet}}\n{{messages[0].body}}",
|
||||
model: "openai/gpt-5.2-mini",
|
||||
deliver: true,
|
||||
channel: "last"
|
||||
channel: "last",
|
||||
// to: "+15551234567"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@@ -73,13 +73,14 @@ To set a default model and thinking level specifically for Gmail hooks, add
|
||||
hooks: {
|
||||
gmail: {
|
||||
model: "openrouter/meta-llama/llama-3.3-70b-instruct:free",
|
||||
thinking: "off"
|
||||
}
|
||||
}
|
||||
thinking: "off",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- Per-hook `model`/`thinking` in the mapping still overrides these defaults.
|
||||
- Fallback order: `hooks.gmail.model` → `agents.defaults.model.fallbacks` → primary (auth/rate-limit/timeouts).
|
||||
- If `agents.defaults.models` is set, the Gmail model must be in the allowlist.
|
||||
@@ -99,6 +100,7 @@ openclaw webhooks gmail setup \
|
||||
```
|
||||
|
||||
Defaults:
|
||||
|
||||
- Uses Tailscale Funnel for the public push endpoint.
|
||||
- Writes `hooks.gmail` config for `openclaw webhooks gmail run`.
|
||||
- Enables the Gmail hook preset (`hooks.presets: ["gmail"]`).
|
||||
@@ -117,6 +119,7 @@ Platform note: on macOS the wizard installs `gcloud`, `gogcli`, and `tailscale`
|
||||
via Homebrew; on Linux install them manually first.
|
||||
|
||||
Gateway auto-start (recommended):
|
||||
|
||||
- When `hooks.enabled=true` and `hooks.gmail.account` is set, the Gateway starts
|
||||
`gog gmail watch serve` on boot and auto-renews the watch.
|
||||
- Set `OPENCLAW_SKIP_GMAIL_WATCHER=1` to opt out (useful if you run the daemon yourself).
|
||||
@@ -131,7 +134,7 @@ openclaw webhooks gmail run
|
||||
|
||||
## One-time setup
|
||||
|
||||
1) Select the GCP project **that owns the OAuth client** used by `gog`.
|
||||
1. Select the GCP project **that owns the OAuth client** used by `gog`.
|
||||
|
||||
```bash
|
||||
gcloud auth login
|
||||
@@ -140,19 +143,19 @@ gcloud config set project <project-id>
|
||||
|
||||
Note: Gmail watch requires the Pub/Sub topic to live in the same project as the OAuth client.
|
||||
|
||||
2) Enable APIs:
|
||||
2. Enable APIs:
|
||||
|
||||
```bash
|
||||
gcloud services enable gmail.googleapis.com pubsub.googleapis.com
|
||||
```
|
||||
|
||||
3) Create a topic:
|
||||
3. Create a topic:
|
||||
|
||||
```bash
|
||||
gcloud pubsub topics create gog-gmail-watch
|
||||
```
|
||||
|
||||
4) Allow Gmail push to publish:
|
||||
4. Allow Gmail push to publish:
|
||||
|
||||
```bash
|
||||
gcloud pubsub topics add-iam-policy-binding gog-gmail-watch \
|
||||
@@ -189,6 +192,7 @@ gog gmail watch serve \
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `--token` protects the push endpoint (`x-gog-token` or `?token=`).
|
||||
- `--hook-url` points to OpenClaw `/hooks/gmail` (mapped; isolated run + summary to main).
|
||||
- `--include-body` and `--max-bytes` control the body snippet sent to OpenClaw.
|
||||
|
||||
@@ -3,7 +3,9 @@ summary: "Hooks: event-driven automation for commands and lifecycle events"
|
||||
read_when:
|
||||
- You want event-driven automation for /new, /reset, /stop, and agent lifecycle events
|
||||
- You want to build, install, or debug hooks
|
||||
title: "Hooks"
|
||||
---
|
||||
|
||||
# Hooks
|
||||
|
||||
Hooks provide an extensible event-driven system for automating actions in response to agent commands and events. Hooks are automatically discovered from directories and can be managed via CLI commands, similar to how skills work in OpenClaw.
|
||||
@@ -14,10 +16,11 @@ Hooks are small scripts that run when something happens. There are two kinds:
|
||||
|
||||
- **Hooks** (this page): run inside the Gateway when agent events fire, like `/new`, `/reset`, `/stop`, or lifecycle events.
|
||||
- **Webhooks**: external HTTP webhooks that let other systems trigger work in OpenClaw. See [Webhook Hooks](/automation/webhook) or use `openclaw webhooks` for Gmail helper commands.
|
||||
|
||||
Hooks can also be bundled inside plugins; see [Plugins](/plugin#plugin-hooks).
|
||||
|
||||
Hooks can also be bundled inside plugins; see [Plugins](/tools/plugin#plugin-hooks).
|
||||
|
||||
Common uses:
|
||||
|
||||
- Save a memory snapshot when you reset a session
|
||||
- Keep an audit trail of commands for troubleshooting or compliance
|
||||
- Trigger follow-up automation when a session starts or ends
|
||||
@@ -28,6 +31,7 @@ If you can write a small TypeScript function, you can write a hook. Hooks are di
|
||||
## Overview
|
||||
|
||||
The hooks system allows you to:
|
||||
|
||||
- Save session context to memory when `/new` is issued
|
||||
- Log all commands for auditing
|
||||
- Trigger custom automations on agent lifecycle events
|
||||
@@ -125,7 +129,8 @@ The `HOOK.md` file contains metadata in YAML frontmatter plus Markdown documenta
|
||||
name: my-hook
|
||||
description: "Short description of what this hook does"
|
||||
homepage: https://docs.openclaw.ai/hooks#my-hook
|
||||
metadata: {"openclaw":{"emoji":"🔗","events":["command:new"],"requires":{"bins":["node"]}}}
|
||||
metadata:
|
||||
{ "openclaw": { "emoji": "🔗", "events": ["command:new"], "requires": { "bins": ["node"] } } }
|
||||
---
|
||||
|
||||
# My Hook
|
||||
@@ -169,11 +174,11 @@ The `metadata.openclaw` object supports:
|
||||
The `handler.ts` file exports a `HookHandler` function:
|
||||
|
||||
```typescript
|
||||
import type { HookHandler } from '../../src/hooks/hooks.js';
|
||||
import type { HookHandler } from "../../src/hooks/hooks.js";
|
||||
|
||||
const myHandler: HookHandler = async (event) => {
|
||||
// Only trigger on 'new' command
|
||||
if (event.type !== 'command' || event.action !== 'new') {
|
||||
if (event.type !== "command" || event.action !== "new") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -184,7 +189,7 @@ const myHandler: HookHandler = async (event) => {
|
||||
// Your custom logic here
|
||||
|
||||
// Optionally send message to user
|
||||
event.messages.push('✨ My hook executed!');
|
||||
event.messages.push("✨ My hook executed!");
|
||||
};
|
||||
|
||||
export default myHandler;
|
||||
@@ -271,7 +276,7 @@ cd ~/.openclaw/hooks/my-hook
|
||||
---
|
||||
name: my-hook
|
||||
description: "Does something useful"
|
||||
metadata: {"openclaw":{"emoji":"🎯","events":["command:new"]}}
|
||||
metadata: { "openclaw": { "emoji": "🎯", "events": ["command:new"] } }
|
||||
---
|
||||
|
||||
# My Custom Hook
|
||||
@@ -282,14 +287,14 @@ This hook does something useful when you issue `/new`.
|
||||
### 4. Create handler.ts
|
||||
|
||||
```typescript
|
||||
import type { HookHandler } from '../../src/hooks/hooks.js';
|
||||
import type { HookHandler } from "../../src/hooks/hooks.js";
|
||||
|
||||
const handler: HookHandler = async (event) => {
|
||||
if (event.type !== 'command' || event.action !== 'new') {
|
||||
if (event.type !== "command" || event.action !== "new") {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[my-hook] Running!');
|
||||
console.log("[my-hook] Running!");
|
||||
// Your logic here
|
||||
};
|
||||
|
||||
@@ -439,7 +444,7 @@ openclaw hooks enable session-memory
|
||||
openclaw hooks disable command-logger
|
||||
```
|
||||
|
||||
## Bundled Hooks
|
||||
## Bundled hook reference
|
||||
|
||||
### session-memory
|
||||
|
||||
@@ -452,6 +457,7 @@ Saves session context to memory when you issue `/new`.
|
||||
**Output**: `<workspace>/memory/YYYY-MM-DD-slug.md` (defaults to `~/.openclaw/workspace`)
|
||||
|
||||
**What it does**:
|
||||
|
||||
1. Uses the pre-reset session entry to locate the correct transcript
|
||||
2. Extracts the last 15 lines of conversation
|
||||
3. Uses LLM to generate a descriptive filename slug
|
||||
@@ -468,6 +474,7 @@ Saves session context to memory when you issue `/new`.
|
||||
```
|
||||
|
||||
**Filename examples**:
|
||||
|
||||
- `2026-01-16-vendor-pitch.md`
|
||||
- `2026-01-16-api-design.md`
|
||||
- `2026-01-16-1430.md` (fallback timestamp if slug generation fails)
|
||||
@@ -489,6 +496,7 @@ Logs all command events to a centralized audit file.
|
||||
**Output**: `~/.openclaw/logs/commands.log`
|
||||
|
||||
**What it does**:
|
||||
|
||||
1. Captures event details (command action, timestamp, session key, sender ID, source)
|
||||
2. Appends to log file in JSONL format
|
||||
3. Runs silently in the background
|
||||
@@ -565,6 +573,7 @@ Internal hooks must be enabled for this to run.
|
||||
**Requirements**: `workspace.dir` must be configured
|
||||
|
||||
**What it does**:
|
||||
|
||||
1. Reads `BOOT.md` from your workspace
|
||||
2. Runs the instructions via the agent runner
|
||||
3. Sends any requested outbound messages via the message tool
|
||||
@@ -603,7 +612,7 @@ const handler: HookHandler = async (event) => {
|
||||
try {
|
||||
await riskyOperation(event);
|
||||
} catch (err) {
|
||||
console.error('[my-handler] Failed:', err instanceof Error ? err.message : String(err));
|
||||
console.error("[my-handler] Failed:", err instanceof Error ? err.message : String(err));
|
||||
// Don't throw - let other handlers run
|
||||
}
|
||||
};
|
||||
@@ -616,7 +625,7 @@ Return early if the event isn't relevant:
|
||||
```typescript
|
||||
const handler: HookHandler = async (event) => {
|
||||
// Only handle 'new' commands
|
||||
if (event.type !== 'command' || event.action !== 'new') {
|
||||
if (event.type !== "command" || event.action !== "new") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -629,13 +638,13 @@ const handler: HookHandler = async (event) => {
|
||||
Specify exact events in metadata when possible:
|
||||
|
||||
```yaml
|
||||
metadata: {"openclaw":{"events":["command:new"]}} # Specific
|
||||
metadata: { "openclaw": { "events": ["command:new"] } } # Specific
|
||||
```
|
||||
|
||||
Rather than:
|
||||
|
||||
```yaml
|
||||
metadata: {"openclaw":{"events":["command"]}} # General - more overhead
|
||||
metadata: { "openclaw": { "events": ["command"] } } # General - more overhead
|
||||
```
|
||||
|
||||
## Debugging
|
||||
@@ -664,7 +673,7 @@ In your handler, log when it's called:
|
||||
|
||||
```typescript
|
||||
const handler: HookHandler = async (event) => {
|
||||
console.log('[my-handler] Triggered:', event.type, event.action);
|
||||
console.log("[my-handler] Triggered:", event.type, event.action);
|
||||
// Your logic
|
||||
};
|
||||
```
|
||||
@@ -698,13 +707,13 @@ tail -f ~/.openclaw/gateway.log
|
||||
Test your handlers in isolation:
|
||||
|
||||
```typescript
|
||||
import { test } from 'vitest';
|
||||
import { createHookEvent } from './src/hooks/hooks.js';
|
||||
import myHandler from './hooks/my-hook/handler.js';
|
||||
import { test } from "vitest";
|
||||
import { createHookEvent } from "./src/hooks/hooks.js";
|
||||
import myHandler from "./hooks/my-hook/handler.js";
|
||||
|
||||
test('my handler works', async () => {
|
||||
const event = createHookEvent('command', 'new', 'test-session', {
|
||||
foo: 'bar'
|
||||
test("my handler works", async () => {
|
||||
const event = createHookEvent("command", "new", "test-session", {
|
||||
foo: "bar",
|
||||
});
|
||||
|
||||
await myHandler(event);
|
||||
@@ -764,18 +773,21 @@ Session reset
|
||||
### Hook Not Discovered
|
||||
|
||||
1. Check directory structure:
|
||||
|
||||
```bash
|
||||
ls -la ~/.openclaw/hooks/my-hook/
|
||||
# Should show: HOOK.md, handler.ts
|
||||
```
|
||||
|
||||
2. Verify HOOK.md format:
|
||||
|
||||
```bash
|
||||
cat ~/.openclaw/hooks/my-hook/HOOK.md
|
||||
# Should have YAML frontmatter with name and metadata
|
||||
```
|
||||
|
||||
3. List all discovered hooks:
|
||||
|
||||
```bash
|
||||
openclaw hooks list
|
||||
```
|
||||
@@ -789,6 +801,7 @@ openclaw hooks info my-hook
|
||||
```
|
||||
|
||||
Look for missing:
|
||||
|
||||
- Binaries (check PATH)
|
||||
- Environment variables
|
||||
- Config values
|
||||
@@ -797,6 +810,7 @@ Look for missing:
|
||||
### Hook Not Executing
|
||||
|
||||
1. Verify hook is enabled:
|
||||
|
||||
```bash
|
||||
openclaw hooks list
|
||||
# Should show ✓ next to enabled hooks
|
||||
@@ -805,6 +819,7 @@ Look for missing:
|
||||
2. Restart your gateway process so hooks reload.
|
||||
|
||||
3. Check gateway logs for errors:
|
||||
|
||||
```bash
|
||||
./scripts/clawlog.sh | grep hook
|
||||
```
|
||||
@@ -843,17 +858,19 @@ node -e "import('./path/to/handler.ts').then(console.log)"
|
||||
**After**:
|
||||
|
||||
1. Create hook directory:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.openclaw/hooks/my-hook
|
||||
mv ./hooks/handlers/my-handler.ts ~/.openclaw/hooks/my-hook/handler.ts
|
||||
```
|
||||
|
||||
2. Create HOOK.md:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: my-hook
|
||||
description: "My custom hook"
|
||||
metadata: {"openclaw":{"emoji":"🎯","events":["command:new"]}}
|
||||
metadata: { "openclaw": { "emoji": "🎯", "events": ["command:new"] } }
|
||||
---
|
||||
|
||||
# My Hook
|
||||
@@ -862,6 +879,7 @@ node -e "import('./path/to/handler.ts').then(console.log)"
|
||||
```
|
||||
|
||||
3. Update config:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
@@ -876,12 +894,14 @@ node -e "import('./path/to/handler.ts').then(console.log)"
|
||||
```
|
||||
|
||||
4. Verify and restart your gateway process:
|
||||
|
||||
```bash
|
||||
openclaw hooks list
|
||||
# Should show: 🎯 my-hook ✓
|
||||
```
|
||||
|
||||
**Benefits of migration**:
|
||||
|
||||
- Automatic discovery
|
||||
- CLI management
|
||||
- Eligibility checking
|
||||
@@ -3,11 +3,13 @@ summary: "Poll sending via gateway + CLI"
|
||||
read_when:
|
||||
- Adding or modifying poll support
|
||||
- Debugging poll sends from the CLI or gateway
|
||||
title: "Polls"
|
||||
---
|
||||
|
||||
# Polls
|
||||
|
||||
|
||||
## Supported channels
|
||||
|
||||
- WhatsApp (web channel)
|
||||
- Discord
|
||||
- MS Teams (Adaptive Cards)
|
||||
@@ -33,6 +35,7 @@ openclaw message poll --channel msteams --target conversation:19:abc@thread.tacv
|
||||
```
|
||||
|
||||
Options:
|
||||
|
||||
- `--channel`: `whatsapp` (default), `discord`, or `msteams`
|
||||
- `--poll-multi`: allow selecting multiple options
|
||||
- `--poll-duration-hours`: Discord-only (defaults to 24 when omitted)
|
||||
@@ -42,6 +45,7 @@ Options:
|
||||
Method: `poll`
|
||||
|
||||
Params:
|
||||
|
||||
- `to` (string, required)
|
||||
- `question` (string, required)
|
||||
- `options` (string[], required)
|
||||
@@ -51,11 +55,13 @@ Params:
|
||||
- `idempotencyKey` (string, required)
|
||||
|
||||
## Channel differences
|
||||
|
||||
- WhatsApp: 2-12 options, `maxSelections` must be within option count, ignores `durationHours`.
|
||||
- Discord: 2-10 options, `durationHours` clamped to 1-768 hours (default 24). `maxSelections > 1` enables multi-select; Discord does not support a strict selection count.
|
||||
- MS Teams: Adaptive Card polls (OpenClaw-managed). No native poll API; `durationHours` is ignored.
|
||||
|
||||
## Agent tool (Message)
|
||||
|
||||
Use the `message` tool with `poll` action (`to`, `pollQuestion`, `pollOption`, optional `pollMulti`, `pollDurationHours`, `channel`).
|
||||
|
||||
Note: Discord has no “pick exactly N” mode; `pollMulti` maps to multi-select.
|
||||
|
||||
122
docs/automation/troubleshooting.md
Normal file
122
docs/automation/troubleshooting.md
Normal file
@@ -0,0 +1,122 @@
|
||||
---
|
||||
summary: "Troubleshoot cron and heartbeat scheduling and delivery"
|
||||
read_when:
|
||||
- Cron did not run
|
||||
- Cron ran but no message was delivered
|
||||
- Heartbeat seems silent or skipped
|
||||
title: "Automation Troubleshooting"
|
||||
---
|
||||
|
||||
# Automation troubleshooting
|
||||
|
||||
Use this page for scheduler and delivery issues (`cron` + `heartbeat`).
|
||||
|
||||
## Command ladder
|
||||
|
||||
```bash
|
||||
openclaw status
|
||||
openclaw gateway status
|
||||
openclaw logs --follow
|
||||
openclaw doctor
|
||||
openclaw channels status --probe
|
||||
```
|
||||
|
||||
Then run automation checks:
|
||||
|
||||
```bash
|
||||
openclaw cron status
|
||||
openclaw cron list
|
||||
openclaw system heartbeat last
|
||||
```
|
||||
|
||||
## Cron not firing
|
||||
|
||||
```bash
|
||||
openclaw cron status
|
||||
openclaw cron list
|
||||
openclaw cron runs --id <jobId> --limit 20
|
||||
openclaw logs --follow
|
||||
```
|
||||
|
||||
Good output looks like:
|
||||
|
||||
- `cron status` reports enabled and a future `nextWakeAtMs`.
|
||||
- Job is enabled and has a valid schedule/timezone.
|
||||
- `cron runs` shows `ok` or explicit skip reason.
|
||||
|
||||
Common signatures:
|
||||
|
||||
- `cron: scheduler disabled; jobs will not run automatically` → cron disabled in config/env.
|
||||
- `cron: timer tick failed` → scheduler tick crashed; inspect surrounding stack/log context.
|
||||
- `reason: not-due` in run output → manual run called without `--force` and job not due yet.
|
||||
|
||||
## Cron fired but no delivery
|
||||
|
||||
```bash
|
||||
openclaw cron runs --id <jobId> --limit 20
|
||||
openclaw cron list
|
||||
openclaw channels status --probe
|
||||
openclaw logs --follow
|
||||
```
|
||||
|
||||
Good output looks like:
|
||||
|
||||
- Run status is `ok`.
|
||||
- Delivery mode/target are set for isolated jobs.
|
||||
- Channel probe reports target channel connected.
|
||||
|
||||
Common signatures:
|
||||
|
||||
- Run succeeded but delivery mode is `none` → no external message is expected.
|
||||
- Delivery target missing/invalid (`channel`/`to`) → run may succeed internally but skip outbound.
|
||||
- Channel auth errors (`unauthorized`, `missing_scope`, `Forbidden`) → delivery blocked by channel credentials/permissions.
|
||||
|
||||
## Heartbeat suppressed or skipped
|
||||
|
||||
```bash
|
||||
openclaw system heartbeat last
|
||||
openclaw logs --follow
|
||||
openclaw config get agents.defaults.heartbeat
|
||||
openclaw channels status --probe
|
||||
```
|
||||
|
||||
Good output looks like:
|
||||
|
||||
- Heartbeat enabled with non-zero interval.
|
||||
- Last heartbeat result is `ran` (or skip reason is understood).
|
||||
|
||||
Common signatures:
|
||||
|
||||
- `heartbeat skipped` with `reason=quiet-hours` → outside `activeHours`.
|
||||
- `requests-in-flight` → main lane busy; heartbeat deferred.
|
||||
- `empty-heartbeat-file` → `HEARTBEAT.md` exists but has no actionable content.
|
||||
- `alerts-disabled` → visibility settings suppress outbound heartbeat messages.
|
||||
|
||||
## Timezone and activeHours gotchas
|
||||
|
||||
```bash
|
||||
openclaw config get agents.defaults.heartbeat.activeHours
|
||||
openclaw config get agents.defaults.heartbeat.activeHours.timezone
|
||||
openclaw config get agents.defaults.userTimezone || echo "agents.defaults.userTimezone not set"
|
||||
openclaw cron list
|
||||
openclaw logs --follow
|
||||
```
|
||||
|
||||
Quick rules:
|
||||
|
||||
- `Config path not found: agents.defaults.userTimezone` means the key is unset; heartbeat falls back to host timezone (or `activeHours.timezone` if set).
|
||||
- Cron without `--tz` uses gateway host timezone.
|
||||
- Heartbeat `activeHours` uses configured timezone resolution (`user`, `local`, or explicit IANA tz).
|
||||
- ISO timestamps without timezone are treated as UTC for cron `at` schedules.
|
||||
|
||||
Common signatures:
|
||||
|
||||
- Jobs run at the wrong wall-clock time after host timezone changes.
|
||||
- Heartbeat always skipped during your daytime because `activeHours.timezone` is wrong.
|
||||
|
||||
Related:
|
||||
|
||||
- [/automation/cron-jobs](/automation/cron-jobs)
|
||||
- [/gateway/heartbeat](/gateway/heartbeat)
|
||||
- [/automation/cron-vs-heartbeat](/automation/cron-vs-heartbeat)
|
||||
- [/concepts/timezone](/concepts/timezone)
|
||||
@@ -3,6 +3,7 @@ summary: "Webhook ingress for wake and isolated agent runs"
|
||||
read_when:
|
||||
- Adding or changing webhook endpoints
|
||||
- Wiring external systems into OpenClaw
|
||||
title: "Webhooks"
|
||||
---
|
||||
|
||||
# Webhooks
|
||||
@@ -16,18 +17,20 @@ Gateway can expose a small HTTP webhook endpoint for external triggers.
|
||||
hooks: {
|
||||
enabled: true,
|
||||
token: "shared-secret",
|
||||
path: "/hooks"
|
||||
}
|
||||
path: "/hooks",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `hooks.token` is required when `hooks.enabled=true`.
|
||||
- `hooks.path` defaults to `/hooks`.
|
||||
|
||||
## Auth
|
||||
|
||||
Every request must include the hook token. Prefer headers:
|
||||
|
||||
- `Authorization: Bearer <token>` (recommended)
|
||||
- `x-openclaw-token: <token>`
|
||||
- `?token=<token>` (deprecated; logs a warning and will be removed in a future major release)
|
||||
@@ -37,6 +40,7 @@ Every request must include the hook token. Prefer headers:
|
||||
### `POST /hooks/wake`
|
||||
|
||||
Payload:
|
||||
|
||||
```json
|
||||
{ "text": "System line", "mode": "now" }
|
||||
```
|
||||
@@ -45,12 +49,14 @@ Payload:
|
||||
- `mode` optional (`now` | `next-heartbeat`): Whether to trigger an immediate heartbeat (default `now`) or wait for the next periodic check.
|
||||
|
||||
Effect:
|
||||
|
||||
- Enqueues a system event for the **main** session
|
||||
- If `mode=now`, triggers an immediate heartbeat
|
||||
|
||||
### `POST /hooks/agent`
|
||||
|
||||
Payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Run this",
|
||||
@@ -78,6 +84,7 @@ Payload:
|
||||
- `timeoutSeconds` optional (number): Maximum duration for the agent run in seconds.
|
||||
|
||||
Effect:
|
||||
|
||||
- Runs an **isolated** agent turn (own session key)
|
||||
- Always posts a summary into the **main** session
|
||||
- If `wakeMode=now`, triggers an immediate heartbeat
|
||||
@@ -89,6 +96,7 @@ turn arbitrary payloads into `wake` or `agent` actions, with optional templates
|
||||
code transforms.
|
||||
|
||||
Mapping options (summary):
|
||||
|
||||
- `hooks.presets: ["gmail"]` enables the built-in Gmail mapping.
|
||||
- `hooks.mappings` lets you define `match`, `action`, and templates in config.
|
||||
- `hooks.transformsDir` + `transform.module` loads a JS/TS module for custom logic.
|
||||
@@ -99,7 +107,7 @@ Mapping options (summary):
|
||||
- `allowUnsafeExternalContent: true` disables the external content safety wrapper for that hook
|
||||
(dangerous; only for trusted internal sources).
|
||||
- `openclaw webhooks gmail setup` writes `hooks.gmail` config for `openclaw webhooks gmail run`.
|
||||
See [Gmail Pub/Sub](/automation/gmail-pubsub) for the full Gmail watch flow.
|
||||
See [Gmail Pub/Sub](/automation/gmail-pubsub) for the full Gmail watch flow.
|
||||
|
||||
## Responses
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ summary: "Brave Search API setup for web_search"
|
||||
read_when:
|
||||
- You want to use Brave Search for web_search
|
||||
- You need a BRAVE_API_KEY or plan details
|
||||
title: "Brave Search"
|
||||
---
|
||||
|
||||
# Brave Search API
|
||||
@@ -11,9 +12,9 @@ OpenClaw uses Brave Search as the default provider for `web_search`.
|
||||
|
||||
## Get an API key
|
||||
|
||||
1) Create a Brave Search API account at https://brave.com/search/api/
|
||||
2) In the dashboard, choose the **Data for Search** plan and generate an API key.
|
||||
3) Store the key in config (recommended) or set `BRAVE_API_KEY` in the Gateway environment.
|
||||
1. Create a Brave Search API account at [https://brave.com/search/api/](https://brave.com/search/api/)
|
||||
2. In the dashboard, choose the **Data for Search** plan and generate an API key.
|
||||
3. Store the key in config (recommended) or set `BRAVE_API_KEY` in the Gateway environment.
|
||||
|
||||
## Config example
|
||||
|
||||
@@ -25,10 +26,10 @@ OpenClaw uses Brave Search as the default provider for `web_search`.
|
||||
provider: "brave",
|
||||
apiKey: "BRAVE_API_KEY_HERE",
|
||||
maxResults: 5,
|
||||
timeoutSeconds: 30
|
||||
}
|
||||
}
|
||||
}
|
||||
timeoutSeconds: 30,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -4,25 +4,30 @@ read_when:
|
||||
- Setting up BlueBubbles channel
|
||||
- Troubleshooting webhook pairing
|
||||
- Configuring iMessage on macOS
|
||||
title: "BlueBubbles"
|
||||
---
|
||||
|
||||
# BlueBubbles (macOS REST)
|
||||
|
||||
Status: bundled plugin that talks to the BlueBubbles macOS server over HTTP. **Recommended for iMessage integration** due to its richer API and easier setup compared to the legacy imsg channel.
|
||||
|
||||
## Overview
|
||||
|
||||
- Runs on macOS via the BlueBubbles helper app ([bluebubbles.app](https://bluebubbles.app)).
|
||||
- Recommended/tested: macOS Sequoia (15). macOS Tahoe (26) works; edit is currently broken on Tahoe, and group icon updates may report success but not sync.
|
||||
- OpenClaw talks to it through its REST API (`GET /api/v1/ping`, `POST /message/text`, `POST /chat/:id/*`).
|
||||
- Incoming messages arrive via webhooks; outgoing replies, typing indicators, read receipts, and tapbacks are REST calls.
|
||||
- Attachments and stickers are ingested as inbound media (and surfaced to the agent when possible).
|
||||
- Pairing/allowlist works the same way as other channels (`/start/pairing` etc) with `channels.bluebubbles.allowFrom` + pairing codes.
|
||||
- Pairing/allowlist works the same way as other channels (`/channels/pairing` etc) with `channels.bluebubbles.allowFrom` + pairing codes.
|
||||
- Reactions are surfaced as system events just like Slack/Telegram so agents can "mention" them before replying.
|
||||
- Advanced features: edit, unsend, reply threading, message effects, group management.
|
||||
|
||||
## Quick start
|
||||
|
||||
1. Install the BlueBubbles server on your Mac (follow the instructions at [bluebubbles.app/install](https://bluebubbles.app/install)).
|
||||
2. In the BlueBubbles config, enable the web API and set a password.
|
||||
3. Run `openclaw onboard` and select BlueBubbles, or configure manually:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
@@ -30,21 +35,99 @@ Status: bundled plugin that talks to the BlueBubbles macOS server over HTTP. **R
|
||||
enabled: true,
|
||||
serverUrl: "http://192.168.1.100:1234",
|
||||
password: "example-password",
|
||||
webhookPath: "/bluebubbles-webhook"
|
||||
}
|
||||
}
|
||||
webhookPath: "/bluebubbles-webhook",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
4. Point BlueBubbles webhooks to your gateway (example: `https://your-gateway-host:3000/bluebubbles-webhook?password=<password>`).
|
||||
5. Start the gateway; it will register the webhook handler and start pairing.
|
||||
|
||||
## Keeping Messages.app alive (VM / headless setups)
|
||||
|
||||
Some macOS VM / always-on setups can end up with Messages.app going “idle” (incoming events stop until the app is opened/foregrounded). A simple workaround is to **poke Messages every 5 minutes** using an AppleScript + LaunchAgent.
|
||||
|
||||
### 1) Save the AppleScript
|
||||
|
||||
Save this as:
|
||||
|
||||
- `~/Scripts/poke-messages.scpt`
|
||||
|
||||
Example script (non-interactive; does not steal focus):
|
||||
|
||||
```applescript
|
||||
try
|
||||
tell application "Messages"
|
||||
if not running then
|
||||
launch
|
||||
end if
|
||||
|
||||
-- Touch the scripting interface to keep the process responsive.
|
||||
set _chatCount to (count of chats)
|
||||
end tell
|
||||
on error
|
||||
-- Ignore transient failures (first-run prompts, locked session, etc).
|
||||
end try
|
||||
```
|
||||
|
||||
### 2) Install a LaunchAgent
|
||||
|
||||
Save this as:
|
||||
|
||||
- `~/Library/LaunchAgents/com.user.poke-messages.plist`
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.user.poke-messages</string>
|
||||
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/bin/bash</string>
|
||||
<string>-lc</string>
|
||||
<string>/usr/bin/osascript "$HOME/Scripts/poke-messages.scpt"</string>
|
||||
</array>
|
||||
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
|
||||
<key>StartInterval</key>
|
||||
<integer>300</integer>
|
||||
|
||||
<key>StandardOutPath</key>
|
||||
<string>/tmp/poke-messages.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/tmp/poke-messages.err</string>
|
||||
</dict>
|
||||
</plist>
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- This runs **every 300 seconds** and **on login**.
|
||||
- The first run may trigger macOS **Automation** prompts (`osascript` → Messages). Approve them in the same user session that runs the LaunchAgent.
|
||||
|
||||
Load it:
|
||||
|
||||
```bash
|
||||
launchctl unload ~/Library/LaunchAgents/com.user.poke-messages.plist 2>/dev/null || true
|
||||
launchctl load ~/Library/LaunchAgents/com.user.poke-messages.plist
|
||||
```
|
||||
|
||||
## Onboarding
|
||||
|
||||
BlueBubbles is available in the interactive setup wizard:
|
||||
|
||||
```
|
||||
openclaw onboard
|
||||
```
|
||||
|
||||
The wizard prompts for:
|
||||
|
||||
- **Server URL** (required): BlueBubbles server address (e.g., `http://192.168.1.100:1234`)
|
||||
- **Password** (required): API password from BlueBubbles Server settings
|
||||
- **Webhook path** (optional): Defaults to `/bluebubbles-webhook`
|
||||
@@ -52,30 +135,37 @@ The wizard prompts for:
|
||||
- **Allow list**: Phone numbers, emails, or chat targets
|
||||
|
||||
You can also add BlueBubbles via CLI:
|
||||
|
||||
```
|
||||
openclaw channels add bluebubbles --http-url http://192.168.1.100:1234 --password <password>
|
||||
```
|
||||
|
||||
## Access control (DMs + groups)
|
||||
|
||||
DMs:
|
||||
|
||||
- Default: `channels.bluebubbles.dmPolicy = "pairing"`.
|
||||
- Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour).
|
||||
- Approve via:
|
||||
- `openclaw pairing list bluebubbles`
|
||||
- `openclaw pairing approve bluebubbles <CODE>`
|
||||
- Pairing is the default token exchange. Details: [Pairing](/start/pairing)
|
||||
- Pairing is the default token exchange. Details: [Pairing](/channels/pairing)
|
||||
|
||||
Groups:
|
||||
|
||||
- `channels.bluebubbles.groupPolicy = open | allowlist | disabled` (default: `allowlist`).
|
||||
- `channels.bluebubbles.groupAllowFrom` controls who can trigger in groups when `allowlist` is set.
|
||||
|
||||
### Mention gating (groups)
|
||||
|
||||
BlueBubbles supports mention gating for group chats, matching iMessage/WhatsApp behavior:
|
||||
|
||||
- Uses `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`) to detect mentions.
|
||||
- When `requireMention` is enabled for a group, the agent only responds when mentioned.
|
||||
- Control commands from authorized senders bypass mention gating.
|
||||
|
||||
Per-group configuration:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
@@ -83,20 +173,22 @@ Per-group configuration:
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: ["+15555550123"],
|
||||
groups: {
|
||||
"*": { requireMention: true }, // default for all groups
|
||||
"iMessage;-;chat123": { requireMention: false } // override for specific group
|
||||
}
|
||||
}
|
||||
}
|
||||
"*": { requireMention: true }, // default for all groups
|
||||
"iMessage;-;chat123": { requireMention: false }, // override for specific group
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Command gating
|
||||
|
||||
- Control commands (e.g., `/config`, `/model`) require authorization.
|
||||
- Uses `allowFrom` and `groupAllowFrom` to determine command authorization.
|
||||
- Authorized senders can run control commands even without mentioning in groups.
|
||||
|
||||
## Typing + read receipts
|
||||
|
||||
- **Typing indicators**: Sent automatically before and during response generation.
|
||||
- **Read receipts**: Controlled by `channels.bluebubbles.sendReadReceipts` (default: `true`).
|
||||
- **Typing indicators**: OpenClaw sends typing start events; BlueBubbles clears typing automatically on send or timeout (manual stop via DELETE is unreliable).
|
||||
@@ -105,13 +197,14 @@ Per-group configuration:
|
||||
{
|
||||
channels: {
|
||||
bluebubbles: {
|
||||
sendReadReceipts: false // disable read receipts
|
||||
}
|
||||
}
|
||||
sendReadReceipts: false, // disable read receipts
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced actions
|
||||
|
||||
BlueBubbles supports advanced message actions when enabled in config:
|
||||
|
||||
```json5
|
||||
@@ -119,24 +212,25 @@ BlueBubbles supports advanced message actions when enabled in config:
|
||||
channels: {
|
||||
bluebubbles: {
|
||||
actions: {
|
||||
reactions: true, // tapbacks (default: true)
|
||||
edit: true, // edit sent messages (macOS 13+, broken on macOS 26 Tahoe)
|
||||
unsend: true, // unsend messages (macOS 13+)
|
||||
reply: true, // reply threading by message GUID
|
||||
sendWithEffect: true, // message effects (slam, loud, etc.)
|
||||
renameGroup: true, // rename group chats
|
||||
setGroupIcon: true, // set group chat icon/photo (flaky on macOS 26 Tahoe)
|
||||
addParticipant: true, // add participants to groups
|
||||
reactions: true, // tapbacks (default: true)
|
||||
edit: true, // edit sent messages (macOS 13+, broken on macOS 26 Tahoe)
|
||||
unsend: true, // unsend messages (macOS 13+)
|
||||
reply: true, // reply threading by message GUID
|
||||
sendWithEffect: true, // message effects (slam, loud, etc.)
|
||||
renameGroup: true, // rename group chats
|
||||
setGroupIcon: true, // set group chat icon/photo (flaky on macOS 26 Tahoe)
|
||||
addParticipant: true, // add participants to groups
|
||||
removeParticipant: true, // remove participants from groups
|
||||
leaveGroup: true, // leave group chats
|
||||
sendAttachment: true // send attachments/media
|
||||
}
|
||||
}
|
||||
}
|
||||
leaveGroup: true, // leave group chats
|
||||
sendAttachment: true, // send attachments/media
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Available actions:
|
||||
|
||||
- **react**: Add/remove tapback reactions (`messageId`, `emoji`, `remove`)
|
||||
- **edit**: Edit a sent message (`messageId`, `text`)
|
||||
- **unsend**: Unsend a message (`messageId`)
|
||||
@@ -151,39 +245,47 @@ Available actions:
|
||||
- Voice memos: set `asVoice: true` with **MP3** or **CAF** audio to send as an iMessage voice message. BlueBubbles converts MP3 → CAF when sending voice memos.
|
||||
|
||||
### Message IDs (short vs full)
|
||||
OpenClaw may surface *short* message IDs (e.g., `1`, `2`) to save tokens.
|
||||
|
||||
OpenClaw may surface _short_ message IDs (e.g., `1`, `2`) to save tokens.
|
||||
|
||||
- `MessageSid` / `ReplyToId` can be short IDs.
|
||||
- `MessageSidFull` / `ReplyToIdFull` contain the provider full IDs.
|
||||
- Short IDs are in-memory; they can expire on restart or cache eviction.
|
||||
- Actions accept short or full `messageId`, but short IDs will error if no longer available.
|
||||
|
||||
Use full IDs for durable automations and storage:
|
||||
|
||||
- Templates: `{{MessageSidFull}}`, `{{ReplyToIdFull}}`
|
||||
- Context: `MessageSidFull` / `ReplyToIdFull` in inbound payloads
|
||||
|
||||
See [Configuration](/gateway/configuration) for template variables.
|
||||
|
||||
## Block streaming
|
||||
|
||||
Control whether responses are sent as a single message or streamed in blocks:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
bluebubbles: {
|
||||
blockStreaming: true // enable block streaming (default behavior)
|
||||
}
|
||||
}
|
||||
blockStreaming: true, // enable block streaming (off by default)
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Media + limits
|
||||
|
||||
- Inbound attachments are downloaded and stored in the media cache.
|
||||
- Media cap via `channels.bluebubbles.mediaMaxMb` (default: 8 MB).
|
||||
- Outbound text is chunked to `channels.bluebubbles.textChunkLimit` (default: 4000 chars).
|
||||
|
||||
## Configuration reference
|
||||
|
||||
Full configuration: [Configuration](/gateway/configuration)
|
||||
|
||||
Provider options:
|
||||
|
||||
- `channels.bluebubbles.enabled`: Enable/disable the channel.
|
||||
- `channels.bluebubbles.serverUrl`: BlueBubbles REST API base URL.
|
||||
- `channels.bluebubbles.password`: API password.
|
||||
@@ -194,7 +296,7 @@ Provider options:
|
||||
- `channels.bluebubbles.groupAllowFrom`: Group sender allowlist.
|
||||
- `channels.bluebubbles.groups`: Per-group config (`requireMention`, etc.).
|
||||
- `channels.bluebubbles.sendReadReceipts`: Send read receipts (default: `true`).
|
||||
- `channels.bluebubbles.blockStreaming`: Enable block streaming (default: `true`).
|
||||
- `channels.bluebubbles.blockStreaming`: Enable block streaming (default: `false`; required for streaming replies).
|
||||
- `channels.bluebubbles.textChunkLimit`: Outbound chunk size in chars (default: 4000).
|
||||
- `channels.bluebubbles.chunkMode`: `length` (default) splits only when exceeding `textChunkLimit`; `newline` splits on blank lines (paragraph boundaries) before length chunking.
|
||||
- `channels.bluebubbles.mediaMaxMb`: Inbound media cap in MB (default: 8).
|
||||
@@ -204,11 +306,14 @@ Provider options:
|
||||
- `channels.bluebubbles.accounts`: Multi-account configuration.
|
||||
|
||||
Related global options:
|
||||
|
||||
- `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`).
|
||||
- `messages.responsePrefix`.
|
||||
|
||||
## Addressing / delivery targets
|
||||
|
||||
Prefer `chat_guid` for stable routing:
|
||||
|
||||
- `chat_guid:iMessage;-;+15555550123` (preferred for groups)
|
||||
- `chat_id:123`
|
||||
- `chat_identifier:...`
|
||||
@@ -216,12 +321,14 @@ Prefer `chat_guid` for stable routing:
|
||||
- If a direct handle does not have an existing DM chat, OpenClaw will create one via `POST /api/v1/chat/new`. This requires the BlueBubbles Private API to be enabled.
|
||||
|
||||
## Security
|
||||
|
||||
- Webhook requests are authenticated by comparing `guid`/`password` query params or headers against `channels.bluebubbles.password`. Requests from `localhost` are also accepted.
|
||||
- Keep the API password and webhook endpoint secret (treat them like credentials).
|
||||
- Localhost trust means a same-host reverse proxy can unintentionally bypass the password. If you proxy the gateway, require auth at the proxy and configure `gateway.trustedProxies`. See [Gateway security](/gateway/security#reverse-proxy-configuration).
|
||||
- Enable HTTPS + firewall rules on the BlueBubbles server if exposing it outside your LAN.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- If typing/read events stop working, check the BlueBubbles webhook logs and verify the gateway path matches `channels.bluebubbles.webhookPath`.
|
||||
- Pairing codes expire after one hour; use `openclaw pairing list bluebubbles` and `openclaw pairing approve bluebubbles <code>`.
|
||||
- Reactions require the BlueBubbles private API (`POST /api/v1/message/react`); ensure the server version exposes it.
|
||||
@@ -230,4 +337,4 @@ Prefer `chat_guid` for stable routing:
|
||||
- OpenClaw auto-hides known-broken actions based on the BlueBubbles server's macOS version. If edit still appears on macOS 26 (Tahoe), disable it manually with `channels.bluebubbles.actions.edit=false`.
|
||||
- For status/health info: `openclaw status --all` or `openclaw status --deep`.
|
||||
|
||||
For general channel workflow reference, see [Channels](/channels) and the [Plugins](/plugins) guide.
|
||||
For general channel workflow reference, see [Channels](/channels) and the [Plugins](/tools/plugin) guide.
|
||||
|
||||
@@ -4,6 +4,7 @@ read_when:
|
||||
- Configuring broadcast groups
|
||||
- Debugging multi-agent replies in WhatsApp
|
||||
status: experimental
|
||||
title: "Broadcast Groups"
|
||||
---
|
||||
|
||||
# Broadcast Groups
|
||||
@@ -22,7 +23,9 @@ Broadcast groups are evaluated after channel allowlists and group activation rul
|
||||
## Use Cases
|
||||
|
||||
### 1. Specialized Agent Teams
|
||||
|
||||
Deploy multiple agents with atomic, focused responsibilities:
|
||||
|
||||
```
|
||||
Group: "Development Team"
|
||||
Agents:
|
||||
@@ -35,6 +38,7 @@ Agents:
|
||||
Each agent processes the same message and provides its specialized perspective.
|
||||
|
||||
### 2. Multi-Language Support
|
||||
|
||||
```
|
||||
Group: "International Support"
|
||||
Agents:
|
||||
@@ -44,6 +48,7 @@ Agents:
|
||||
```
|
||||
|
||||
### 3. Quality Assurance Workflows
|
||||
|
||||
```
|
||||
Group: "Customer Support"
|
||||
Agents:
|
||||
@@ -52,6 +57,7 @@ Agents:
|
||||
```
|
||||
|
||||
### 4. Task Automation
|
||||
|
||||
```
|
||||
Group: "Project Management"
|
||||
Agents:
|
||||
@@ -65,6 +71,7 @@ Agents:
|
||||
### Basic Setup
|
||||
|
||||
Add a top-level `broadcast` section (next to `bindings`). Keys are WhatsApp peer ids:
|
||||
|
||||
- group chats: group JID (e.g. `120363403215116621@g.us`)
|
||||
- DMs: E.164 phone number (e.g. `+15551234567`)
|
||||
|
||||
@@ -83,7 +90,9 @@ Add a top-level `broadcast` section (next to `bindings`). Keys are WhatsApp peer
|
||||
Control how agents process messages:
|
||||
|
||||
#### Parallel (Default)
|
||||
|
||||
All agents process simultaneously:
|
||||
|
||||
```json
|
||||
{
|
||||
"broadcast": {
|
||||
@@ -94,7 +103,9 @@ All agents process simultaneously:
|
||||
```
|
||||
|
||||
#### Sequential
|
||||
|
||||
Agents process in order (one waits for previous to finish):
|
||||
|
||||
```json
|
||||
{
|
||||
"broadcast": {
|
||||
@@ -152,7 +163,7 @@ Agents process in order (one waits for previous to finish):
|
||||
4. **If not in broadcast list**:
|
||||
- Normal routing applies (first matching binding)
|
||||
|
||||
Note: broadcast groups do not bypass channel allowlists or group activation rules (mentions/commands/etc). They only change *which agents run* when a message is eligible for processing.
|
||||
Note: broadcast groups do not bypass channel allowlists or group activation rules (mentions/commands/etc). They only change _which agents run_ when a message is eligible for processing.
|
||||
|
||||
### Session Isolation
|
||||
|
||||
@@ -166,6 +177,7 @@ Each agent in a broadcast group maintains completely separate:
|
||||
- **Group context buffer** (recent group messages used for context) is shared per peer, so all broadcast agents see the same context when triggered
|
||||
|
||||
This allows each agent to have:
|
||||
|
||||
- Different personalities
|
||||
- Different tool access (e.g., read-only vs. read-write)
|
||||
- Different models (e.g., opus vs. sonnet)
|
||||
@@ -176,6 +188,7 @@ This allows each agent to have:
|
||||
In group `120363403215116621@g.us` with agents `["alfred", "baerbel"]`:
|
||||
|
||||
**Alfred's context:**
|
||||
|
||||
```
|
||||
Session: agent:alfred:whatsapp:group:120363403215116621@g.us
|
||||
History: [user message, alfred's previous responses]
|
||||
@@ -184,8 +197,9 @@ Tools: read, write, exec
|
||||
```
|
||||
|
||||
**Bärbel's context:**
|
||||
|
||||
```
|
||||
Session: agent:baerbel:whatsapp:group:120363403215116621@g.us
|
||||
Session: agent:baerbel:whatsapp:group:120363403215116621@g.us
|
||||
History: [user message, baerbel's previous responses]
|
||||
Workspace: /Users/pascal/openclaw-baerbel/
|
||||
Tools: read only
|
||||
@@ -230,10 +244,10 @@ Give agents only the tools they need:
|
||||
{
|
||||
"agents": {
|
||||
"reviewer": {
|
||||
"tools": { "allow": ["read", "exec"] } // Read-only
|
||||
"tools": { "allow": ["read", "exec"] } // Read-only
|
||||
},
|
||||
"fixer": {
|
||||
"tools": { "allow": ["read", "write", "edit", "exec"] } // Read-write
|
||||
"tools": { "allow": ["read", "write", "edit", "exec"] } // Read-write
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -242,6 +256,7 @@ Give agents only the tools they need:
|
||||
### 4. Monitor Performance
|
||||
|
||||
With many agents, consider:
|
||||
|
||||
- Using `"strategy": "parallel"` (default) for speed
|
||||
- Limiting broadcast groups to 5-10 agents
|
||||
- Using faster models for simpler agents
|
||||
@@ -260,6 +275,7 @@ Result: Agent A and C respond, Agent B logs error
|
||||
### Providers
|
||||
|
||||
Broadcast groups currently work with:
|
||||
|
||||
- ✅ WhatsApp (implemented)
|
||||
- 🚧 Telegram (planned)
|
||||
- 🚧 Discord (planned)
|
||||
@@ -272,7 +288,10 @@ Broadcast groups work alongside existing routing:
|
||||
```json
|
||||
{
|
||||
"bindings": [
|
||||
{ "match": { "channel": "whatsapp", "peer": { "kind": "group", "id": "GROUP_A" } }, "agentId": "alfred" }
|
||||
{
|
||||
"match": { "channel": "whatsapp", "peer": { "kind": "group", "id": "GROUP_A" } },
|
||||
"agentId": "alfred"
|
||||
}
|
||||
],
|
||||
"broadcast": {
|
||||
"GROUP_B": ["agent1", "agent2"]
|
||||
@@ -290,11 +309,13 @@ Broadcast groups work alongside existing routing:
|
||||
### Agents Not Responding
|
||||
|
||||
**Check:**
|
||||
|
||||
1. Agent IDs exist in `agents.list`
|
||||
2. Peer ID format is correct (e.g., `120363403215116621@g.us`)
|
||||
3. Agents are not in deny lists
|
||||
|
||||
**Debug:**
|
||||
|
||||
```bash
|
||||
tail -f ~/.openclaw/logs/gateway.log | grep broadcast
|
||||
```
|
||||
@@ -308,6 +329,7 @@ tail -f ~/.openclaw/logs/gateway.log | grep broadcast
|
||||
### Performance Issues
|
||||
|
||||
**If slow with many agents:**
|
||||
|
||||
- Reduce number of agents per group
|
||||
- Use lighter models (sonnet instead of opus)
|
||||
- Check sandbox startup time
|
||||
@@ -329,9 +351,21 @@ tail -f ~/.openclaw/logs/gateway.log | grep broadcast
|
||||
},
|
||||
"agents": {
|
||||
"list": [
|
||||
{ "id": "code-formatter", "workspace": "~/agents/formatter", "tools": { "allow": ["read", "write"] } },
|
||||
{ "id": "security-scanner", "workspace": "~/agents/security", "tools": { "allow": ["read", "exec"] } },
|
||||
{ "id": "test-coverage", "workspace": "~/agents/testing", "tools": { "allow": ["read", "exec"] } },
|
||||
{
|
||||
"id": "code-formatter",
|
||||
"workspace": "~/agents/formatter",
|
||||
"tools": { "allow": ["read", "write"] }
|
||||
},
|
||||
{
|
||||
"id": "security-scanner",
|
||||
"workspace": "~/agents/security",
|
||||
"tools": { "allow": ["read", "exec"] }
|
||||
},
|
||||
{
|
||||
"id": "test-coverage",
|
||||
"workspace": "~/agents/testing",
|
||||
"tools": { "allow": ["read", "exec"] }
|
||||
},
|
||||
{ "id": "docs-checker", "workspace": "~/agents/docs", "tools": { "allow": ["read"] } }
|
||||
]
|
||||
}
|
||||
@@ -340,6 +374,7 @@ tail -f ~/.openclaw/logs/gateway.log | grep broadcast
|
||||
|
||||
**User sends:** Code snippet
|
||||
**Responses:**
|
||||
|
||||
- code-formatter: "Fixed indentation and added type hints"
|
||||
- security-scanner: "⚠️ SQL injection vulnerability in line 12"
|
||||
- test-coverage: "Coverage is 45%, missing tests for error cases"
|
||||
@@ -381,7 +416,6 @@ interface OpenClawConfig {
|
||||
- `strategy` (optional): How to process agents
|
||||
- `"parallel"` (default): All agents process simultaneously
|
||||
- `"sequential"`: Agents process in array order
|
||||
|
||||
- `[peerId]`: WhatsApp group JID, E.164 number, or other peer ID
|
||||
- Value: Array of agent IDs that should process messages
|
||||
|
||||
@@ -395,6 +429,7 @@ interface OpenClawConfig {
|
||||
## Future Enhancements
|
||||
|
||||
Planned features:
|
||||
|
||||
- [ ] Shared context mode (agents see each other's responses)
|
||||
- [ ] Agent coordination (agents can signal each other)
|
||||
- [ ] Dynamic agent selection (choose agents based on message content)
|
||||
@@ -402,6 +437,6 @@ Planned features:
|
||||
|
||||
## See Also
|
||||
|
||||
- [Multi-Agent Configuration](/multi-agent-sandbox-tools)
|
||||
- [Routing Configuration](/concepts/channel-routing)
|
||||
- [Multi-Agent Configuration](/tools/multi-agent-sandbox-tools)
|
||||
- [Routing Configuration](/channels/channel-routing)
|
||||
- [Session Management](/concepts/sessions)
|
||||
@@ -2,9 +2,10 @@
|
||||
summary: "Routing rules per channel (WhatsApp, Telegram, Discord, Slack) and shared context"
|
||||
read_when:
|
||||
- Changing channel routing or inbox behavior
|
||||
title: "Channel Routing"
|
||||
---
|
||||
# Channels & routing
|
||||
|
||||
# Channels & routing
|
||||
|
||||
OpenClaw routes replies **back to the channel where a message came from**. The
|
||||
model does not choose a channel; routing is deterministic and controlled by the
|
||||
@@ -62,12 +63,12 @@ Config:
|
||||
broadcast: {
|
||||
strategy: "parallel",
|
||||
"120363403215116621@g.us": ["alfred", "baerbel"],
|
||||
"+15555550123": ["support", "logger"]
|
||||
}
|
||||
"+15555550123": ["support", "logger"],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
See: [Broadcast Groups](/broadcast-groups).
|
||||
See: [Broadcast Groups](/channels/broadcast-groups).
|
||||
|
||||
## Config overview
|
||||
|
||||
@@ -79,14 +80,12 @@ Example:
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
list: [
|
||||
{ id: "support", name: "Support", workspace: "~/.openclaw/workspace-support" }
|
||||
]
|
||||
list: [{ id: "support", name: "Support", workspace: "~/.openclaw/workspace-support" }],
|
||||
},
|
||||
bindings: [
|
||||
{ match: { channel: "slack", teamId: "T123" }, agentId: "support" },
|
||||
{ match: { channel: "telegram", peer: { kind: "group", id: "-100123" } }, agentId: "support" }
|
||||
]
|
||||
{ match: { channel: "telegram", peer: { kind: "group", id: "-100123" } }, agentId: "support" },
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
@@ -108,6 +107,7 @@ agent in one place.
|
||||
## Reply context
|
||||
|
||||
Inbound replies include:
|
||||
|
||||
- `ReplyToId`, `ReplyToBody`, and `ReplyToSender` when available.
|
||||
- Quoted context is appended to `Body` as a `[Replying to ...]` block.
|
||||
|
||||
@@ -2,42 +2,47 @@
|
||||
summary: "Discord bot support status, capabilities, and configuration"
|
||||
read_when:
|
||||
- Working on Discord channel features
|
||||
title: "Discord"
|
||||
---
|
||||
# Discord (Bot API)
|
||||
|
||||
# Discord (Bot API)
|
||||
|
||||
Status: ready for DM and guild text channels via the official Discord bot gateway.
|
||||
|
||||
## Quick setup (beginner)
|
||||
1) Create a Discord bot and copy the bot token.
|
||||
2) In the Discord app settings, enable **Message Content Intent** (and **Server Members Intent** if you plan to use allowlists or name lookups).
|
||||
3) Set the token for OpenClaw:
|
||||
|
||||
1. Create a Discord bot and copy the bot token.
|
||||
2. In the Discord app settings, enable **Message Content Intent** (and **Server Members Intent** if you plan to use allowlists or name lookups).
|
||||
3. Set the token for OpenClaw:
|
||||
- Env: `DISCORD_BOT_TOKEN=...`
|
||||
- Or config: `channels.discord.token: "..."`.
|
||||
- If both are set, config takes precedence (env fallback is default-account only).
|
||||
4) Invite the bot to your server with message permissions (create a private server if you just want DMs).
|
||||
5) Start the gateway.
|
||||
6) DM access is pairing by default; approve the pairing code on first contact.
|
||||
4. Invite the bot to your server with message permissions (create a private server if you just want DMs).
|
||||
5. Start the gateway.
|
||||
6. DM access is pairing by default; approve the pairing code on first contact.
|
||||
|
||||
Minimal config:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
discord: {
|
||||
enabled: true,
|
||||
token: "YOUR_BOT_TOKEN"
|
||||
}
|
||||
}
|
||||
token: "YOUR_BOT_TOKEN",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Goals
|
||||
|
||||
- Talk to OpenClaw via Discord DMs or guild channels.
|
||||
- Direct chats collapse into the agent's main session (default `agent:main:main`); guild channels stay isolated as `agent:<agentId>:discord:channel:<channelId>` (display names use `discord:<guildSlug>#<channelSlug>`).
|
||||
- Group DMs are ignored by default; enable via `channels.discord.dm.groupEnabled` and optionally restrict by `channels.discord.dm.groupChannels`.
|
||||
- Keep routing deterministic: replies always go back to the channel they arrived on.
|
||||
|
||||
## How it works
|
||||
|
||||
1. Create a Discord application → Bot, enable the intents you need (DMs + guild messages + message content), and grab the bot token.
|
||||
2. Invite the bot to your server with the permissions required to read/send messages where you want to use it.
|
||||
3. Configure OpenClaw with `channels.discord.token` (or `DISCORD_BOT_TOKEN` as a fallback).
|
||||
@@ -64,12 +69,14 @@ Note: Slugs are lowercase with spaces replaced by `-`. Channel names are slugged
|
||||
Note: Guild context `[from:]` lines include `author.tag` + `id` to make ping-ready replies easy.
|
||||
|
||||
## Config writes
|
||||
|
||||
By default, Discord is allowed to write config updates triggered by `/config set|unset` (requires `commands.config: true`).
|
||||
|
||||
Disable with:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: { discord: { configWrites: false } }
|
||||
channels: { discord: { configWrites: false } },
|
||||
}
|
||||
```
|
||||
|
||||
@@ -78,28 +85,34 @@ Disable with:
|
||||
This is the “Discord Developer Portal” setup for running OpenClaw in a server (guild) channel like `#help`.
|
||||
|
||||
### 1) Create the Discord app + bot user
|
||||
|
||||
1. Discord Developer Portal → **Applications** → **New Application**
|
||||
2. In your app:
|
||||
- **Bot** → **Add Bot**
|
||||
- Copy the **Bot Token** (this is what you put in `DISCORD_BOT_TOKEN`)
|
||||
|
||||
### 2) Enable the gateway intents OpenClaw needs
|
||||
|
||||
Discord blocks “privileged intents” unless you explicitly enable them.
|
||||
|
||||
In **Bot** → **Privileged Gateway Intents**, enable:
|
||||
|
||||
- **Message Content Intent** (required to read message text in most guilds; without it you’ll see “Used disallowed intents” or the bot will connect but not react to messages)
|
||||
- **Server Members Intent** (recommended; required for some member/user lookups and allowlist matching in guilds)
|
||||
|
||||
You usually do **not** need **Presence Intent**.
|
||||
You usually do **not** need **Presence Intent**. Setting the bot's own presence (`setPresence` action) uses gateway OP3 and does not require this intent; it is only needed if you want to receive presence updates about other guild members.
|
||||
|
||||
### 3) Generate an invite URL (OAuth2 URL Generator)
|
||||
|
||||
In your app: **OAuth2** → **URL Generator**
|
||||
|
||||
**Scopes**
|
||||
|
||||
- ✅ `bot`
|
||||
- ✅ `applications.commands` (required for native commands)
|
||||
|
||||
**Bot Permissions** (minimal baseline)
|
||||
|
||||
- ✅ View Channels
|
||||
- ✅ Send Messages
|
||||
- ✅ Read Message History
|
||||
@@ -113,6 +126,7 @@ Avoid **Administrator** unless you’re debugging and fully trust the bot.
|
||||
Copy the generated URL, open it, pick your server, and install the bot.
|
||||
|
||||
### 4) Get the ids (guild/user/channel)
|
||||
|
||||
Discord uses numeric ids everywhere; OpenClaw config prefers ids.
|
||||
|
||||
1. Discord (desktop/web) → **User Settings** → **Advanced** → enable **Developer Mode**
|
||||
@@ -124,7 +138,9 @@ Discord uses numeric ids everywhere; OpenClaw config prefers ids.
|
||||
### 5) Configure OpenClaw
|
||||
|
||||
#### Token
|
||||
|
||||
Set the bot token via env var (recommended on servers):
|
||||
|
||||
- `DISCORD_BOT_TOKEN=...`
|
||||
|
||||
Or via config:
|
||||
@@ -134,15 +150,16 @@ Or via config:
|
||||
channels: {
|
||||
discord: {
|
||||
enabled: true,
|
||||
token: "YOUR_BOT_TOKEN"
|
||||
}
|
||||
}
|
||||
token: "YOUR_BOT_TOKEN",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Multi-account support: use `channels.discord.accounts` with per-account tokens and optional `name`. See [`gateway/configuration`](/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern.
|
||||
|
||||
#### Allowlist + channel routing
|
||||
|
||||
Example “single server, only allow me, only allow #help”:
|
||||
|
||||
```json5
|
||||
@@ -152,41 +169,45 @@ Example “single server, only allow me, only allow #help”:
|
||||
enabled: true,
|
||||
dm: { enabled: false },
|
||||
guilds: {
|
||||
"YOUR_GUILD_ID": {
|
||||
YOUR_GUILD_ID: {
|
||||
users: ["YOUR_USER_ID"],
|
||||
requireMention: true,
|
||||
channels: {
|
||||
help: { allow: true, requireMention: true }
|
||||
}
|
||||
}
|
||||
help: { allow: true, requireMention: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
retry: {
|
||||
attempts: 3,
|
||||
minDelayMs: 500,
|
||||
maxDelayMs: 30000,
|
||||
jitter: 0.1
|
||||
}
|
||||
}
|
||||
}
|
||||
jitter: 0.1,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `requireMention: true` means the bot only replies when mentioned (recommended for shared channels).
|
||||
- `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`) also count as mentions for guild messages.
|
||||
- Multi-agent override: set per-agent patterns on `agents.list[].groupChat.mentionPatterns`.
|
||||
- If `channels` is present, any channel not listed is denied by default.
|
||||
- Use a `"*"` channel entry to apply defaults across all channels; explicit channel entries override the wildcard.
|
||||
- Threads inherit parent channel config (allowlist, `requireMention`, skills, prompts, etc.) unless you add the thread channel id explicitly.
|
||||
- Owner hint: when a per-guild or per-channel `users` allowlist matches the sender, OpenClaw treats that sender as the owner in the system prompt. For a global owner across channels, set `commands.ownerAllowFrom`.
|
||||
- Bot-authored messages are ignored by default; set `channels.discord.allowBots=true` to allow them (own messages remain filtered).
|
||||
- Warning: If you allow replies to other bots (`channels.discord.allowBots=true`), prevent bot-to-bot reply loops with `requireMention`, `channels.discord.guilds.*.channels.<id>.users` allowlists, and/or clear guardrails in `AGENTS.md` and `SOUL.md`.
|
||||
|
||||
### 6) Verify it works
|
||||
|
||||
1. Start the gateway.
|
||||
2. In your server channel, send: `@Krill hello` (or whatever your bot name is).
|
||||
3. If nothing happens: check **Troubleshooting** below.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
- First: run `openclaw doctor` and `openclaw channels status --probe` (actionable warnings + quick audits).
|
||||
- **“Used disallowed intents”**: enable **Message Content Intent** (and likely **Server Members Intent**) in the Developer Portal, then restart the gateway.
|
||||
- **Bot connects but never replies in a guild channel**:
|
||||
@@ -202,8 +223,14 @@ Notes:
|
||||
- `requireMention` must live under `channels.discord.guilds` (or a specific channel). `channels.discord.requireMention` at the top level is ignored.
|
||||
- **Permission audits** (`channels status --probe`) only check numeric channel IDs. If you use slugs/names as `channels.discord.guilds.*.channels` keys, the audit can’t verify permissions.
|
||||
- **DMs don’t work**: `channels.discord.dm.enabled=false`, `channels.discord.dm.policy="disabled"`, or you haven’t been approved yet (`channels.discord.dm.policy="pairing"`).
|
||||
- **Exec approvals in Discord**: Discord supports a **button UI** for exec approvals in DMs (Allow once / Always allow / Deny). `/approve <id> ...` is only for forwarded approvals and won’t resolve Discord’s button prompts. If you see `❌ Failed to submit approval: Error: unknown approval id` or the UI never shows up, check:
|
||||
- `channels.discord.execApprovals.enabled: true` in your config.
|
||||
- Your Discord user ID is listed in `channels.discord.execApprovals.approvers` (the UI is only sent to approvers).
|
||||
- Use the buttons in the DM prompt (**Allow once**, **Always allow**, **Deny**).
|
||||
- See [Exec approvals](/tools/exec-approvals) and [Slash commands](/tools/slash-commands) for the broader approvals and command flow.
|
||||
|
||||
## Capabilities & limits
|
||||
|
||||
- DMs and guild text channels (threads are treated as separate channels; voice not supported).
|
||||
- Typing indicators sent best-effort; message chunking uses `channels.discord.textChunkLimit` (default 2000) and splits tall replies by line count (`channels.discord.maxLinesPerMessage`, default 17).
|
||||
- Optional newline chunking: set `channels.discord.chunkMode="newline"` to split on blank lines (paragraph boundaries) before length chunking.
|
||||
@@ -213,6 +240,7 @@ Notes:
|
||||
- Native reply threading is **off by default**; enable with `channels.discord.replyToMode` and reply tags.
|
||||
|
||||
## Retry policy
|
||||
|
||||
Outbound Discord API calls retry on rate limits (429) using Discord `retry_after` when available, with exponential backoff and jitter. Configure via `channels.discord.retry`. See [Retry policy](/concepts/retry).
|
||||
|
||||
## Config
|
||||
@@ -227,9 +255,9 @@ Outbound Discord API calls retry on rate limits (429) using Discord `retry_after
|
||||
guilds: {
|
||||
"*": {
|
||||
channels: {
|
||||
general: { allow: true }
|
||||
}
|
||||
}
|
||||
general: { allow: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
mediaMaxMb: 8,
|
||||
actions: {
|
||||
@@ -250,7 +278,8 @@ Outbound Discord API calls retry on rate limits (429) using Discord `retry_after
|
||||
channels: true,
|
||||
voiceStatus: true,
|
||||
events: true,
|
||||
moderation: false
|
||||
moderation: false,
|
||||
presence: false,
|
||||
},
|
||||
replyToMode: "off",
|
||||
dm: {
|
||||
@@ -258,7 +287,7 @@ Outbound Discord API calls retry on rate limits (429) using Discord `retry_after
|
||||
policy: "pairing", // pairing | allowlist | open | disabled
|
||||
allowFrom: ["123456789012345678", "steipete"],
|
||||
groupEnabled: false,
|
||||
groupChannels: ["openclaw-dm"]
|
||||
groupChannels: ["openclaw-dm"],
|
||||
},
|
||||
guilds: {
|
||||
"*": { requireMention: true },
|
||||
@@ -274,13 +303,13 @@ Outbound Discord API calls retry on rate limits (429) using Discord `retry_after
|
||||
requireMention: true,
|
||||
users: ["987654321098765432"],
|
||||
skills: ["search", "docs"],
|
||||
systemPrompt: "Keep answers short."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
systemPrompt: "Keep answers short.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@@ -306,7 +335,7 @@ ack reaction after the bot replies.
|
||||
- `guilds.<id>.channels.<channel>.toolsBySender`: optional per-sender tool policy overrides within the channel (`"*"` wildcard supported).
|
||||
- `guilds.<id>.channels.<channel>.users`: optional per-channel user allowlist.
|
||||
- `guilds.<id>.channels.<channel>.skills`: skill filter (omit = all skills, empty = none).
|
||||
- `guilds.<id>.channels.<channel>.systemPrompt`: extra system prompt for the channel (combined with channel topic).
|
||||
- `guilds.<id>.channels.<channel>.systemPrompt`: extra system prompt for the channel. Discord channel topics are injected as **untrusted** context (not system prompt).
|
||||
- `guilds.<id>.channels.<channel>.enabled`: set `false` to disable the channel.
|
||||
- `guilds.<id>.channels`: channel rules (keys are channel slugs or ids).
|
||||
- `guilds.<id>.requireMention`: per-guild mention requirement (overridable per channel).
|
||||
@@ -318,6 +347,7 @@ ack reaction after the bot replies.
|
||||
- `historyLimit`: number of recent guild messages to include as context when replying to a mention (default 20; falls back to `messages.groupChat.historyLimit`; `0` disables).
|
||||
- `dmHistoryLimit`: DM history limit in user turns. Per-user overrides: `dms["<user_id>"].historyLimit`.
|
||||
- `retry`: retry policy for outbound Discord API calls (attempts, minDelayMs, maxDelayMs, jitter).
|
||||
- `pluralkit`: resolve PluralKit proxied messages so system members appear as distinct senders.
|
||||
- `actions`: per-action tool gates; omit to allow all (set `false` to disable).
|
||||
- `reactions` (covers react + read reactions)
|
||||
- `stickers`, `emojiUploads`, `stickerUploads`, `polls`, `permissions`, `messages`, `threads`, `pins`, `search`
|
||||
@@ -325,49 +355,86 @@ ack reaction after the bot replies.
|
||||
- `channels` (create/edit/delete channels + categories + permissions)
|
||||
- `roles` (role add/remove, default `false`)
|
||||
- `moderation` (timeout/kick/ban, default `false`)
|
||||
- `presence` (bot status/activity, default `false`)
|
||||
- `execApprovals`: Discord-only exec approval DMs (button UI). Supports `enabled`, `approvers`, `agentFilter`, `sessionFilter`.
|
||||
|
||||
Reaction notifications use `guilds.<id>.reactionNotifications`:
|
||||
|
||||
- `off`: no reaction events.
|
||||
- `own`: reactions on the bot's own messages (default).
|
||||
- `all`: all reactions on all messages.
|
||||
- `allowlist`: reactions from `guilds.<id>.users` on all messages (empty list disables).
|
||||
|
||||
### PluralKit (PK) support
|
||||
|
||||
Enable PK lookups so proxied messages resolve to the underlying system + member.
|
||||
When enabled, OpenClaw uses the member identity for allowlists and labels the
|
||||
sender as `Member (PK:System)` to avoid accidental Discord pings.
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
discord: {
|
||||
pluralkit: {
|
||||
enabled: true,
|
||||
token: "pk_live_...", // optional; required for private systems
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Allowlist notes (PK-enabled):
|
||||
|
||||
- Use `pk:<memberId>` in `dm.allowFrom`, `guilds.<id>.users`, or per-channel `users`.
|
||||
- Member display names are also matched by name/slug.
|
||||
- Lookups use the **original** Discord message ID (the pre-proxy message), so
|
||||
the PK API only resolves it within its 30-minute window.
|
||||
- If PK lookups fail (e.g., private system without a token), proxied messages
|
||||
are treated as bot messages and are dropped unless `channels.discord.allowBots=true`.
|
||||
|
||||
### Tool action defaults
|
||||
|
||||
| Action group | Default | Notes |
|
||||
| --- | --- | --- |
|
||||
| reactions | enabled | React + list reactions + emojiList |
|
||||
| stickers | enabled | Send stickers |
|
||||
| emojiUploads | enabled | Upload emojis |
|
||||
| stickerUploads | enabled | Upload stickers |
|
||||
| polls | enabled | Create polls |
|
||||
| permissions | enabled | Channel permission snapshot |
|
||||
| messages | enabled | Read/send/edit/delete |
|
||||
| threads | enabled | Create/list/reply |
|
||||
| pins | enabled | Pin/unpin/list |
|
||||
| search | enabled | Message search (preview feature) |
|
||||
| memberInfo | enabled | Member info |
|
||||
| roleInfo | enabled | Role list |
|
||||
| channelInfo | enabled | Channel info + list |
|
||||
| channels | enabled | Channel/category management |
|
||||
| voiceStatus | enabled | Voice state lookup |
|
||||
| events | enabled | List/create scheduled events |
|
||||
| roles | disabled | Role add/remove |
|
||||
| moderation | disabled | Timeout/kick/ban |
|
||||
| Action group | Default | Notes |
|
||||
| -------------- | -------- | ---------------------------------- |
|
||||
| reactions | enabled | React + list reactions + emojiList |
|
||||
| stickers | enabled | Send stickers |
|
||||
| emojiUploads | enabled | Upload emojis |
|
||||
| stickerUploads | enabled | Upload stickers |
|
||||
| polls | enabled | Create polls |
|
||||
| permissions | enabled | Channel permission snapshot |
|
||||
| messages | enabled | Read/send/edit/delete |
|
||||
| threads | enabled | Create/list/reply |
|
||||
| pins | enabled | Pin/unpin/list |
|
||||
| search | enabled | Message search (preview feature) |
|
||||
| memberInfo | enabled | Member info |
|
||||
| roleInfo | enabled | Role list |
|
||||
| channelInfo | enabled | Channel info + list |
|
||||
| channels | enabled | Channel/category management |
|
||||
| voiceStatus | enabled | Voice state lookup |
|
||||
| events | enabled | List/create scheduled events |
|
||||
| roles | disabled | Role add/remove |
|
||||
| moderation | disabled | Timeout/kick/ban |
|
||||
| presence | disabled | Bot status/activity (setPresence) |
|
||||
|
||||
- `replyToMode`: `off` (default), `first`, or `all`. Applies only when the model includes a reply tag.
|
||||
|
||||
## Reply tags
|
||||
|
||||
To request a threaded reply, the model can include one tag in its output:
|
||||
|
||||
- `[[reply_to_current]]` — reply to the triggering Discord message.
|
||||
- `[[reply_to:<id>]]` — reply to a specific message id from context/history.
|
||||
Current message ids are appended to prompts as `[message_id: …]`; history entries already include ids.
|
||||
Current message ids are appended to prompts as `[message_id: …]`; history entries already include ids.
|
||||
|
||||
Behavior is controlled by `channels.discord.replyToMode`:
|
||||
|
||||
- `off`: ignore tags.
|
||||
- `first`: only the first outbound chunk/attachment is a reply.
|
||||
- `all`: every outbound chunk/attachment is a reply.
|
||||
|
||||
Allowlist matching notes:
|
||||
|
||||
- `allowFrom`/`users`/`groupChannels` accept ids, names, tags, or mentions like `<@id>`.
|
||||
- Prefixes like `discord:`/`user:` (users) and `channel:` (group DMs) are supported.
|
||||
- Use `*` to allow any sender/channel.
|
||||
@@ -379,12 +446,15 @@ Allowlist matching notes:
|
||||
and logs the mapping; unresolved entries are kept as typed.
|
||||
|
||||
Native command notes:
|
||||
|
||||
- The registered commands mirror OpenClaw’s chat commands.
|
||||
- Native commands honor the same allowlists as DMs/guild messages (`channels.discord.dm.allowFrom`, `channels.discord.guilds`, per-channel rules).
|
||||
- Slash commands may still be visible in Discord UI to users who aren’t allowlisted; OpenClaw enforces allowlists on execution and replies “not authorized”.
|
||||
|
||||
## Tool actions
|
||||
|
||||
The agent can call `discord` with actions like:
|
||||
|
||||
- `react` / `reactions` (add or list reactions)
|
||||
- `sticker`, `poll`, `permissions`
|
||||
- `readMessages`, `sendMessage`, `editMessage`, `deleteMessage`
|
||||
@@ -394,11 +464,13 @@ The agent can call `discord` with actions like:
|
||||
- `searchMessages`, `memberInfo`, `roleInfo`, `roleAdd`, `roleRemove`, `emojiList`
|
||||
- `channelInfo`, `channelList`, `voiceStatus`, `eventList`, `eventCreate`
|
||||
- `timeout`, `kick`, `ban`
|
||||
- `setPresence` (bot activity and online status)
|
||||
|
||||
Discord message ids are surfaced in the injected context (`[discord message id: …]` and history lines) so the agent can target them.
|
||||
Emoji can be unicode (e.g., `✅`) or custom emoji syntax like `<:party_blob:1234567890>`.
|
||||
|
||||
## Safety & ops
|
||||
|
||||
- Treat the bot token like a password; prefer the `DISCORD_BOT_TOKEN` env var on supervised hosts or lock down the config file permissions.
|
||||
- Only grant the bot permissions it needs (typically Read/Send Messages).
|
||||
- If the bot is stuck or rate limited, restart the gateway (`openclaw gateway --force`) after confirming no other processes own the Discord session.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user