Compare commits
666 commits
@tpmjs/too
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae0d5e37cf | ||
|
|
4fa8344b34 | ||
|
|
66fb7ef226 | ||
|
|
5ac3beab37 | ||
|
|
894f9842d1 | ||
|
|
dda28d642c | ||
|
|
e42bbdff34 | ||
|
|
1cd44b4e97 | ||
|
|
c8e8a7d9b4 | ||
|
|
cc69d98b6f | ||
|
|
7327992a9d | ||
|
|
6eb0ce7e23 | ||
|
|
022ecda6bf | ||
|
|
7dca00b5da | ||
|
|
2a2f0f487d | ||
|
|
198f9f7d1e | ||
|
|
9ec8bf2454 | ||
|
|
293fe08910 | ||
|
|
15fc413f9c | ||
|
|
a9e01fe772 | ||
|
|
36598fec61 | ||
|
|
99017322ba | ||
|
|
bd232a3407 | ||
|
|
727a44af63 | ||
|
|
4bcdab09f2 | ||
|
|
a52d32c367 | ||
|
|
f3a46045ba | ||
|
|
54bfbff71c | ||
|
|
90d575797f | ||
|
|
0d30a9cabb | ||
|
|
86e523f3dc | ||
|
|
ce44aeab3a | ||
|
|
1675e6ce6c | ||
|
|
0f7e5a3ace | ||
|
|
fa4e7754e6 | ||
|
|
32c6e097ed | ||
|
|
760cc4b77e | ||
|
|
b092ca490b | ||
|
|
ffc6ddcdbb | ||
|
|
3f62228c56 | ||
|
|
ee066a20ca | ||
|
|
f9c5d903a1 | ||
|
|
211be5d197 | ||
|
|
59eefe8d55 | ||
|
|
0e5815e4ce | ||
|
|
2df4b53354 | ||
|
|
13e4fd954d | ||
|
|
19417964de | ||
|
|
8542e3d7d5 | ||
|
|
16c7b8155e | ||
|
|
9fc928adae | ||
|
|
88e66d54ce | ||
|
|
4e3cb78ba4 | ||
|
|
c51fee1484 | ||
|
|
67fa89b1fe | ||
|
|
5926373be6 | ||
|
|
9c6a09e747 | ||
|
|
dff523fa99 | ||
|
|
69162682d1 | ||
|
|
88c4848ab6 | ||
|
|
bfd45341bb | ||
|
|
209fd275f3 | ||
|
|
b5731cf1a1 | ||
|
|
9995d73052 | ||
|
|
9f0d7a2b17 | ||
|
|
234548c6b2 | ||
|
|
a44f38eda9 | ||
|
|
9c731d582e | ||
|
|
1118463e6b | ||
|
|
0489646e55 | ||
|
|
ca6f908ebd | ||
|
|
fb06055f80 | ||
|
|
c4ea363497 | ||
|
|
c4430fb1bf | ||
|
|
360e9ab2bf | ||
|
|
0d3395b32f | ||
|
|
19a905d9d1 | ||
|
|
3259e038fb | ||
|
|
d2740c3f69 | ||
|
|
f0b55a23f9 | ||
|
|
16c3b0df10 | ||
|
|
43c8a57c26 | ||
|
|
ab1133d202 | ||
|
|
a6681c5930 | ||
|
|
5d00f2a711 | ||
|
|
514ea3c0db | ||
|
|
9ab8946c9d | ||
|
|
c5a17a0a69 | ||
|
|
ad974239fd | ||
|
|
474e7f9cda | ||
|
|
a7c7c08f44 | ||
|
|
48c41e8733 | ||
|
|
5fafc90e3e | ||
|
|
92e4f5c467 | ||
|
|
192284ad07 | ||
|
|
01b7295daa | ||
|
|
f5d7364c96 | ||
|
|
ffc5807ba0 | ||
|
|
a3436ca5a5 | ||
|
|
cf2fd3ad9e | ||
|
|
8964e0c237 | ||
|
|
2d939ea11b | ||
|
|
474e11fc13 | ||
|
|
c1af7a2bde | ||
|
|
b71fe338c5 | ||
|
|
a8fe178dc6 | ||
|
|
36d8ec6f09 | ||
|
|
e079959cd8 | ||
|
|
f3a73f1457 | ||
|
|
ef95e10a1e | ||
|
|
3acb6f49e5 | ||
|
|
c56534a4ae | ||
|
|
ac6fdb87e7 | ||
|
|
3c5c218207 | ||
|
|
dc684d3b0e | ||
|
|
ad2f8a629b | ||
|
|
ee3dd3dcd0 | ||
|
|
a1f45fa33a | ||
|
|
c52e6eaf24 | ||
|
|
ea742c9386 | ||
|
|
154f000505 | ||
|
|
aa438078f9 | ||
|
|
028c87056a | ||
|
|
5abc85dafc | ||
|
|
69cec40fe1 | ||
|
|
21b2c46516 | ||
|
|
08cd131088 | ||
|
|
c677110bfe | ||
|
|
257aa55282 | ||
|
|
7e401c82eb | ||
|
|
7f907660c8 | ||
|
|
3cd2fc9674 | ||
|
|
1234b383e7 | ||
|
|
aad8b58031 | ||
|
|
6fbb104380 | ||
|
|
9ebb5ee02e | ||
|
|
0fa4374d9e | ||
|
|
c46a02247a | ||
|
|
ba451a6b4b | ||
|
|
3cde10faf4 | ||
|
|
4973679cbf | ||
|
|
2f02f0f412 | ||
|
|
f943e8e685 | ||
|
|
860d13f44f | ||
|
|
f8302c92d6 | ||
|
|
3e0923fc4a | ||
|
|
301d08303e | ||
|
|
5014822b67 | ||
|
|
014da0171e | ||
|
|
64ee34d35f | ||
|
|
de26322e18 | ||
|
|
cdadbc86d3 | ||
|
|
77204577c1 | ||
|
|
a7a0cbb988 | ||
|
|
839e1b3bef | ||
|
|
23d5159b28 | ||
|
|
7d9321d9c6 | ||
|
|
fdb61ed010 | ||
|
|
82a734b3bd | ||
|
|
fd87edd9e9 | ||
|
|
baea035adc | ||
|
|
0489f5cfdb | ||
|
|
7483e697fc | ||
|
|
76955bde18 | ||
|
|
0770d88815 | ||
|
|
144f07ea75 | ||
|
|
f0d271720c | ||
|
|
499a38ce1b | ||
|
|
066e599293 | ||
|
|
46b212e65a | ||
|
|
3324034e6f | ||
|
|
68480758f7 | ||
|
|
f0b247f449 | ||
|
|
55bd27a504 | ||
|
|
3b5bc83c52 | ||
|
|
3456b26c9d | ||
|
|
c21ed5b41e | ||
|
|
cae05f1504 | ||
|
|
44606e2b78 | ||
|
|
045ad36d14 | ||
|
|
c5eca8fdeb | ||
|
|
ad46943270 | ||
|
|
4461bf70c8 | ||
|
|
54d39c38fa | ||
|
|
4d265cd24d | ||
|
|
1c903cd26e | ||
|
|
27e7d45646 | ||
|
|
e4c09a475f | ||
|
|
e6857e245e | ||
|
|
a548edc0db | ||
|
|
c1655dd318 | ||
|
|
2c3cea8730 | ||
|
|
66670bc9fb | ||
|
|
69ae0dda2f | ||
|
|
b5fa46f6be | ||
|
|
ce3596ea22 | ||
|
|
0c78c1bf1f | ||
|
|
2cd2b10cd0 | ||
|
|
b1dd3371cd | ||
|
|
d22d0e6f59 | ||
|
|
1c3855fa4f | ||
|
|
3d021e079f | ||
|
|
c0511f3628 | ||
|
|
3955cde54b | ||
|
|
bcfa99677c | ||
|
|
f8bfbc3ed2 | ||
|
|
9e1fe2be43 | ||
|
|
a7b4c31881 | ||
|
|
e73b7955a3 | ||
|
|
faf4b622c7 | ||
|
|
d64983163c | ||
|
|
aefb23f318 | ||
|
|
002a537764 | ||
|
|
e0aa8f9619 | ||
|
|
1e49e5f9a5 | ||
|
|
366c1ca147 | ||
|
|
d364bea497 | ||
|
|
232980e5bc | ||
|
|
fefc316038 | ||
|
|
77e697db56 | ||
|
|
0e6be48fdc | ||
|
|
5aad08a3b8 | ||
|
|
907ac2301b | ||
|
|
a8a52c972c | ||
|
|
0f37e64783 | ||
|
|
83e2cededa | ||
|
|
8219f0dde5 | ||
|
|
0fc39da6c9 | ||
|
|
56c4df74b7 | ||
|
|
6880e78f75 | ||
|
|
d524d2ca16 | ||
|
|
b6d507fcf5 | ||
|
|
72ff35b333 | ||
|
|
a39b68e866 | ||
|
|
2fb790a847 | ||
|
|
2b4526ba95 | ||
|
|
3fe93b58d7 | ||
|
|
efb30f7874 | ||
|
|
cd5eaeaf59 | ||
|
|
74a9de290e | ||
|
|
6c781c0711 | ||
|
|
390194265b | ||
|
|
bf97513a59 | ||
|
|
95cb0a96f0 | ||
|
|
e2bb06df60 | ||
|
|
790a338f4d | ||
|
|
a3f1f3935e | ||
|
|
b663ca3e05 | ||
|
|
b4be05e004 | ||
|
|
c8c1a12f22 | ||
|
|
490d76a50b | ||
|
|
b87d5f627c | ||
|
|
c8abfb8968 | ||
|
|
41cbeefabb | ||
|
|
8bbde047b5 | ||
|
|
938a382aab | ||
|
|
079c328bfc | ||
|
|
74c7e63b9f | ||
|
|
0012eba137 | ||
|
|
536b727fc6 | ||
|
|
5c0b9c5ae9 | ||
|
|
49af625254 | ||
|
|
d95a7119c7 | ||
|
|
6661957111 | ||
|
|
d646e2310e | ||
|
|
55ec58065e | ||
|
|
6714f9dc63 | ||
|
|
e5e8750613 | ||
|
|
6bcbce8d1d | ||
|
|
5f48cb50df | ||
|
|
f0177a4a17 | ||
|
|
93cdb60a45 | ||
|
|
a5630e4f41 | ||
|
|
b889d64faf | ||
|
|
52c7d8d844 | ||
|
|
5e0651b987 | ||
|
|
5c9f1a1d0a | ||
|
|
cb157611df | ||
|
|
bc5eb61086 | ||
|
|
849802bfeb | ||
|
|
8c6185e6bb | ||
|
|
ad3457ab32 | ||
|
|
4c5972adc4 | ||
|
|
9b6a4a625e | ||
|
|
3612d12c90 | ||
|
|
9c52203e54 | ||
|
|
9d157800b9 | ||
|
|
9bcc1a8c15 | ||
|
|
e2153120f9 | ||
|
|
3cd65587c5 | ||
|
|
d65031bbeb | ||
|
|
7435a39a3d | ||
|
|
33287d931f | ||
|
|
f86ac2c6b0 | ||
|
|
846676ba15 | ||
|
|
300083aff9 | ||
|
|
346a3f6da0 | ||
|
|
b6a553341a | ||
|
|
bc36d366bc | ||
|
|
84d894e920 | ||
|
|
346b0e83ea | ||
|
|
eaaa40f130 | ||
|
|
1ff0e49e41 | ||
|
|
6f44a021cc | ||
|
|
00b79c1f1a | ||
|
|
337d70f663 | ||
|
|
836ebbf828 | ||
|
|
e02fbc3cd7 | ||
|
|
6f8617fead | ||
|
|
d6de3e025a | ||
|
|
9f254a5ae8 | ||
|
|
f7a6df06c5 | ||
|
|
dc5a2292fc | ||
|
|
cefd237005 | ||
|
|
f40fedae8d | ||
|
|
3bad010833 | ||
|
|
1594f98cc1 | ||
|
|
2c0e5f99d8 | ||
|
|
8e572714ae | ||
|
|
d24be7acbf | ||
|
|
2fe2fcef5c | ||
|
|
4b392a2eed | ||
|
|
361a49f3d7 | ||
|
|
54dedc3056 | ||
|
|
b8b4e44b50 | ||
|
|
114798ce9e | ||
|
|
fd4d78ae09 | ||
|
|
dc1397aa1f | ||
|
|
9bb856563e | ||
|
|
d99f810572 | ||
|
|
3f5c1bda61 | ||
|
|
d156b29286 | ||
|
|
42dac85d92 | ||
|
|
be4e802e7e | ||
|
|
d0e304cdab | ||
|
|
1efb9b0003 | ||
|
|
a137afd65d | ||
|
|
0730ae6f42 | ||
|
|
3dd0caf621 | ||
|
|
287d84163f | ||
|
|
152a076580 | ||
|
|
1afe5bbc19 | ||
|
|
0cbc3e0dd4 | ||
|
|
53ae105602 | ||
|
|
670e3e3ca3 | ||
|
|
724bf18822 | ||
|
|
a4cfea5cc3 | ||
|
|
bc1eb57e2a | ||
|
|
00908118b3 | ||
|
|
f519952441 | ||
|
|
edcff21be7 | ||
|
|
6b48ab224d | ||
|
|
e6bc107812 | ||
|
|
96fb52d047 | ||
|
|
515a461bbf | ||
|
|
3f15153dcf | ||
|
|
aba7c976c1 | ||
|
|
fbcd7150c9 | ||
|
|
d2c149c6be | ||
|
|
c0f9e9884d | ||
|
|
3a0a125b6d | ||
|
|
9371a11eb0 | ||
|
|
1469e36f3f | ||
|
|
1d771efef6 | ||
|
|
19884af03e | ||
|
|
2a6d4c6c2e | ||
|
|
471fbba786 | ||
|
|
cc3a991661 | ||
|
|
1b1c4f6474 | ||
|
|
4f95046249 | ||
|
|
5a0a21fd60 | ||
|
|
bfed830187 | ||
|
|
0b1bb7725d | ||
|
|
552f319583 | ||
|
|
4cbd84edb8 | ||
|
|
9c4004628f | ||
|
|
698165c0d7 | ||
|
|
2e37faf385 | ||
|
|
8230ab0eec | ||
|
|
828801f463 | ||
|
|
abd9682f5e | ||
|
|
137de1c353 | ||
|
|
f8740fb7ff | ||
|
|
e4b84fb1cd | ||
|
|
87a9517ec3 | ||
|
|
499f163c31 | ||
|
|
522c198041 | ||
|
|
4451eddc91 | ||
|
|
1c9a328929 | ||
|
|
b57d21ca65 | ||
|
|
4f417af2ab | ||
|
|
de41d42349 | ||
|
|
1d417d9a22 | ||
|
|
25d74e5c2e | ||
|
|
59ff53c5ba | ||
|
|
763650e976 | ||
|
|
ed3a215ee8 | ||
|
|
6060492169 | ||
|
|
09fd0a5833 | ||
|
|
d1aad1b3ea | ||
|
|
d00ab8afbb | ||
|
|
1560f38f85 | ||
|
|
03405ad754 | ||
|
|
ba633df0a5 | ||
|
|
8569def0e9 | ||
|
|
de97175d6c | ||
|
|
a42ec19b0b | ||
|
|
21f1818070 | ||
|
|
579aa99c56 | ||
|
|
0d93366659 | ||
|
|
17b9ffb6aa | ||
|
|
80bef7d84c | ||
|
|
5d2096fb5d | ||
|
|
2d9b06020d | ||
|
|
16004298b2 | ||
|
|
3519008631 | ||
|
|
2e6cc4642a | ||
|
|
be5421063a | ||
|
|
36a0735ab1 | ||
|
|
409d1232a6 | ||
|
|
913ce4ffe9 | ||
|
|
fd4520dbf6 | ||
|
|
8f4fd77d79 | ||
|
|
82efaf6d59 | ||
|
|
979bb92aff | ||
|
|
22a13166b2 | ||
|
|
ae5b3e6249 | ||
|
|
c2d244a5c9 | ||
|
|
1c8b516db8 | ||
|
|
26421be4e6 | ||
|
|
369979d3c8 | ||
|
|
7b575a87cc | ||
|
|
2fb63ad506 | ||
|
|
5f7a9881ea | ||
|
|
fada29ecbe | ||
|
|
4a13709a3d | ||
|
|
e298109e94 | ||
|
|
70d0c5ba94 | ||
|
|
b279be2a25 | ||
|
|
a57af97585 | ||
|
|
29e1b9d9ac | ||
|
|
f86794e463 | ||
|
|
1efb2623b3 | ||
|
|
289544f101 | ||
|
|
c8806c49aa | ||
|
|
fb8cce3b38 | ||
|
|
afc97c89a3 | ||
|
|
effd242b07 | ||
|
|
597fc2abf1 | ||
|
|
61c89824f9 | ||
|
|
606cf1714c | ||
|
|
c189bd366a | ||
|
|
9855425be4 | ||
|
|
421405eee0 | ||
|
|
62ec33f092 | ||
|
|
b992b6064a | ||
|
|
c2546a04bb | ||
|
|
99bd0fb061 | ||
|
|
365d6127af | ||
|
|
aa99bda14c | ||
|
|
82a6f33a68 | ||
|
|
fd4c0d9136 | ||
|
|
8b532ffa79 | ||
|
|
81efba0de5 | ||
|
|
861e8c2b7e | ||
|
|
e84eda7525 | ||
|
|
deb9e3ae06 | ||
|
|
56e688a13c | ||
|
|
78bdc37583 | ||
|
|
5e795bc086 | ||
|
|
b7ea9bf4d6 | ||
|
|
5fc584fc66 | ||
|
|
70d112982e | ||
|
|
06271e079f | ||
|
|
00441c30db | ||
|
|
489d0125ac | ||
|
|
7bd0caea97 | ||
|
|
30e9bb41b7 | ||
|
|
00718382fc | ||
|
|
0e5d4d6e3b | ||
|
|
1b0fad9baf | ||
|
|
7bb293b6de | ||
|
|
d75c489a68 | ||
|
|
3bb8033c69 | ||
|
|
7d7b5ab8b6 | ||
|
|
0e084de1c5 | ||
|
|
feeb3cf3e8 | ||
|
|
3f54f16573 | ||
|
|
9642f0b7e7 | ||
|
|
8a79396a0a | ||
|
|
a60c02576f | ||
|
|
33c20db291 | ||
|
|
8605a54310 | ||
|
|
1b655571cc | ||
|
|
9d44630675 | ||
|
|
0b93014242 | ||
|
|
f0566ea8c0 | ||
|
|
87b2ef3ad5 | ||
|
|
c20ee3df04 | ||
|
|
48aa12f351 | ||
|
|
6482c31f17 | ||
|
|
4413ac00f6 | ||
|
|
aa1a5cd246 | ||
|
|
097f35f69f | ||
|
|
2b81b6536e | ||
|
|
246a5429af | ||
|
|
5c935dca69 | ||
|
|
45480260d0 | ||
|
|
84f40f2bb4 | ||
|
|
b4acee2895 | ||
|
|
f536ebcee9 | ||
|
|
2b8b1547e9 | ||
|
|
ed53b68c75 | ||
|
|
9577a5979a | ||
|
|
311b331eee | ||
|
|
9bd47c0fad | ||
|
|
ee825aa97c | ||
|
|
e85bbe8638 | ||
|
|
8f27d1cfa7 | ||
|
|
bd4878576e | ||
|
|
c8e590419e | ||
|
|
1ae6923d1a | ||
|
|
3d00dee042 | ||
|
|
81bc2c96c2 | ||
|
|
4b337c8fe0 | ||
|
|
e09596a7fa | ||
|
|
04e7315185 | ||
|
|
4a49bb458e | ||
|
|
d744b12115 | ||
|
|
3e574ac243 | ||
|
|
2405ac279d | ||
|
|
1469b0a060 | ||
|
|
ac98491928 | ||
|
|
cab13f6df2 | ||
|
|
022d363ccb | ||
|
|
833a023688 | ||
|
|
a99b572ac2 | ||
|
|
70f2e421f9 | ||
|
|
4479189df8 | ||
|
|
150d48d0ba | ||
|
|
1e58537a65 | ||
|
|
89c0d552f6 | ||
|
|
e6d7fc6209 | ||
|
|
6ebe7353ab | ||
|
|
0ff50fbd9b | ||
|
|
903e1c1948 | ||
|
|
d10257a8f4 | ||
|
|
9ff4cd6d05 | ||
|
|
9fd56be9cd | ||
|
|
31ff2fa87f | ||
|
|
ade550dcf4 | ||
|
|
d843d7b78d | ||
|
|
0c7b058593 | ||
|
|
47577a43e5 | ||
|
|
1bd484797f | ||
|
|
e3b6ff6d25 | ||
|
|
e6c1be6a53 | ||
|
|
3219da5a03 | ||
|
|
83afeef2dc | ||
|
|
0d1fa3e292 | ||
|
|
38c85bd904 | ||
|
|
8b000cf0ca | ||
|
|
129f45353a | ||
|
|
0a37ca5aa7 | ||
|
|
5c2cc529ef | ||
|
|
c751a056a9 | ||
|
|
cbcb1e49c7 | ||
|
|
b02c464960 | ||
|
|
4ba39e07f8 | ||
|
|
9f1c3fb383 | ||
|
|
a9e91c02d1 | ||
|
|
54ee6439cb | ||
|
|
97db95243a | ||
|
|
e437f69333 | ||
|
|
f59a22dbad | ||
|
|
e0c5ca9969 | ||
|
|
89564af030 | ||
|
|
6676f01a2d | ||
|
|
63bab53b0e | ||
|
|
2a989c5527 | ||
|
|
b41382f365 | ||
|
|
ac22733cfc | ||
|
|
f9dfc67de4 | ||
|
|
dd8fceb0d5 | ||
|
|
46c3bcbff7 | ||
|
|
b9269c5574 | ||
|
|
77e7eedbe0 | ||
|
|
f6514314c0 | ||
|
|
4556213a1c | ||
|
|
958697f788 | ||
|
|
64a05482e4 | ||
|
|
e18ebb9282 | ||
|
|
b51060d9e7 | ||
|
|
c2b5284da4 | ||
|
|
e2af4cfd6a | ||
|
|
cf10a73e5a | ||
|
|
1d8b9a0f31 | ||
|
|
4cb7555c49 | ||
|
|
9b9f0f0859 | ||
|
|
9fc4339d52 | ||
|
|
6e7c75dd14 | ||
|
|
377f90989d | ||
|
|
1bf7879c1e | ||
|
|
8441b5fc70 | ||
|
|
08d1e5a717 | ||
|
|
9e863fe181 | ||
|
|
3aef5bb2bb | ||
|
|
323c7496f0 | ||
|
|
8454452476 | ||
|
|
cd2cfe25cf | ||
|
|
1c949f6a11 | ||
|
|
9aa7a1e2be | ||
|
|
ec27965ab4 | ||
|
|
956f9fdbae | ||
|
|
c1e8e55409 | ||
|
|
bb672edf15 | ||
|
|
287d9b97c9 | ||
|
|
b7e6a6b2dc | ||
|
|
ba893d646a | ||
|
|
d597a71eb4 | ||
|
|
141a64d888 | ||
|
|
f59c2c5123 | ||
|
|
0658aea425 | ||
|
|
277c9f74cd | ||
|
|
63cd815bf4 | ||
|
|
0b30c62ff9 | ||
|
|
484190720a | ||
|
|
da12d1fc5b | ||
|
|
37fcc4b42c | ||
|
|
3940e51299 | ||
|
|
81f0072cb2 | ||
|
|
013b98a53e | ||
|
|
cd3dee8133 | ||
|
|
a6ec7df800 | ||
|
|
d839536eb0 | ||
|
|
2474f884dd | ||
|
|
408102602c | ||
|
|
4876b2e4f4 | ||
|
|
fce4eece1a | ||
|
|
ac8f6d239c | ||
|
|
56e0c0da79 | ||
|
|
268112f96e | ||
|
|
a92c2c250c | ||
|
|
fa6ba5b6cd | ||
|
|
36eebc43d4 | ||
|
|
0ba4e023ac | ||
|
|
88b06362f1 | ||
|
|
d5fb091420 | ||
|
|
f0511eb498 | ||
|
|
f702ab8669 | ||
|
|
165cacf45a | ||
|
|
fe96bc780d | ||
|
|
295199d43f | ||
|
|
38cc823a58 | ||
|
|
30f581cbc3 | ||
|
|
0296433980 | ||
|
|
14b64f0320 | ||
|
|
6d8bea6c8d | ||
|
|
ab46b6e116 | ||
|
|
6d967f3501 | ||
|
|
bba538336a | ||
|
|
d5e8d50082 | ||
|
|
818f6e9ed6 | ||
|
|
32499bbc68 | ||
|
|
506251bfa5 | ||
|
|
10870b594c |
2186 changed files with 1203362 additions and 7620 deletions
102
.claude/commands/blocks-develop.md
Normal file
102
.claude/commands/blocks-develop.md
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
---
|
||||
description: Develop and validate TPMJS tools using the blocks CLI
|
||||
---
|
||||
|
||||
Help the user develop new tools for the TPMJS registry using the blocks CLI. This workflow covers defining tools in blocks.yml, implementing them with AI SDK v6, validating with the blocks CLI, and publishing to npm.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### 1. Define Tool in blocks.yml
|
||||
|
||||
Add tool definition to `packages/tools/official/blocks.yml`:
|
||||
|
||||
```yaml
|
||||
blocks:
|
||||
category.toolName:
|
||||
type: utility
|
||||
description: "Clear description for LLMs"
|
||||
path: "tool-directory-name"
|
||||
domain_rules:
|
||||
- id: rule_name
|
||||
description: "Implementation requirement"
|
||||
inputs:
|
||||
- name: paramName
|
||||
type: string
|
||||
description: "Parameter description"
|
||||
outputs:
|
||||
- name: result
|
||||
type: ResultType
|
||||
description: "Output description"
|
||||
measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance]
|
||||
```
|
||||
|
||||
### 2. Create Package Structure
|
||||
|
||||
```
|
||||
packages/tools/official/tool-name/
|
||||
├── package.json # npm package with tpmjs field
|
||||
├── tsconfig.json # Extends @tpmjs/tsconfig
|
||||
├── tsup.config.ts # Build config
|
||||
├── block.ts # REQUIRED by validator
|
||||
├── index.ts # Re-export from src
|
||||
└── src/index.ts # Main implementation
|
||||
```
|
||||
|
||||
### 3. Implement with AI SDK v6
|
||||
|
||||
```typescript
|
||||
import { jsonSchema, tool } from 'ai';
|
||||
|
||||
export const myTool = tool({
|
||||
description: 'Description for LLMs',
|
||||
parameters: jsonSchema<InputType>({
|
||||
type: 'object',
|
||||
properties: { /* ... */ },
|
||||
required: ['field1'],
|
||||
}),
|
||||
async execute(input): Promise<OutputType> {
|
||||
// REAL implementation - no stubs
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
export default myTool;
|
||||
```
|
||||
|
||||
### 4. Run Validation
|
||||
|
||||
```bash
|
||||
cd packages/tools/official
|
||||
pnpm blocks run tool-name # Validate single tool
|
||||
pnpm blocks run tool-name --force # Force full validation
|
||||
pnpm blocks run --all # Validate all tools
|
||||
```
|
||||
|
||||
### 5. Build and Publish
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
npm publish --access public
|
||||
|
||||
# Trigger sync to tpmjs.com
|
||||
source apps/web/.env.local
|
||||
curl -X POST https://tpmjs.com/api/sync/keyword -H "Authorization: Bearer $CRON_SECRET"
|
||||
```
|
||||
|
||||
## Valid Categories
|
||||
|
||||
For `tpmjs.category` in package.json: `research`, `web`, `data`, `documentation`, `engineering`, `security`, `statistics`, `ops`, `agent`, `sandbox`, `utilities`, `html`, `compliance`
|
||||
|
||||
## Required Files
|
||||
|
||||
- **block.ts** at root: `export const block = { name: 'tool-name', tools: { myTool } };`
|
||||
- **index.ts** at root: `export * from './src/index.js';`
|
||||
- Both are required for the validator to find the tool
|
||||
|
||||
## Common Issues
|
||||
|
||||
- "invalid tpmjs field" during sync = Invalid category or missing tools array
|
||||
- "Tool not found in exports" = Export name must match blocks.yml
|
||||
- "Required file not found" = Need index.ts and block.ts at package root
|
||||
|
||||
When helping the user, read the full skill documentation at `.claude/skills/blocks-develop.md` for comprehensive details on entities, measures, and multi-tool packages.
|
||||
248
.claude/pipelines/tool-request.md
Normal file
248
.claude/pipelines/tool-request.md
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
# Tool Request Pipeline Specification
|
||||
|
||||
Automated pipeline for creating TPMJS tools from GitHub issues using Claude.
|
||||
|
||||
## Overview
|
||||
|
||||
When a maintainer applies the `tool-request` label to an issue, Claude automatically:
|
||||
1. Analyzes the tool idea and designs the implementation
|
||||
2. Determines the best package (existing or new)
|
||||
3. Implements the tool with AI SDK v6
|
||||
4. Validates using blocks CLI
|
||||
5. Creates an auto-merge PR
|
||||
6. Publishes to npm
|
||||
7. Syncs to tpmjs.com registry
|
||||
|
||||
## Trigger
|
||||
|
||||
| Setting | Value |
|
||||
|---------|-------|
|
||||
| Label | `tool-request` |
|
||||
| Who can apply | Maintainers only |
|
||||
| Trigger mechanism | Label application triggers workflow, which comments `@claude` |
|
||||
| Concurrency | Parallel execution allowed |
|
||||
| Rate limit | None (trust maintainers) |
|
||||
|
||||
## Input Requirements
|
||||
|
||||
| Setting | Value |
|
||||
|---------|-------|
|
||||
| Input format | Accept vague ideas - Claude designs autonomously |
|
||||
| Structured template | Not required |
|
||||
| Clarification | Claude fills gaps autonomously, doesn't ask first |
|
||||
| Mid-flight edits | Incorporate edits - check for changes at each step |
|
||||
|
||||
## Package Organization
|
||||
|
||||
| Setting | Value |
|
||||
|---------|-------|
|
||||
| Strategy | Hybrid - default to categories, allow functional cohesion exceptions |
|
||||
| Package selection | Analyze all existing tools in candidate packages to find best fit |
|
||||
| New vs existing | Claude decides based on functional cohesion analysis |
|
||||
| blocks.yml access | Full access - Claude adds entries as part of workflow |
|
||||
|
||||
### Decision Logic for Package Selection
|
||||
|
||||
1. Search existing packages for functionally related tools
|
||||
2. If strong match found (>70% conceptual overlap), add to existing package
|
||||
3. If no match or tool is foundational for a new domain, create new package
|
||||
4. Exception: tightly coupled tools (e.g., e2b-*) stay together regardless of category
|
||||
|
||||
## Validation & Iteration
|
||||
|
||||
| Setting | Value |
|
||||
|---------|-------|
|
||||
| Max attempts | 3 before escalating to human review |
|
||||
| On failure | Iterate in-issue - Claude fixes and retries |
|
||||
| Runtime test | Execute with sample inputs, capture output as screenshot |
|
||||
| Tool restrictions | None - any valid tool that passes validation is allowed |
|
||||
|
||||
### Validation Steps
|
||||
|
||||
1. `pnpm blocks run <tool-name>` - domain rules and output measures
|
||||
2. TypeScript compilation check
|
||||
3. Execute tool with generated sample inputs
|
||||
4. Verify output structure matches schema
|
||||
5. Capture execution output as proof in issue comment
|
||||
|
||||
## Publishing
|
||||
|
||||
| Setting | Value |
|
||||
|---------|-------|
|
||||
| Branch strategy | Auto-merge PR - create for visibility, auto-merge if CI passes |
|
||||
| Version bump | Minor (0.X.0) - new functionality = minor version |
|
||||
| NPM auth | Use existing `NPM_TOKEN` secret |
|
||||
| On publish failure | Comment explaining failure, wait for human to fix and re-trigger |
|
||||
|
||||
### PR Template
|
||||
|
||||
```markdown
|
||||
## Tool: `<tool-name>`
|
||||
|
||||
**Package:** `@tpmjs/tools-<package>`
|
||||
**Version:** `0.X.0` -> `0.Y.0`
|
||||
|
||||
### Description
|
||||
<tool description>
|
||||
|
||||
### Implementation
|
||||
- [ ] blocks.yml entry added
|
||||
- [ ] Package files created
|
||||
- [ ] Validation passed
|
||||
- [ ] Runtime test passed
|
||||
|
||||
### Test Output
|
||||
<screenshot of tool execution>
|
||||
|
||||
---
|
||||
Auto-generated by Claude from #<issue-number>
|
||||
```
|
||||
|
||||
## Post-Publish
|
||||
|
||||
| Setting | Value |
|
||||
|---------|-------|
|
||||
| Registry sync | Auto-sync - call `/api/sync/keyword` after publish |
|
||||
| Verify listing | Confirm tool appears on tpmjs.com before reporting success |
|
||||
| Collections | Standalone only - no auto-add |
|
||||
| Duplicates | Propose enhancement to existing tool if duplicate detected |
|
||||
|
||||
## Status Tracking
|
||||
|
||||
### Labels (managed by Claude)
|
||||
|
||||
| Label | Meaning |
|
||||
|-------|---------|
|
||||
| `tool-request` | Initial trigger (applied by maintainer) |
|
||||
| `claude-working` | Claude is actively processing |
|
||||
| `validation-failed` | Validation failed, iterating |
|
||||
| `published` | Successfully published to npm |
|
||||
| `escalated` | Requires human intervention |
|
||||
|
||||
### Issue Lifecycle
|
||||
|
||||
1. Maintainer applies `tool-request` label
|
||||
2. Workflow triggers, adds `claude-working` label
|
||||
3. On validation failure: add `validation-failed`, retry (max 3x)
|
||||
4. On success: remove other labels, add `published`
|
||||
5. Keep issue open 24h for feedback
|
||||
6. Auto-close after 24h
|
||||
|
||||
## Success Report
|
||||
|
||||
Full changelog posted to issue:
|
||||
|
||||
```markdown
|
||||
## Tool Published Successfully
|
||||
|
||||
**Package:** `@tpmjs/tools-<package>@<version>`
|
||||
**NPM:** https://www.npmjs.com/package/@tpmjs/tools-<package>
|
||||
**Registry:** https://tpmjs.com/tool/@tpmjs/tools-<package>/<tool-name>
|
||||
|
||||
### Changes
|
||||
- Added `<tool-name>` tool
|
||||
- Updated blocks.yml
|
||||
- Bumped version from X.Y.Z to X.Y+1.0
|
||||
|
||||
### Validation Results
|
||||
<validation output>
|
||||
|
||||
### Test Execution
|
||||
<screenshot of tool running with sample inputs>
|
||||
|
||||
### Files Changed
|
||||
<file diff summary>
|
||||
|
||||
---
|
||||
This issue will auto-close in 24 hours. Reply if you have feedback.
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Scenario | Action |
|
||||
|----------|--------|
|
||||
| Validation fails 3x | Add `escalated` label, assign to maintainer with diagnostic info |
|
||||
| NPM publish fails | Comment explaining failure, wait for human fix |
|
||||
| Duplicate detected | Comment explaining existing tool, propose enhancement instead |
|
||||
| blocks.yml conflict | Rebase and retry automatically |
|
||||
| Issue edited mid-work | Detect changes, incorporate into implementation |
|
||||
|
||||
## Context & Memory
|
||||
|
||||
| Setting | Value |
|
||||
|---------|-------|
|
||||
| State tracking | Full conversation - Claude remembers entire issue thread |
|
||||
| Previous attempts | Tracked within issue context |
|
||||
| Cross-issue | No memory between different issues |
|
||||
|
||||
## Workflow File Structure
|
||||
|
||||
```yaml
|
||||
name: Tool Request Pipeline
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
trigger-claude:
|
||||
if: github.event.label.name == 'tool-request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Add working label
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels: ['claude-working']
|
||||
});
|
||||
|
||||
- name: Comment to trigger Claude
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: '@claude Please implement this tool request following the tool-request pipeline specification at `.claude/pipelines/tool-request.md`.'
|
||||
});
|
||||
```
|
||||
|
||||
## Claude Instructions
|
||||
|
||||
When triggered, Claude should:
|
||||
|
||||
1. **Read this spec** at `.claude/pipelines/tool-request.md`
|
||||
2. **Analyze the issue** - extract tool name, description, intended functionality
|
||||
3. **Check for duplicates** - search existing tools for similar functionality
|
||||
4. **Select package** - analyze existing packages, decide new vs existing
|
||||
5. **Design the tool** - define inputs, outputs, implementation approach
|
||||
6. **Implement** - create/update blocks.yml, create package files
|
||||
7. **Validate** - run `pnpm blocks run <tool>` in packages/tools/official
|
||||
8. **Test** - execute with sample inputs, capture output
|
||||
9. **Create PR** - feature branch, include all changes
|
||||
10. **Publish** - after CI passes, `npm publish`
|
||||
11. **Sync** - trigger registry sync
|
||||
12. **Report** - full changelog to issue
|
||||
13. **Cleanup** - update labels, schedule auto-close
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Only maintainers can apply trigger label
|
||||
- NPM_TOKEN is existing secret, not exposed in logs
|
||||
- Tool code is reviewed via PR (even if auto-merged)
|
||||
- No restrictions on tool types - trust validation + maintainer judgment
|
||||
- Full audit trail in issue comments
|
||||
|
||||
## Dry Run
|
||||
|
||||
No dry run mode. Validation is sufficient safeguard. If testing needed, create a test issue and manually delete artifacts after.
|
||||
|
||||
---
|
||||
|
||||
*Specification created: 2026-01-19*
|
||||
*Interview conducted with: @ajax*
|
||||
16
.claude/plugins/ralph-wiggum/commands/cancel-ralph.md
Normal file
16
.claude/plugins/ralph-wiggum/commands/cancel-ralph.md
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
description: Cancel the active Ralph loop
|
||||
command: rm -f .claude/ralph-loop.local.md && echo "Ralph loop cancelled"
|
||||
---
|
||||
|
||||
# Cancel Ralph Loop
|
||||
|
||||
Immediately cancel any active Ralph loop and allow normal session exit.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/cancel-ralph
|
||||
```
|
||||
|
||||
This removes the state file that drives the loop, allowing the session to exit normally.
|
||||
43
.claude/plugins/ralph-wiggum/commands/ralph-loop.md
Normal file
43
.claude/plugins/ralph-wiggum/commands/ralph-loop.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
description: Start Ralph Wiggum loop in current session
|
||||
command: "${CLAUDE_PLUGIN_ROOT}/scripts/setup-ralph-loop.sh" $ARGUMENTS
|
||||
---
|
||||
|
||||
# Ralph Loop
|
||||
|
||||
Start an iterative development loop that continues until the task is complete.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/ralph-loop "Your task description" [--max-iterations N] [--validation-script PATH] [--completion-promise TEXT]
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. You provide a task and optional validation criteria
|
||||
2. Claude works on the task
|
||||
3. When Claude tries to exit, the stop hook intercepts
|
||||
4. If validation fails OR completion promise not met, the loop continues
|
||||
5. Claude sees previous work and continues iterating
|
||||
6. Loop ends when validation passes or max iterations reached
|
||||
|
||||
## Important Rules
|
||||
|
||||
- If a completion promise is set, you may ONLY output it when the statement is completely and unequivocally TRUE
|
||||
- Do NOT use false completion promises as an exit strategy
|
||||
- The loop persists until genuine completion is achieved
|
||||
- Use validation scripts for programmatic verification
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# With validation script only
|
||||
/ralph-loop "Build the SDK package" --validation-script ./scripts/validate-sdk.sh
|
||||
|
||||
# With completion promise
|
||||
/ralph-loop "Fix all type errors" --completion-promise "ALL_TYPES_PASS"
|
||||
|
||||
# With both
|
||||
/ralph-loop "Complete feature X" --max-iterations 15 --validation-script ./validate.sh --completion-promise "FEATURE_COMPLETE"
|
||||
```
|
||||
15
.claude/plugins/ralph-wiggum/hooks/hooks.json
Normal file
15
.claude/plugins/ralph-wiggum/hooks/hooks.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"description": "Ralph Wiggum plugin stop hook for self-referential loops",
|
||||
"hooks": {
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/stop-hook.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
111
.claude/plugins/ralph-wiggum/hooks/stop-hook.sh
Executable file
111
.claude/plugins/ralph-wiggum/hooks/stop-hook.sh
Executable file
|
|
@ -0,0 +1,111 @@
|
|||
#!/bin/bash
|
||||
# Ralph Wiggum Stop Hook - Self-referential loop for iterative development
|
||||
# This hook intercepts the Stop event and decides whether to continue the loop
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
STATE_FILE=".claude/ralph-loop.local.md"
|
||||
TRANSCRIPT_FILE="${CLAUDE_TRANSCRIPT:-}"
|
||||
|
||||
# Check if ralph loop is active
|
||||
if [[ ! -f "$STATE_FILE" ]]; then
|
||||
# No active loop, allow normal exit
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Parse the state file frontmatter
|
||||
parse_frontmatter() {
|
||||
local key="$1"
|
||||
sed -n '/^---$/,/^---$/p' "$STATE_FILE" | grep "^${key}:" | sed "s/^${key}: *//" | tr -d '"'
|
||||
}
|
||||
|
||||
iteration=$(parse_frontmatter "iteration")
|
||||
max_iterations=$(parse_frontmatter "max_iterations")
|
||||
completion_promise=$(parse_frontmatter "completion_promise")
|
||||
prompt=$(parse_frontmatter "prompt")
|
||||
validation_script=$(parse_frontmatter "validation_script")
|
||||
|
||||
# Validate numeric fields
|
||||
if ! [[ "$iteration" =~ ^[0-9]+$ ]]; then
|
||||
echo "Error: Invalid iteration count in state file" >&2
|
||||
rm -f "$STATE_FILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! [[ "$max_iterations" =~ ^[0-9]+$ ]]; then
|
||||
echo "Error: Invalid max_iterations in state file" >&2
|
||||
rm -f "$STATE_FILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if max iterations reached
|
||||
if [[ "$max_iterations" -gt 0 ]] && [[ "$iteration" -ge "$max_iterations" ]]; then
|
||||
echo "Ralph loop reached max iterations ($max_iterations). Exiting." >&2
|
||||
rm -f "$STATE_FILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Run validation script if provided
|
||||
validation_passed=false
|
||||
if [[ -n "$validation_script" ]] && [[ -f "$validation_script" ]]; then
|
||||
echo "Running validation script: $validation_script" >&2
|
||||
if bash "$validation_script" 2>&1; then
|
||||
validation_passed=true
|
||||
echo "Validation PASSED!" >&2
|
||||
else
|
||||
echo "Validation FAILED. Continuing loop..." >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for completion promise in transcript
|
||||
if [[ -n "$completion_promise" ]] && [[ -n "$TRANSCRIPT_FILE" ]] && [[ -f "$TRANSCRIPT_FILE" ]]; then
|
||||
# Get the last assistant message
|
||||
last_message=$(tail -100 "$TRANSCRIPT_FILE" | grep -o '<promise>[^<]*</promise>' | tail -1 | sed 's/<promise>\(.*\)<\/promise>/\1/' || true)
|
||||
|
||||
if [[ "$last_message" == "$completion_promise" ]]; then
|
||||
# Also check if validation passed (if validation script exists)
|
||||
if [[ -z "$validation_script" ]] || [[ "$validation_passed" == "true" ]]; then
|
||||
echo "Completion promise matched and validation passed. Ralph loop complete!" >&2
|
||||
rm -f "$STATE_FILE"
|
||||
exit 0
|
||||
else
|
||||
echo "Completion promise matched but validation failed. Continuing..." >&2
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# If validation passed without explicit promise, we can exit
|
||||
if [[ "$validation_passed" == "true" ]] && [[ -z "$completion_promise" ]]; then
|
||||
echo "Validation passed. Ralph loop complete!" >&2
|
||||
rm -f "$STATE_FILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Increment iteration
|
||||
new_iteration=$((iteration + 1))
|
||||
|
||||
# Update state file
|
||||
sed -i.bak "s/^iteration: .*/iteration: $new_iteration/" "$STATE_FILE"
|
||||
rm -f "${STATE_FILE}.bak"
|
||||
|
||||
# Build the continuation message
|
||||
cat << EOF
|
||||
{
|
||||
"decision": "block",
|
||||
"reason": "Ralph loop iteration $new_iteration of $max_iterations",
|
||||
"message": "
|
||||
---
|
||||
RALPH LOOP - Iteration $new_iteration / $max_iterations
|
||||
---
|
||||
|
||||
Continue working on the task. Your previous iteration's work is preserved in the codebase.
|
||||
|
||||
TASK: $prompt
|
||||
|
||||
$(if [[ -n "$validation_script" ]]; then echo "VALIDATION: Run the validation to check progress. Script: $validation_script"; fi)
|
||||
$(if [[ -n "$completion_promise" ]]; then echo "COMPLETION: Output <promise>$completion_promise</promise> ONLY when the task is completely done AND validation passes."; fi)
|
||||
|
||||
Review what you've done so far and continue from where you left off.
|
||||
"
|
||||
}
|
||||
EOF
|
||||
120
.claude/plugins/ralph-wiggum/scripts/setup-ralph-loop.sh
Executable file
120
.claude/plugins/ralph-wiggum/scripts/setup-ralph-loop.sh
Executable file
|
|
@ -0,0 +1,120 @@
|
|||
#!/bin/bash
|
||||
# Setup Ralph Loop - Initialize the iterative development loop
|
||||
# Usage: setup-ralph-loop.sh "PROMPT" [--max-iterations N] [--completion-promise TEXT] [--validation-script PATH]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
STATE_FILE=".claude/ralph-loop.local.md"
|
||||
|
||||
# Default values
|
||||
MAX_ITERATIONS=20
|
||||
COMPLETION_PROMISE=""
|
||||
VALIDATION_SCRIPT=""
|
||||
PROMPT=""
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--max-iterations)
|
||||
MAX_ITERATIONS="$2"
|
||||
shift 2
|
||||
;;
|
||||
--completion-promise)
|
||||
COMPLETION_PROMISE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--validation-script)
|
||||
VALIDATION_SCRIPT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--help|-h)
|
||||
cat << EOF
|
||||
Ralph Loop Setup
|
||||
|
||||
Usage: setup-ralph-loop.sh "PROMPT" [OPTIONS]
|
||||
|
||||
Options:
|
||||
--max-iterations N Maximum iterations before stopping (default: 20, 0 = unlimited)
|
||||
--completion-promise TXT Phrase to output when complete (use <promise>TXT</promise>)
|
||||
--validation-script PATH Script to run for validation (exit 0 = pass)
|
||||
--help, -h Show this help
|
||||
|
||||
Example:
|
||||
setup-ralph-loop.sh "Build the SDK package" --max-iterations 10 --validation-script ./validate.sh
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
if [[ -z "$PROMPT" ]]; then
|
||||
PROMPT="$1"
|
||||
else
|
||||
PROMPT="$PROMPT $1"
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validate prompt
|
||||
if [[ -z "$PROMPT" ]]; then
|
||||
echo "Error: PROMPT is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate max iterations
|
||||
if ! [[ "$MAX_ITERATIONS" =~ ^[0-9]+$ ]]; then
|
||||
echo "Error: --max-iterations must be a number" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate validation script exists if provided
|
||||
if [[ -n "$VALIDATION_SCRIPT" ]] && [[ ! -f "$VALIDATION_SCRIPT" ]]; then
|
||||
echo "Error: Validation script not found: $VALIDATION_SCRIPT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create state directory
|
||||
mkdir -p "$(dirname "$STATE_FILE")"
|
||||
|
||||
# Create state file
|
||||
cat << EOF > "$STATE_FILE"
|
||||
---
|
||||
iteration: 1
|
||||
max_iterations: $MAX_ITERATIONS
|
||||
completion_promise: "$COMPLETION_PROMISE"
|
||||
validation_script: "$VALIDATION_SCRIPT"
|
||||
prompt: "$PROMPT"
|
||||
started_at: "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
||||
---
|
||||
|
||||
# Ralph Loop State
|
||||
|
||||
This file tracks the state of an active Ralph loop. DO NOT DELETE while loop is running.
|
||||
|
||||
## Configuration
|
||||
- **Task**: $PROMPT
|
||||
- **Max Iterations**: $MAX_ITERATIONS
|
||||
- **Validation Script**: ${VALIDATION_SCRIPT:-"None"}
|
||||
- **Completion Promise**: ${COMPLETION_PROMISE:-"None (validation only)"}
|
||||
|
||||
## Progress Log
|
||||
|
||||
Iteration logs will be appended below as the loop progresses.
|
||||
|
||||
---
|
||||
EOF
|
||||
|
||||
echo "Ralph loop initialized!"
|
||||
echo " Task: $PROMPT"
|
||||
echo " Max iterations: $MAX_ITERATIONS"
|
||||
echo " Validation: ${VALIDATION_SCRIPT:-"None"}"
|
||||
echo " Completion promise: ${COMPLETION_PROMISE:-"None"}"
|
||||
echo ""
|
||||
echo "The loop will continue until:"
|
||||
if [[ -n "$VALIDATION_SCRIPT" ]]; then
|
||||
echo " - Validation script passes ($VALIDATION_SCRIPT returns exit code 0)"
|
||||
fi
|
||||
if [[ -n "$COMPLETION_PROMISE" ]]; then
|
||||
echo " - You output: <promise>$COMPLETION_PROMISE</promise>"
|
||||
fi
|
||||
echo " - OR max iterations ($MAX_ITERATIONS) is reached"
|
||||
1
.claude/skills/agentmail
Symbolic link
1
.claude/skills/agentmail
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../.agents/skills/agentmail
|
||||
357
.claude/skills/blocks-develop.md
Normal file
357
.claude/skills/blocks-develop.md
Normal file
|
|
@ -0,0 +1,357 @@
|
|||
# TPMJS Tool Development with Blocks CLI
|
||||
|
||||
Use this skill when developing new tools for the TPMJS registry. This covers the full workflow from defining a tool in blocks.yml through implementation, validation, and publishing.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Navigate to official tools directory
|
||||
cd packages/tools/official
|
||||
|
||||
# Run validation on a specific tool
|
||||
pnpm blocks run <block-name>
|
||||
|
||||
# Run validation on all tools
|
||||
pnpm blocks run --all
|
||||
|
||||
# Force full validation (ignore cache)
|
||||
pnpm blocks run <block-name> --force
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### 1. Define the Tool Block in blocks.yml
|
||||
|
||||
Add your tool definition to `packages/tools/official/blocks.yml` in the `blocks:` section:
|
||||
|
||||
```yaml
|
||||
blocks:
|
||||
# Category.toolName format
|
||||
sandbox.myTool:
|
||||
type: utility
|
||||
description: "Clear, LLM-friendly description of what the tool does"
|
||||
path: "my-tool" # Directory name under packages/tools/official/
|
||||
domain_rules:
|
||||
- id: rule_name
|
||||
description: "What this implementation must do"
|
||||
inputs:
|
||||
- name: inputName
|
||||
type: string
|
||||
description: "Description for LLMs"
|
||||
- name: optionalInput
|
||||
type: number
|
||||
optional: true
|
||||
description: "Optional parameter"
|
||||
outputs:
|
||||
- name: result
|
||||
type: MyResultType
|
||||
description: "What the tool returns"
|
||||
measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance]
|
||||
```
|
||||
|
||||
**Key Fields:**
|
||||
- `type`: Usually `utility` for single-shot tools
|
||||
- `path`: Directory name (kebab-case)
|
||||
- `domain_rules`: Implementation requirements the validator checks
|
||||
- `inputs/outputs`: Schema for validation
|
||||
- `measures`: Quality constraints from the domain section
|
||||
|
||||
### 2. Create the Tool Package
|
||||
|
||||
Create the directory structure:
|
||||
|
||||
```
|
||||
packages/tools/official/my-tool/
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
├── tsup.config.ts
|
||||
├── block.ts # Required by validator
|
||||
├── index.ts # Re-export from src
|
||||
└── src/
|
||||
└── index.ts # Main implementation
|
||||
```
|
||||
|
||||
**package.json:**
|
||||
```json
|
||||
{
|
||||
"name": "@tpmjs/tools-my-tool",
|
||||
"version": "0.1.0",
|
||||
"description": "Short description for npm",
|
||||
"type": "module",
|
||||
"keywords": ["tpmjs", "category-name", "ai"],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": ["dist"],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"dev": "tsup --watch",
|
||||
"type-check": "tsc --noEmit",
|
||||
"clean": "rm -rf dist .turbo"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tpmjs/tsconfig": "workspace:*",
|
||||
"tsup": "^8.5.1",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"ai": "6.0.23"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tpmjs/tpmjs.git",
|
||||
"directory": "packages/tools/official/my-tool"
|
||||
},
|
||||
"homepage": "https://tpmjs.com",
|
||||
"license": "MIT",
|
||||
"tpmjs": {
|
||||
"category": "sandbox",
|
||||
"frameworks": ["vercel-ai"],
|
||||
"tools": [
|
||||
{
|
||||
"name": "myTool",
|
||||
"description": "Clear description (20+ chars) of what this tool does."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Valid categories for tpmjs.category:**
|
||||
- `research`, `web`, `data`, `documentation`, `engineering`
|
||||
- `security`, `statistics`, `ops`, `agent`, `sandbox`, `utilities`
|
||||
- `html`, `compliance`
|
||||
|
||||
**tsconfig.json:**
|
||||
```json
|
||||
{
|
||||
"extends": "@tpmjs/tsconfig/react-library.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
```
|
||||
|
||||
**tsup.config.ts:**
|
||||
```typescript
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
format: ['esm'],
|
||||
dts: true,
|
||||
clean: true,
|
||||
sourcemap: true,
|
||||
target: 'es2022',
|
||||
});
|
||||
```
|
||||
|
||||
**block.ts (Required by validator):**
|
||||
```typescript
|
||||
import { myTool } from './src/index.js';
|
||||
|
||||
export const block = {
|
||||
name: 'my-tool',
|
||||
description: 'Short description',
|
||||
tools: { myTool },
|
||||
};
|
||||
|
||||
export default block;
|
||||
```
|
||||
|
||||
**index.ts (Root re-export):**
|
||||
```typescript
|
||||
export * from './src/index.js';
|
||||
export { default } from './src/index.js';
|
||||
```
|
||||
|
||||
### 3. Implement the Tool
|
||||
|
||||
**src/index.ts:**
|
||||
```typescript
|
||||
import { jsonSchema, tool } from 'ai';
|
||||
|
||||
// Define input/output types
|
||||
interface MyToolInput {
|
||||
param1: string;
|
||||
param2?: number;
|
||||
}
|
||||
|
||||
interface MyToolResult {
|
||||
data: string;
|
||||
metadata: {
|
||||
processedAt: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Export the tool using AI SDK v6 pattern
|
||||
export const myTool = tool({
|
||||
description: 'Clear description for LLMs explaining what this tool does and when to use it.',
|
||||
parameters: jsonSchema<MyToolInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
param1: {
|
||||
type: 'string',
|
||||
description: 'Description of param1',
|
||||
},
|
||||
param2: {
|
||||
type: 'number',
|
||||
description: 'Optional description of param2',
|
||||
},
|
||||
},
|
||||
required: ['param1'],
|
||||
}),
|
||||
async execute(input): Promise<MyToolResult> {
|
||||
// REAL implementation - no stubs, no TODOs
|
||||
const result = await doSomething(input.param1);
|
||||
|
||||
return {
|
||||
data: result,
|
||||
metadata: {
|
||||
processedAt: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// Default export for compatibility
|
||||
export default myTool;
|
||||
```
|
||||
|
||||
### 4. Run Validation
|
||||
|
||||
```bash
|
||||
cd packages/tools/official
|
||||
|
||||
# Validate your tool
|
||||
pnpm blocks run my-tool
|
||||
|
||||
# The validator runs 3 stages:
|
||||
# 1. schema - Validates inputs/outputs match blocks.yml
|
||||
# 2. shape - Verifies exports and structure
|
||||
# 3. domain - Checks domain rules are satisfied
|
||||
```
|
||||
|
||||
**Common validation errors:**
|
||||
- `Required file "index.ts" not found` - Need index.ts at package root
|
||||
- `Required file "block.ts" not found` - Need block.ts at package root
|
||||
- `Tool "myTool" not found in exports` - Export name must match blocks.yml
|
||||
- `invalid tpmjs field` - Category must be valid, tools array required
|
||||
|
||||
### 5. Build and Publish
|
||||
|
||||
```bash
|
||||
# Build the package
|
||||
pnpm build
|
||||
|
||||
# Publish to npm
|
||||
npm publish --access public
|
||||
|
||||
# Trigger sync to tpmjs.com
|
||||
source apps/web/.env.local
|
||||
curl -X POST https://tpmjs.com/api/sync/keyword \
|
||||
-H "Authorization: Bearer $CRON_SECRET"
|
||||
```
|
||||
|
||||
## Multi-Tool Packages
|
||||
|
||||
For packages with multiple tools (like unsandbox):
|
||||
|
||||
**blocks.yml:**
|
||||
```yaml
|
||||
blocks:
|
||||
sandbox.executeCodeAsync:
|
||||
type: utility
|
||||
path: "unsandbox" # Same path for all tools in package
|
||||
# ...
|
||||
|
||||
sandbox.getJob:
|
||||
type: utility
|
||||
path: "unsandbox" # Same path
|
||||
# ...
|
||||
```
|
||||
|
||||
**block.ts:**
|
||||
```typescript
|
||||
import { executeCodeAsync, getJob, listJobs } from './src/index.js';
|
||||
|
||||
export const block = {
|
||||
name: 'unsandbox',
|
||||
tools: { executeCodeAsync, getJob, listJobs },
|
||||
};
|
||||
|
||||
export default block;
|
||||
```
|
||||
|
||||
**package.json tpmjs field:**
|
||||
```json
|
||||
{
|
||||
"tpmjs": {
|
||||
"category": "sandbox",
|
||||
"frameworks": ["vercel-ai"],
|
||||
"tools": [
|
||||
{ "name": "executeCodeAsync", "description": "..." },
|
||||
{ "name": "getJob", "description": "..." },
|
||||
{ "name": "listJobs", "description": "..." }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Philosophy (from blocks.yml)
|
||||
|
||||
- Every tool MUST be a working, production-ready implementation - no stubs, no TODOs
|
||||
- Tools use AI SDK v6 `tool()` + `jsonSchema()` pattern exclusively
|
||||
- Each tool does ONE thing exceptionally well (single-shot, one call in, one result out)
|
||||
- Tools return structured, typed outputs that agents can reliably parse
|
||||
- Error handling is explicit - throw meaningful errors, never silently fail
|
||||
- Dependencies are minimal and production-stable
|
||||
|
||||
## Domain Entities
|
||||
|
||||
When defining outputs, reference existing entities from blocks.yml:
|
||||
|
||||
```yaml
|
||||
# Example entities available:
|
||||
url: [href, domain, protocol, path, query, fragment]
|
||||
webpage: [url, title, html, text, metadata]
|
||||
text_content: [raw, sentences, paragraphs, wordCount]
|
||||
claim: [statement, confidence, needsCitation, category]
|
||||
timeline: [events, dateRange, gaps, eventCount]
|
||||
```
|
||||
|
||||
Or define new entities in the `domain.entities` section if needed.
|
||||
|
||||
## Quality Measures
|
||||
|
||||
Reference these in your tool's `measures` array:
|
||||
|
||||
- `working_implementation` - No stubs, TODOs, or placeholders
|
||||
- `valid_output_structure` - Returns correct typed object
|
||||
- `proper_error_handling` - Throws descriptive errors
|
||||
- `ai_sdk_compliance` - Uses tool() and jsonSchema()
|
||||
- `npm_publishable` - Valid package.json with tpmjs field
|
||||
- `readme_documentation` - Has README with examples
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
```bash
|
||||
# Force rebuild without cache
|
||||
pnpm blocks run my-tool --force --no-cache
|
||||
|
||||
# See JSON output for debugging
|
||||
pnpm blocks run my-tool --json
|
||||
|
||||
# Check if validator finds your package
|
||||
ls packages/tools/official/my-tool/
|
||||
# Must have: index.ts, block.ts at root level
|
||||
```
|
||||
43
.claude/skills/remotion-best-practices/SKILL.md
Normal file
43
.claude/skills/remotion-best-practices/SKILL.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
name: remotion-best-practices
|
||||
description: Best practices for Remotion - Video creation in React
|
||||
metadata:
|
||||
tags: remotion, video, react, animation, composition
|
||||
---
|
||||
|
||||
## When to use
|
||||
|
||||
Use this skills whenever you are dealing with Remotion code to obtain the domain-specific knowledge.
|
||||
|
||||
## How to use
|
||||
|
||||
Read individual rule files for detailed explanations and code examples:
|
||||
|
||||
- [rules/3d.md](rules/3d.md) - 3D content in Remotion using Three.js and React Three Fiber
|
||||
- [rules/animations.md](rules/animations.md) - Fundamental animation skills for Remotion
|
||||
- [rules/assets.md](rules/assets.md) - Importing images, videos, audio, and fonts into Remotion
|
||||
- [rules/audio.md](rules/audio.md) - Using audio and sound in Remotion - importing, trimming, volume, speed, pitch
|
||||
- [rules/calculate-metadata.md](rules/calculate-metadata.md) - Dynamically set composition duration, dimensions, and props
|
||||
- [rules/can-decode.md](rules/can-decode.md) - Check if a video can be decoded by the browser using Mediabunny
|
||||
- [rules/charts.md](rules/charts.md) - Chart and data visualization patterns for Remotion
|
||||
- [rules/compositions.md](rules/compositions.md) - Defining compositions, stills, folders, default props and dynamic metadata
|
||||
- [rules/display-captions.md](rules/display-captions.md) - Displaying captions in Remotion with TikTok-style pages and word highlighting
|
||||
- [rules/extract-frames.md](rules/extract-frames.md) - Extract frames from videos at specific timestamps using Mediabunny
|
||||
- [rules/fonts.md](rules/fonts.md) - Loading Google Fonts and local fonts in Remotion
|
||||
- [rules/get-audio-duration.md](rules/get-audio-duration.md) - Getting the duration of an audio file in seconds with Mediabunny
|
||||
- [rules/get-video-dimensions.md](rules/get-video-dimensions.md) - Getting the width and height of a video file with Mediabunny
|
||||
- [rules/get-video-duration.md](rules/get-video-duration.md) - Getting the duration of a video file in seconds with Mediabunny
|
||||
- [rules/gifs.md](rules/gifs.md) - Displaying GIFs synchronized with Remotion's timeline
|
||||
- [rules/images.md](rules/images.md) - Embedding images in Remotion using the Img component
|
||||
- [rules/import-srt-captions.md](rules/import-srt-captions.md) - Importing .srt subtitle files into Remotion using @remotion/captions
|
||||
- [rules/lottie.md](rules/lottie.md) - Embedding Lottie animations in Remotion
|
||||
- [rules/measuring-dom-nodes.md](rules/measuring-dom-nodes.md) - Measuring DOM element dimensions in Remotion
|
||||
- [rules/measuring-text.md](rules/measuring-text.md) - Measuring text dimensions, fitting text to containers, and checking overflow
|
||||
- [rules/sequencing.md](rules/sequencing.md) - Sequencing patterns for Remotion - delay, trim, limit duration of items
|
||||
- [rules/tailwind.md](rules/tailwind.md) - Using TailwindCSS in Remotion
|
||||
- [rules/text-animations.md](rules/text-animations.md) - Typography and text animation patterns for Remotion
|
||||
- [rules/timing.md](rules/timing.md) - Interpolation curves in Remotion - linear, easing, spring animations
|
||||
- [rules/transcribe-captions.md](rules/transcribe-captions.md) - Transcribing audio to generate captions in Remotion
|
||||
- [rules/transitions.md](rules/transitions.md) - Scene transition patterns for Remotion
|
||||
- [rules/trimming.md](rules/trimming.md) - Trimming patterns for Remotion - cut the beginning or end of animations
|
||||
- [rules/videos.md](rules/videos.md) - Embedding videos in Remotion - trimming, volume, speed, looping, pitch
|
||||
86
.claude/skills/remotion-best-practices/rules/3d.md
Normal file
86
.claude/skills/remotion-best-practices/rules/3d.md
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
---
|
||||
name: 3d
|
||||
description: 3D content in Remotion using Three.js and React Three Fiber.
|
||||
metadata:
|
||||
tags: 3d, three, threejs
|
||||
---
|
||||
|
||||
# Using Three.js and React Three Fiber in Remotion
|
||||
|
||||
Follow React Three Fiber and Three.js best practices.
|
||||
Only the following Remotion-specific rules need to be followed:
|
||||
|
||||
## Prerequisites
|
||||
|
||||
First, the `@remotion/three` package needs to be installed.
|
||||
If it is not, use the following command:
|
||||
|
||||
```bash
|
||||
npx remotion add @remotion/three # If project uses npm
|
||||
bunx remotion add @remotion/three # If project uses bun
|
||||
yarn remotion add @remotion/three # If project uses yarn
|
||||
pnpm exec remotion add @remotion/three # If project uses pnpm
|
||||
```
|
||||
|
||||
## Using ThreeCanvas
|
||||
|
||||
You MUST wrap 3D content in `<ThreeCanvas>` and include proper lighting.
|
||||
`<ThreeCanvas>` MUST have a `width` and `height` prop.
|
||||
|
||||
```tsx
|
||||
import { ThreeCanvas } from "@remotion/three";
|
||||
import { useVideoConfig } from "remotion";
|
||||
|
||||
const { width, height } = useVideoConfig();
|
||||
|
||||
<ThreeCanvas width={width} height={height}>
|
||||
<ambientLight intensity={0.4} />
|
||||
<directionalLight position={[5, 5, 5]} intensity={0.8} />
|
||||
<mesh>
|
||||
<sphereGeometry args={[1, 32, 32]} />
|
||||
<meshStandardMaterial color="red" />
|
||||
</mesh>
|
||||
</ThreeCanvas>
|
||||
```
|
||||
|
||||
## No animations not driven by `useCurrentFrame()`
|
||||
|
||||
Shaders, models etc MUST NOT animate by themselves.
|
||||
No animations are allowed unless they are driven by `useCurrentFrame()`.
|
||||
Otherwise, it will cause flickering during rendering.
|
||||
|
||||
Using `useFrame()` from `@react-three/fiber` is forbidden.
|
||||
|
||||
## Animate using `useCurrentFrame()`
|
||||
|
||||
Use `useCurrentFrame()` to perform animations.
|
||||
|
||||
```tsx
|
||||
const frame = useCurrentFrame();
|
||||
const rotationY = frame * 0.02;
|
||||
|
||||
<mesh rotation={[0, rotationY, 0]}>
|
||||
<boxGeometry args={[2, 2, 2]} />
|
||||
<meshStandardMaterial color="#4a9eff" />
|
||||
</mesh>
|
||||
```
|
||||
|
||||
## Using `<Sequence>` inside `<ThreeCanvas>`
|
||||
|
||||
The `layout` prop of any `<Sequence>` inside a `<ThreeCanvas>` must be set to `none`.
|
||||
|
||||
```tsx
|
||||
import { Sequence } from "remotion";
|
||||
import { ThreeCanvas } from "@remotion/three";
|
||||
|
||||
const { width, height } = useVideoConfig();
|
||||
|
||||
<ThreeCanvas width={width} height={height}>
|
||||
<Sequence layout="none">
|
||||
<mesh>
|
||||
<boxGeometry args={[2, 2, 2]} />
|
||||
<meshStandardMaterial color="#4a9eff" />
|
||||
</mesh>
|
||||
</Sequence>
|
||||
</ThreeCanvas>
|
||||
```
|
||||
29
.claude/skills/remotion-best-practices/rules/animations.md
Normal file
29
.claude/skills/remotion-best-practices/rules/animations.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
name: animations
|
||||
description: Fundamental animation skills for Remotion
|
||||
metadata:
|
||||
tags: animations, transitions, frames, useCurrentFrame
|
||||
---
|
||||
|
||||
All animations MUST be driven by the `useCurrentFrame()` hook.
|
||||
Write animations in seconds and multiply them by the `fps` value from `useVideoConfig()`.
|
||||
|
||||
```tsx
|
||||
import { useCurrentFrame } from "remotion";
|
||||
|
||||
export const FadeIn = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const opacity = interpolate(frame, [0, 2 * fps], [0, 1], {
|
||||
extrapolateRight: 'clamp',
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{ opacity }}>Hello World!</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
CSS transitions or animations are FORBIDDEN - they will not render correctly.
|
||||
Tailwind animation class names are FORBIDDEN - they will not render correctly.
|
||||
78
.claude/skills/remotion-best-practices/rules/assets.md
Normal file
78
.claude/skills/remotion-best-practices/rules/assets.md
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
---
|
||||
name: assets
|
||||
description: Importing images, videos, audio, and fonts into Remotion
|
||||
metadata:
|
||||
tags: assets, staticFile, images, fonts, public
|
||||
---
|
||||
|
||||
# Importing assets in Remotion
|
||||
|
||||
## The public folder
|
||||
|
||||
Place assets in the `public/` folder at your project root.
|
||||
|
||||
## Using staticFile()
|
||||
|
||||
You MUST use `staticFile()` to reference files from the `public/` folder:
|
||||
|
||||
```tsx
|
||||
import {Img, staticFile} from 'remotion';
|
||||
|
||||
export const MyComposition = () => {
|
||||
return <Img src={staticFile('logo.png')} />;
|
||||
};
|
||||
```
|
||||
|
||||
The function returns an encoded URL that works correctly when deploying to subdirectories.
|
||||
|
||||
## Using with components
|
||||
|
||||
**Images:**
|
||||
|
||||
```tsx
|
||||
import {Img, staticFile} from 'remotion';
|
||||
|
||||
<Img src={staticFile('photo.png')} />;
|
||||
```
|
||||
|
||||
**Videos:**
|
||||
|
||||
```tsx
|
||||
import {Video} from '@remotion/media';
|
||||
import {staticFile} from 'remotion';
|
||||
|
||||
<Video src={staticFile('clip.mp4')} />;
|
||||
```
|
||||
|
||||
**Audio:**
|
||||
|
||||
```tsx
|
||||
import {Audio} from '@remotion/media';
|
||||
import {staticFile} from 'remotion';
|
||||
|
||||
<Audio src={staticFile('music.mp3')} />;
|
||||
```
|
||||
|
||||
**Fonts:**
|
||||
|
||||
```tsx
|
||||
import {staticFile} from 'remotion';
|
||||
|
||||
const fontFamily = new FontFace('MyFont', `url(${staticFile('font.woff2')})`);
|
||||
await fontFamily.load();
|
||||
document.fonts.add(fontFamily);
|
||||
```
|
||||
|
||||
## Remote URLs
|
||||
|
||||
Remote URLs can be used directly without `staticFile()`:
|
||||
|
||||
```tsx
|
||||
<Img src="https://example.com/image.png" />
|
||||
<Video src="https://remotion.media/video.mp4" />
|
||||
```
|
||||
|
||||
## Important notes
|
||||
|
||||
- Remotion components (`<Img>`, `<Video>`, `<Audio>`) ensure assets are fully loaded before rendering
|
||||
- Special characters in filenames (`#`, `?`, `&`) are automatically encoded
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
import { loadFont } from '@remotion/google-fonts/Inter';
|
||||
import { AbsoluteFill, spring, useCurrentFrame, useVideoConfig } from 'remotion';
|
||||
|
||||
const { fontFamily } = loadFont();
|
||||
|
||||
const COLOR_BAR = '#D4AF37';
|
||||
const COLOR_TEXT = '#ffffff';
|
||||
const COLOR_MUTED = '#888888';
|
||||
const COLOR_BG = '#0a0a0a';
|
||||
const COLOR_AXIS = '#333333';
|
||||
|
||||
// Ideal composition size: 1280x720
|
||||
|
||||
const Title: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
||||
<div style={{ textAlign: 'center', marginBottom: 40 }}>
|
||||
<div style={{ color: COLOR_TEXT, fontSize: 48, fontWeight: 600 }}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const YAxis: React.FC<{ steps: number[]; height: number }> = ({ steps, height }) => (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
height,
|
||||
paddingRight: 16,
|
||||
}}
|
||||
>
|
||||
{steps
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((step) => (
|
||||
<div
|
||||
key={step}
|
||||
style={{
|
||||
color: COLOR_MUTED,
|
||||
fontSize: 20,
|
||||
textAlign: 'right',
|
||||
}}
|
||||
>
|
||||
{step.toLocaleString()}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
const Bar: React.FC<{
|
||||
height: number;
|
||||
progress: number;
|
||||
}> = ({ height, progress }) => (
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height,
|
||||
backgroundColor: COLOR_BAR,
|
||||
borderRadius: '8px 8px 0 0',
|
||||
opacity: progress,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const XAxis: React.FC<{
|
||||
children: React.ReactNode;
|
||||
labels: string[];
|
||||
height: number;
|
||||
}> = ({ children, labels, height }) => (
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-end',
|
||||
gap: 16,
|
||||
height,
|
||||
borderLeft: `2px solid ${COLOR_AXIS}`,
|
||||
borderBottom: `2px solid ${COLOR_AXIS}`,
|
||||
paddingLeft: 16,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 16,
|
||||
paddingLeft: 16,
|
||||
marginTop: 12,
|
||||
}}
|
||||
>
|
||||
{labels.map((label) => (
|
||||
<div
|
||||
key={label}
|
||||
style={{
|
||||
flex: 1,
|
||||
textAlign: 'center',
|
||||
color: COLOR_MUTED,
|
||||
fontSize: 20,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const MyAnimation = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps, height } = useVideoConfig();
|
||||
|
||||
const data = [
|
||||
{ month: 'Jan', price: 2039 },
|
||||
{ month: 'Mar', price: 2160 },
|
||||
{ month: 'May', price: 2327 },
|
||||
{ month: 'Jul', price: 2426 },
|
||||
{ month: 'Sep', price: 2634 },
|
||||
{ month: 'Nov', price: 2672 },
|
||||
];
|
||||
|
||||
const minPrice = 2000;
|
||||
const maxPrice = 2800;
|
||||
const priceRange = maxPrice - minPrice;
|
||||
const chartHeight = height - 280;
|
||||
const yAxisSteps = [2000, 2400, 2800];
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
backgroundColor: COLOR_BG,
|
||||
padding: 60,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
fontFamily,
|
||||
}}
|
||||
>
|
||||
<Title>Gold Price 2024</Title>
|
||||
|
||||
<div style={{ display: 'flex', flex: 1 }}>
|
||||
<YAxis steps={yAxisSteps} height={chartHeight} />
|
||||
<XAxis height={chartHeight} labels={data.map((d) => d.month)}>
|
||||
{data.map((item, i) => {
|
||||
const progress = spring({
|
||||
frame: frame - i * 5 - 10,
|
||||
fps,
|
||||
config: { damping: 18, stiffness: 80 },
|
||||
});
|
||||
|
||||
const barHeight = ((item.price - minPrice) / priceRange) * chartHeight * progress;
|
||||
|
||||
return <Bar key={item.month} height={barHeight} progress={progress} />;
|
||||
})}
|
||||
</XAxis>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
import { AbsoluteFill, interpolate, useCurrentFrame, useVideoConfig } from 'remotion';
|
||||
|
||||
const COLOR_BG = '#ffffff';
|
||||
const COLOR_TEXT = '#000000';
|
||||
const FULL_TEXT = 'From prompt to motion graphics. This is Remotion.';
|
||||
const PAUSE_AFTER = 'From prompt to motion graphics.';
|
||||
const FONT_SIZE = 72;
|
||||
const FONT_WEIGHT = 700;
|
||||
const CHAR_FRAMES = 2;
|
||||
const CURSOR_BLINK_FRAMES = 16;
|
||||
const PAUSE_SECONDS = 1;
|
||||
|
||||
// Ideal composition size: 1280x720
|
||||
|
||||
const getTypedText = ({
|
||||
frame,
|
||||
fullText,
|
||||
pauseAfter,
|
||||
charFrames,
|
||||
pauseFrames,
|
||||
}: {
|
||||
frame: number;
|
||||
fullText: string;
|
||||
pauseAfter: string;
|
||||
charFrames: number;
|
||||
pauseFrames: number;
|
||||
}): string => {
|
||||
const pauseIndex = fullText.indexOf(pauseAfter);
|
||||
const preLen = pauseIndex >= 0 ? pauseIndex + pauseAfter.length : fullText.length;
|
||||
|
||||
let typedChars = 0;
|
||||
if (frame < preLen * charFrames) {
|
||||
typedChars = Math.floor(frame / charFrames);
|
||||
} else if (frame < preLen * charFrames + pauseFrames) {
|
||||
typedChars = preLen;
|
||||
} else {
|
||||
const postPhase = frame - preLen * charFrames - pauseFrames;
|
||||
typedChars = Math.min(fullText.length, preLen + Math.floor(postPhase / charFrames));
|
||||
}
|
||||
return fullText.slice(0, typedChars);
|
||||
};
|
||||
|
||||
const Cursor: React.FC<{
|
||||
frame: number;
|
||||
blinkFrames: number;
|
||||
symbol?: string;
|
||||
}> = ({ frame, blinkFrames, symbol = '\u258C' }) => {
|
||||
const opacity = interpolate(frame % blinkFrames, [0, blinkFrames / 2, blinkFrames], [1, 0, 1], {
|
||||
extrapolateLeft: 'clamp',
|
||||
extrapolateRight: 'clamp',
|
||||
});
|
||||
|
||||
return <span style={{ opacity }}>{symbol}</span>;
|
||||
};
|
||||
|
||||
export const MyAnimation = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const pauseFrames = Math.round(fps * PAUSE_SECONDS);
|
||||
|
||||
const typedText = getTypedText({
|
||||
frame,
|
||||
fullText: FULL_TEXT,
|
||||
pauseAfter: PAUSE_AFTER,
|
||||
charFrames: CHAR_FRAMES,
|
||||
pauseFrames,
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
backgroundColor: COLOR_BG,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
color: COLOR_TEXT,
|
||||
fontSize: FONT_SIZE,
|
||||
fontWeight: FONT_WEIGHT,
|
||||
fontFamily: 'sans-serif',
|
||||
}}
|
||||
>
|
||||
<span>{typedText}</span>
|
||||
<Cursor frame={frame} blinkFrames={CURSOR_BLINK_FRAMES} />
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
import { loadFont } from '@remotion/google-fonts/Inter';
|
||||
import type React from 'react';
|
||||
import { AbsoluteFill, spring, useCurrentFrame, useVideoConfig } from 'remotion';
|
||||
|
||||
/*
|
||||
* Highlight a word in a sentence with a spring-animated wipe effect.
|
||||
*/
|
||||
|
||||
// Ideal composition size: 1280x720
|
||||
|
||||
const COLOR_BG = '#ffffff';
|
||||
const COLOR_TEXT = '#000000';
|
||||
const COLOR_HIGHLIGHT = '#A7C7E7';
|
||||
const FULL_TEXT = 'This is Remotion.';
|
||||
const HIGHLIGHT_WORD = 'Remotion';
|
||||
const FONT_SIZE = 72;
|
||||
const FONT_WEIGHT = 700;
|
||||
const HIGHLIGHT_START_FRAME = 30;
|
||||
const HIGHLIGHT_WIPE_DURATION = 18;
|
||||
|
||||
const { fontFamily } = loadFont();
|
||||
|
||||
const Highlight: React.FC<{
|
||||
word: string;
|
||||
color: string;
|
||||
delay: number;
|
||||
durationInFrames: number;
|
||||
}> = ({ word, color, delay, durationInFrames }) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const highlightProgress = spring({
|
||||
fps,
|
||||
frame,
|
||||
config: { damping: 200 },
|
||||
delay,
|
||||
durationInFrames,
|
||||
});
|
||||
const scaleX = Math.max(0, Math.min(1, highlightProgress));
|
||||
|
||||
return (
|
||||
<span style={{ position: 'relative', display: 'inline-block' }}>
|
||||
<span
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: '50%',
|
||||
height: '1.05em',
|
||||
transform: `translateY(-50%) scaleX(${scaleX})`,
|
||||
transformOrigin: 'left center',
|
||||
backgroundColor: color,
|
||||
borderRadius: '0.18em',
|
||||
zIndex: 0,
|
||||
}}
|
||||
/>
|
||||
<span style={{ position: 'relative', zIndex: 1 }}>{word}</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export const MyAnimation = () => {
|
||||
const highlightIndex = FULL_TEXT.indexOf(HIGHLIGHT_WORD);
|
||||
const hasHighlight = highlightIndex >= 0;
|
||||
const preText = hasHighlight ? FULL_TEXT.slice(0, highlightIndex) : FULL_TEXT;
|
||||
const postText = hasHighlight ? FULL_TEXT.slice(highlightIndex + HIGHLIGHT_WORD.length) : '';
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
backgroundColor: COLOR_BG,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontFamily,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
color: COLOR_TEXT,
|
||||
fontSize: FONT_SIZE,
|
||||
fontWeight: FONT_WEIGHT,
|
||||
}}
|
||||
>
|
||||
{hasHighlight ? (
|
||||
<>
|
||||
<span>{preText}</span>
|
||||
<Highlight
|
||||
word={HIGHLIGHT_WORD}
|
||||
color={COLOR_HIGHLIGHT}
|
||||
delay={HIGHLIGHT_START_FRAME}
|
||||
durationInFrames={HIGHLIGHT_WIPE_DURATION}
|
||||
/>
|
||||
<span>{postText}</span>
|
||||
</>
|
||||
) : (
|
||||
<span>{FULL_TEXT}</span>
|
||||
)}
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
172
.claude/skills/remotion-best-practices/rules/audio.md
Normal file
172
.claude/skills/remotion-best-practices/rules/audio.md
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
---
|
||||
name: audio
|
||||
description: Using audio and sound in Remotion - importing, trimming, volume, speed, pitch
|
||||
metadata:
|
||||
tags: audio, media, trim, volume, speed, loop, pitch, mute, sound, sfx
|
||||
---
|
||||
|
||||
# Using audio in Remotion
|
||||
|
||||
## Prerequisites
|
||||
|
||||
First, the @remotion/media package needs to be installed.
|
||||
If it is not installed, use the following command:
|
||||
|
||||
```bash
|
||||
npx remotion add @remotion/media # If project uses npm
|
||||
bunx remotion add @remotion/media # If project uses bun
|
||||
yarn remotion add @remotion/media # If project uses yarn
|
||||
pnpm exec remotion add @remotion/media # If project uses pnpm
|
||||
```
|
||||
|
||||
## Importing Audio
|
||||
|
||||
Use `<Audio>` from `@remotion/media` to add audio to your composition.
|
||||
|
||||
```tsx
|
||||
import { Audio } from "@remotion/media";
|
||||
import { staticFile } from "remotion";
|
||||
|
||||
export const MyComposition = () => {
|
||||
return <Audio src={staticFile("audio.mp3")} />;
|
||||
};
|
||||
```
|
||||
|
||||
Remote URLs are also supported:
|
||||
|
||||
```tsx
|
||||
<Audio src="https://remotion.media/audio.mp3" />
|
||||
```
|
||||
|
||||
By default, audio plays from the start, at full volume and full length.
|
||||
Multiple audio tracks can be layered by adding multiple `<Audio>` components.
|
||||
|
||||
## Trimming
|
||||
|
||||
Use `trimBefore` and `trimAfter` to remove portions of the audio. Values are in frames.
|
||||
|
||||
```tsx
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
return (
|
||||
<Audio
|
||||
src={staticFile("audio.mp3")}
|
||||
trimBefore={2 * fps} // Skip the first 2 seconds
|
||||
trimAfter={10 * fps} // End at the 10 second mark
|
||||
/>
|
||||
);
|
||||
```
|
||||
|
||||
The audio still starts playing at the beginning of the composition - only the specified portion is played.
|
||||
|
||||
## Delaying
|
||||
|
||||
Wrap the audio in a `<Sequence>` to delay when it starts:
|
||||
|
||||
```tsx
|
||||
import { Sequence, staticFile } from "remotion";
|
||||
import { Audio } from "@remotion/media";
|
||||
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
return (
|
||||
<Sequence from={1 * fps}>
|
||||
<Audio src={staticFile("audio.mp3")} />
|
||||
</Sequence>
|
||||
);
|
||||
```
|
||||
|
||||
The audio will start playing after 1 second.
|
||||
|
||||
## Volume
|
||||
|
||||
Set a static volume (0 to 1):
|
||||
|
||||
```tsx
|
||||
<Audio src={staticFile("audio.mp3")} volume={0.5} />
|
||||
```
|
||||
|
||||
Or use a callback for dynamic volume based on the current frame:
|
||||
|
||||
```tsx
|
||||
import { interpolate } from "remotion";
|
||||
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
return (
|
||||
<Audio
|
||||
src={staticFile("audio.mp3")}
|
||||
volume={(f) =>
|
||||
interpolate(f, [0, 1 * fps], [0, 1], { extrapolateRight: "clamp" })
|
||||
}
|
||||
/>
|
||||
);
|
||||
```
|
||||
|
||||
The value of `f` starts at 0 when the audio begins to play, not the composition frame.
|
||||
|
||||
## Muting
|
||||
|
||||
Use `muted` to silence the audio. It can be set dynamically:
|
||||
|
||||
```tsx
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
return (
|
||||
<Audio
|
||||
src={staticFile("audio.mp3")}
|
||||
muted={frame >= 2 * fps && frame <= 4 * fps} // Mute between 2s and 4s
|
||||
/>
|
||||
);
|
||||
```
|
||||
|
||||
## Speed
|
||||
|
||||
Use `playbackRate` to change the playback speed:
|
||||
|
||||
```tsx
|
||||
<Audio src={staticFile("audio.mp3")} playbackRate={2} /> {/* 2x speed */}
|
||||
<Audio src={staticFile("audio.mp3")} playbackRate={0.5} /> {/* Half speed */}
|
||||
```
|
||||
|
||||
Reverse playback is not supported.
|
||||
|
||||
## Looping
|
||||
|
||||
Use `loop` to loop the audio indefinitely:
|
||||
|
||||
```tsx
|
||||
<Audio src={staticFile("audio.mp3")} loop />
|
||||
```
|
||||
|
||||
Use `loopVolumeCurveBehavior` to control how the frame count behaves when looping:
|
||||
|
||||
- `"repeat"`: Frame count resets to 0 each loop (default)
|
||||
- `"extend"`: Frame count continues incrementing
|
||||
|
||||
```tsx
|
||||
<Audio
|
||||
src={staticFile("audio.mp3")}
|
||||
loop
|
||||
loopVolumeCurveBehavior="extend"
|
||||
volume={(f) => interpolate(f, [0, 300], [1, 0])} // Fade out over multiple loops
|
||||
/>
|
||||
```
|
||||
|
||||
## Pitch
|
||||
|
||||
Use `toneFrequency` to adjust the pitch without affecting speed. Values range from 0.01 to 2:
|
||||
|
||||
```tsx
|
||||
<Audio
|
||||
src={staticFile("audio.mp3")}
|
||||
toneFrequency={1.5} // Higher pitch
|
||||
/>
|
||||
<Audio
|
||||
src={staticFile("audio.mp3")}
|
||||
toneFrequency={0.8} // Lower pitch
|
||||
/>
|
||||
```
|
||||
|
||||
Pitch shifting only works during server-side rendering, not in the Remotion Studio preview or in the `<Player />`.
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
---
|
||||
name: calculate-metadata
|
||||
description: Dynamically set composition duration, dimensions, and props
|
||||
metadata:
|
||||
tags: calculateMetadata, duration, dimensions, props, dynamic
|
||||
---
|
||||
|
||||
# Using calculateMetadata
|
||||
|
||||
Use `calculateMetadata` on a `<Composition>` to dynamically set duration, dimensions, and transform props before rendering.
|
||||
|
||||
```tsx
|
||||
<Composition id="MyComp" component={MyComponent} durationInFrames={300} fps={30} width={1920} height={1080} defaultProps={{videoSrc: 'https://remotion.media/video.mp4'}} calculateMetadata={calculateMetadata} />
|
||||
```
|
||||
|
||||
## Setting duration based on a video
|
||||
|
||||
Use the `getMediaMetadata()` function from the mediabunny/metadata skill to get the video duration:
|
||||
|
||||
```tsx
|
||||
import {CalculateMetadataFunction} from 'remotion';
|
||||
import {getMediaMetadata} from '../get-media-metadata';
|
||||
|
||||
const calculateMetadata: CalculateMetadataFunction<Props> = async ({props}) => {
|
||||
const {durationInSeconds} = await getMediaMetadata(props.videoSrc);
|
||||
|
||||
return {
|
||||
durationInFrames: Math.ceil(durationInSeconds * 30),
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## Matching dimensions of a video
|
||||
|
||||
```tsx
|
||||
const calculateMetadata: CalculateMetadataFunction<Props> = async ({props}) => {
|
||||
const {durationInSeconds, dimensions} = await getMediaMetadata(props.videoSrc);
|
||||
|
||||
return {
|
||||
durationInFrames: Math.ceil(durationInSeconds * 30),
|
||||
width: dimensions?.width ?? 1920,
|
||||
height: dimensions?.height ?? 1080,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## Setting duration based on multiple videos
|
||||
|
||||
```tsx
|
||||
const calculateMetadata: CalculateMetadataFunction<Props> = async ({props}) => {
|
||||
const metadataPromises = props.videos.map((video) => getMediaMetadata(video.src));
|
||||
const allMetadata = await Promise.all(metadataPromises);
|
||||
|
||||
const totalDuration = allMetadata.reduce((sum, meta) => sum + meta.durationInSeconds, 0);
|
||||
|
||||
return {
|
||||
durationInFrames: Math.ceil(totalDuration * 30),
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## Setting a default outName
|
||||
|
||||
Set the default output filename based on props:
|
||||
|
||||
```tsx
|
||||
const calculateMetadata: CalculateMetadataFunction<Props> = async ({props}) => {
|
||||
return {
|
||||
defaultOutName: `video-${props.id}.mp4`,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## Transforming props
|
||||
|
||||
Fetch data or transform props before rendering:
|
||||
|
||||
```tsx
|
||||
const calculateMetadata: CalculateMetadataFunction<Props> = async ({props, abortSignal}) => {
|
||||
const response = await fetch(props.dataUrl, {signal: abortSignal});
|
||||
const data = await response.json();
|
||||
|
||||
return {
|
||||
props: {
|
||||
...props,
|
||||
fetchedData: data,
|
||||
},
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
The `abortSignal` cancels stale requests when props change in the Studio.
|
||||
|
||||
## Return value
|
||||
|
||||
All fields are optional. Returned values override the `<Composition>` props:
|
||||
|
||||
- `durationInFrames`: Number of frames
|
||||
- `width`: Composition width in pixels
|
||||
- `height`: Composition height in pixels
|
||||
- `fps`: Frames per second
|
||||
- `props`: Transformed props passed to the component
|
||||
- `defaultOutName`: Default output filename
|
||||
- `defaultCodec`: Default codec for rendering
|
||||
75
.claude/skills/remotion-best-practices/rules/can-decode.md
Normal file
75
.claude/skills/remotion-best-practices/rules/can-decode.md
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
---
|
||||
name: can-decode
|
||||
description: Check if a video can be decoded by the browser using Mediabunny
|
||||
metadata:
|
||||
tags: decode, validation, video, audio, compatibility, browser
|
||||
---
|
||||
|
||||
# Checking if a video can be decoded
|
||||
|
||||
Use Mediabunny to check if a video can be decoded by the browser before attempting to play it.
|
||||
|
||||
## The `canDecode()` function
|
||||
|
||||
This function can be copy-pasted into any project.
|
||||
|
||||
```tsx
|
||||
import { Input, ALL_FORMATS, UrlSource } from "mediabunny";
|
||||
|
||||
export const canDecode = async (src: string) => {
|
||||
const input = new Input({
|
||||
formats: ALL_FORMATS,
|
||||
source: new UrlSource(src, {
|
||||
getRetryDelay: () => null,
|
||||
}),
|
||||
});
|
||||
|
||||
try {
|
||||
await input.getFormat();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
const videoTrack = await input.getPrimaryVideoTrack();
|
||||
if (videoTrack && !(await videoTrack.canDecode())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const audioTrack = await input.getPrimaryAudioTrack();
|
||||
if (audioTrack && !(await audioTrack.canDecode())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
const src = "https://remotion.media/video.mp4";
|
||||
const isDecodable = await canDecode(src);
|
||||
|
||||
if (isDecodable) {
|
||||
console.log("Video can be decoded");
|
||||
} else {
|
||||
console.log("Video cannot be decoded by this browser");
|
||||
}
|
||||
```
|
||||
|
||||
## Using with Blob
|
||||
|
||||
For file uploads or drag-and-drop, use `BlobSource`:
|
||||
|
||||
```tsx
|
||||
import { Input, ALL_FORMATS, BlobSource } from "mediabunny";
|
||||
|
||||
export const canDecodeBlob = async (blob: Blob) => {
|
||||
const input = new Input({
|
||||
formats: ALL_FORMATS,
|
||||
source: new BlobSource(blob),
|
||||
});
|
||||
|
||||
// Same validation logic as above
|
||||
};
|
||||
```
|
||||
58
.claude/skills/remotion-best-practices/rules/charts.md
Normal file
58
.claude/skills/remotion-best-practices/rules/charts.md
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
---
|
||||
name: charts
|
||||
description: Chart and data visualization patterns for Remotion. Use when creating bar charts, pie charts, histograms, progress bars, or any data-driven animations.
|
||||
metadata:
|
||||
tags: charts, data, visualization, bar-chart, pie-chart, graphs
|
||||
---
|
||||
|
||||
# Charts in Remotion
|
||||
|
||||
You can create bar charts in Remotion by using regular React code - HTML and SVG is allowed, as well as D3.js.
|
||||
|
||||
## No animations not powered by `useCurrentFrame()`
|
||||
|
||||
Disable all animations by third party libraries.
|
||||
They will cause flickering during rendering.
|
||||
Instead, drive all animations from `useCurrentFrame()`.
|
||||
|
||||
## Bar Chart Animations
|
||||
|
||||
See [Bar Chart Example](assets/charts/bar-chart.tsx) for a basic example implmentation.
|
||||
|
||||
### Staggered Bars
|
||||
|
||||
You can animate the height of the bars and stagger them like this:
|
||||
|
||||
```tsx
|
||||
const STAGGER_DELAY = 5;
|
||||
const frame = useCurrentFrame();
|
||||
const {fps} = useVideoConfig();
|
||||
|
||||
const bars = data.map((item, i) => {
|
||||
const delay = i * STAGGER_DELAY;
|
||||
const height = spring({
|
||||
frame,
|
||||
fps,
|
||||
delay,
|
||||
config: {damping: 200},
|
||||
});
|
||||
return <div style={{height: height * item.value}} />;
|
||||
});
|
||||
```
|
||||
|
||||
## Pie Chart Animation
|
||||
|
||||
Animate segments using stroke-dashoffset, starting from 12 o'clock.
|
||||
|
||||
```tsx
|
||||
const frame = useCurrentFrame();
|
||||
const {fps} = useVideoConfig();
|
||||
|
||||
const progress = interpolate(frame, [0, 100], [0, 1]);
|
||||
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
const segmentLength = (value / total) * circumference;
|
||||
const offset = interpolate(progress, [0, 1], [segmentLength, 0]);
|
||||
|
||||
<circle r={radius} cx={center} cy={center} fill="none" stroke={color} strokeWidth={strokeWidth} strokeDasharray={`${segmentLength} ${circumference}`} strokeDashoffset={offset} transform={`rotate(-90 ${center} ${center})`} />;
|
||||
```
|
||||
146
.claude/skills/remotion-best-practices/rules/compositions.md
Normal file
146
.claude/skills/remotion-best-practices/rules/compositions.md
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
---
|
||||
name: compositions
|
||||
description: Defining compositions, stills, folders, default props and dynamic metadata
|
||||
metadata:
|
||||
tags: composition, still, folder, props, metadata
|
||||
---
|
||||
|
||||
A `<Composition>` defines the component, width, height, fps and duration of a renderable video.
|
||||
|
||||
It normally is placed in the `src/Root.tsx` file.
|
||||
|
||||
```tsx
|
||||
import { Composition } from "remotion";
|
||||
import { MyComposition } from "./MyComposition";
|
||||
|
||||
export const RemotionRoot = () => {
|
||||
return (
|
||||
<Composition
|
||||
id="MyComposition"
|
||||
component={MyComposition}
|
||||
durationInFrames={100}
|
||||
fps={30}
|
||||
width={1080}
|
||||
height={1080}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Default Props
|
||||
|
||||
Pass `defaultProps` to provide initial values for your component.
|
||||
Values must be JSON-serializable (`Date`, `Map`, `Set`, and `staticFile()` are supported).
|
||||
|
||||
```tsx
|
||||
import { Composition } from "remotion";
|
||||
import { MyComposition, MyCompositionProps } from "./MyComposition";
|
||||
|
||||
export const RemotionRoot = () => {
|
||||
return (
|
||||
<Composition
|
||||
id="MyComposition"
|
||||
component={MyComposition}
|
||||
durationInFrames={100}
|
||||
fps={30}
|
||||
width={1080}
|
||||
height={1080}
|
||||
defaultProps={{
|
||||
title: "Hello World",
|
||||
color: "#ff0000",
|
||||
} satisfies MyCompositionProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
Use `type` declarations for props rather than `interface` to ensure `defaultProps` type safety.
|
||||
|
||||
## Folders
|
||||
|
||||
Use `<Folder>` to organize compositions in the sidebar.
|
||||
Folder names can only contain letters, numbers, and hyphens.
|
||||
|
||||
```tsx
|
||||
import { Composition, Folder } from "remotion";
|
||||
|
||||
export const RemotionRoot = () => {
|
||||
return (
|
||||
<>
|
||||
<Folder name="Marketing">
|
||||
<Composition id="Promo" /* ... */ />
|
||||
<Composition id="Ad" /* ... */ />
|
||||
</Folder>
|
||||
<Folder name="Social">
|
||||
<Folder name="Instagram">
|
||||
<Composition id="Story" /* ... */ />
|
||||
<Composition id="Reel" /* ... */ />
|
||||
</Folder>
|
||||
</Folder>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Stills
|
||||
|
||||
Use `<Still>` for single-frame images. It does not require `durationInFrames` or `fps`.
|
||||
|
||||
```tsx
|
||||
import { Still } from "remotion";
|
||||
import { Thumbnail } from "./Thumbnail";
|
||||
|
||||
export const RemotionRoot = () => {
|
||||
return (
|
||||
<Still
|
||||
id="Thumbnail"
|
||||
component={Thumbnail}
|
||||
width={1280}
|
||||
height={720}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Calculate Metadata
|
||||
|
||||
Use `calculateMetadata` to make dimensions, duration, or props dynamic based on data.
|
||||
|
||||
```tsx
|
||||
import { Composition, CalculateMetadataFunction } from "remotion";
|
||||
import { MyComposition, MyCompositionProps } from "./MyComposition";
|
||||
|
||||
const calculateMetadata: CalculateMetadataFunction<MyCompositionProps> = async ({
|
||||
props,
|
||||
abortSignal,
|
||||
}) => {
|
||||
const data = await fetch(`https://api.example.com/video/${props.videoId}`, {
|
||||
signal: abortSignal,
|
||||
}).then((res) => res.json());
|
||||
|
||||
return {
|
||||
durationInFrames: Math.ceil(data.duration * 30),
|
||||
props: {
|
||||
...props,
|
||||
videoUrl: data.url,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const RemotionRoot = () => {
|
||||
return (
|
||||
<Composition
|
||||
id="MyComposition"
|
||||
component={MyComposition}
|
||||
durationInFrames={100} // Placeholder, will be overridden
|
||||
fps={30}
|
||||
width={1080}
|
||||
height={1080}
|
||||
defaultProps={{ videoId: "abc123" }}
|
||||
calculateMetadata={calculateMetadata}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
The function can return `props`, `durationInFrames`, `width`, `height`, `fps`, and codec-related defaults. It runs once before rendering begins.
|
||||
126
.claude/skills/remotion-best-practices/rules/display-captions.md
Normal file
126
.claude/skills/remotion-best-practices/rules/display-captions.md
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
---
|
||||
name: display-captions
|
||||
description: Displaying captions in Remotion with TikTok-style pages and word highlighting
|
||||
metadata:
|
||||
tags: captions, subtitles, display, tiktok, highlight
|
||||
---
|
||||
|
||||
# Displaying captions in Remotion
|
||||
|
||||
This guide explains how to display captions in Remotion, assuming you already have captions in the `Caption` format.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
First, the @remotion/captions package needs to be installed.
|
||||
If it is not installed, use the following command:
|
||||
|
||||
```bash
|
||||
npx remotion add @remotion/captions # If project uses npm
|
||||
bunx remotion add @remotion/captions # If project uses bun
|
||||
yarn remotion add @remotion/captions # If project uses yarn
|
||||
pnpm exec remotion add @remotion/captions # If project uses pnpm
|
||||
```
|
||||
|
||||
## Creating pages
|
||||
|
||||
Use `createTikTokStyleCaptions()` to group captions into pages. The `combineTokensWithinMilliseconds` option controls how many words appear at once:
|
||||
|
||||
```tsx
|
||||
import {useMemo} from 'react';
|
||||
import {createTikTokStyleCaptions} from '@remotion/captions';
|
||||
import type {Caption} from '@remotion/captions';
|
||||
|
||||
// How often captions should switch (in milliseconds)
|
||||
// Higher values = more words per page
|
||||
// Lower values = fewer words (more word-by-word)
|
||||
const SWITCH_CAPTIONS_EVERY_MS = 1200;
|
||||
|
||||
const {pages} = useMemo(() => {
|
||||
return createTikTokStyleCaptions({
|
||||
captions,
|
||||
combineTokensWithinMilliseconds: SWITCH_CAPTIONS_EVERY_MS,
|
||||
});
|
||||
}, [captions]);
|
||||
```
|
||||
|
||||
## Rendering with Sequences
|
||||
|
||||
Map over the pages and render each one in a `<Sequence>`. Calculate the start frame and duration from the page timing:
|
||||
|
||||
```tsx
|
||||
import {Sequence, useVideoConfig, AbsoluteFill} from 'remotion';
|
||||
import type {TikTokPage} from '@remotion/captions';
|
||||
|
||||
const CaptionedContent: React.FC = () => {
|
||||
const {fps} = useVideoConfig();
|
||||
|
||||
return (
|
||||
<AbsoluteFill>
|
||||
{pages.map((page, index) => {
|
||||
const nextPage = pages[index + 1] ?? null;
|
||||
const startFrame = (page.startMs / 1000) * fps;
|
||||
const endFrame = Math.min(
|
||||
nextPage ? (nextPage.startMs / 1000) * fps : Infinity,
|
||||
startFrame + (SWITCH_CAPTIONS_EVERY_MS / 1000) * fps,
|
||||
);
|
||||
const durationInFrames = endFrame - startFrame;
|
||||
|
||||
if (durationInFrames <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Sequence
|
||||
key={index}
|
||||
from={startFrame}
|
||||
durationInFrames={durationInFrames}
|
||||
>
|
||||
<CaptionPage page={page} />
|
||||
</Sequence>
|
||||
);
|
||||
})}
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Word highlighting
|
||||
|
||||
A caption page contains `tokens` which you can use to highlight the currently spoken word:
|
||||
|
||||
```tsx
|
||||
import {AbsoluteFill, useCurrentFrame, useVideoConfig} from 'remotion';
|
||||
import type {TikTokPage} from '@remotion/captions';
|
||||
|
||||
const HIGHLIGHT_COLOR = '#39E508';
|
||||
|
||||
const CaptionPage: React.FC<{page: TikTokPage}> = ({page}) => {
|
||||
const frame = useCurrentFrame();
|
||||
const {fps} = useVideoConfig();
|
||||
|
||||
// Current time relative to the start of the sequence
|
||||
const currentTimeMs = (frame / fps) * 1000;
|
||||
// Convert to absolute time by adding the page start
|
||||
const absoluteTimeMs = page.startMs + currentTimeMs;
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{justifyContent: 'center', alignItems: 'center'}}>
|
||||
<div style={{fontSize: 80, fontWeight: 'bold', whiteSpace: 'pre'}}>
|
||||
{page.tokens.map((token) => {
|
||||
const isActive =
|
||||
token.fromMs <= absoluteTimeMs && token.toMs > absoluteTimeMs;
|
||||
|
||||
return (
|
||||
<span
|
||||
key={token.fromMs}
|
||||
style={{color: isActive ? HIGHLIGHT_COLOR : 'white'}}
|
||||
>
|
||||
{token.text}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
```
|
||||
229
.claude/skills/remotion-best-practices/rules/extract-frames.md
Normal file
229
.claude/skills/remotion-best-practices/rules/extract-frames.md
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
---
|
||||
name: extract-frames
|
||||
description: Extract frames from videos at specific timestamps using Mediabunny
|
||||
metadata:
|
||||
tags: frames, extract, video, thumbnail, filmstrip, canvas
|
||||
---
|
||||
|
||||
# Extracting frames from videos
|
||||
|
||||
Use Mediabunny to extract frames from videos at specific timestamps. This is useful for generating thumbnails, filmstrips, or processing individual frames.
|
||||
|
||||
## The `extractFrames()` function
|
||||
|
||||
This function can be copy-pasted into any project.
|
||||
|
||||
```tsx
|
||||
import {
|
||||
ALL_FORMATS,
|
||||
Input,
|
||||
UrlSource,
|
||||
VideoSample,
|
||||
VideoSampleSink,
|
||||
} from "mediabunny";
|
||||
|
||||
type Options = {
|
||||
track: { width: number; height: number };
|
||||
container: string;
|
||||
durationInSeconds: number | null;
|
||||
};
|
||||
|
||||
export type ExtractFramesTimestampsInSecondsFn = (
|
||||
options: Options
|
||||
) => Promise<number[]> | number[];
|
||||
|
||||
export type ExtractFramesProps = {
|
||||
src: string;
|
||||
timestampsInSeconds: number[] | ExtractFramesTimestampsInSecondsFn;
|
||||
onVideoSample: (sample: VideoSample) => void;
|
||||
signal?: AbortSignal;
|
||||
};
|
||||
|
||||
export async function extractFrames({
|
||||
src,
|
||||
timestampsInSeconds,
|
||||
onVideoSample,
|
||||
signal,
|
||||
}: ExtractFramesProps): Promise<void> {
|
||||
using input = new Input({
|
||||
formats: ALL_FORMATS,
|
||||
source: new UrlSource(src),
|
||||
});
|
||||
|
||||
const [durationInSeconds, format, videoTrack] = await Promise.all([
|
||||
input.computeDuration(),
|
||||
input.getFormat(),
|
||||
input.getPrimaryVideoTrack(),
|
||||
]);
|
||||
|
||||
if (!videoTrack) {
|
||||
throw new Error("No video track found in the input");
|
||||
}
|
||||
|
||||
if (signal?.aborted) {
|
||||
throw new Error("Aborted");
|
||||
}
|
||||
|
||||
const timestamps =
|
||||
typeof timestampsInSeconds === "function"
|
||||
? await timestampsInSeconds({
|
||||
track: {
|
||||
width: videoTrack.displayWidth,
|
||||
height: videoTrack.displayHeight,
|
||||
},
|
||||
container: format.name,
|
||||
durationInSeconds,
|
||||
})
|
||||
: timestampsInSeconds;
|
||||
|
||||
if (timestamps.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (signal?.aborted) {
|
||||
throw new Error("Aborted");
|
||||
}
|
||||
|
||||
const sink = new VideoSampleSink(videoTrack);
|
||||
|
||||
for await (using videoSample of sink.samplesAtTimestamps(timestamps)) {
|
||||
if (signal?.aborted) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!videoSample) {
|
||||
continue;
|
||||
}
|
||||
|
||||
onVideoSample(videoSample);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Basic usage
|
||||
|
||||
Extract frames at specific timestamps:
|
||||
|
||||
```tsx
|
||||
await extractFrames({
|
||||
src: "https://remotion.media/video.mp4",
|
||||
timestampsInSeconds: [0, 1, 2, 3, 4],
|
||||
onVideoSample: (sample) => {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = sample.displayWidth;
|
||||
canvas.height = sample.displayHeight;
|
||||
const ctx = canvas.getContext("2d");
|
||||
sample.draw(ctx!, 0, 0);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Creating a filmstrip
|
||||
|
||||
Use a callback function to dynamically calculate timestamps based on video metadata:
|
||||
|
||||
```tsx
|
||||
const canvasWidth = 500;
|
||||
const canvasHeight = 80;
|
||||
const fromSeconds = 0;
|
||||
const toSeconds = 10;
|
||||
|
||||
await extractFrames({
|
||||
src: "https://remotion.media/video.mp4",
|
||||
timestampsInSeconds: async ({ track, durationInSeconds }) => {
|
||||
const aspectRatio = track.width / track.height;
|
||||
const amountOfFramesFit = Math.ceil(
|
||||
canvasWidth / (canvasHeight * aspectRatio)
|
||||
);
|
||||
const segmentDuration = toSeconds - fromSeconds;
|
||||
const timestamps: number[] = [];
|
||||
|
||||
for (let i = 0; i < amountOfFramesFit; i++) {
|
||||
timestamps.push(
|
||||
fromSeconds + (segmentDuration / amountOfFramesFit) * (i + 0.5)
|
||||
);
|
||||
}
|
||||
|
||||
return timestamps;
|
||||
},
|
||||
onVideoSample: (sample) => {
|
||||
console.log(`Frame at ${sample.timestamp}s`);
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = sample.displayWidth;
|
||||
canvas.height = sample.displayHeight;
|
||||
const ctx = canvas.getContext("2d");
|
||||
sample.draw(ctx!, 0, 0);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Cancellation with AbortSignal
|
||||
|
||||
Cancel frame extraction after a timeout:
|
||||
|
||||
```tsx
|
||||
const controller = new AbortController();
|
||||
|
||||
setTimeout(() => controller.abort(), 5000);
|
||||
|
||||
try {
|
||||
await extractFrames({
|
||||
src: "https://remotion.media/video.mp4",
|
||||
timestampsInSeconds: [0, 1, 2, 3, 4],
|
||||
onVideoSample: (sample) => {
|
||||
using frame = sample;
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = frame.displayWidth;
|
||||
canvas.height = frame.displayHeight;
|
||||
const ctx = canvas.getContext("2d");
|
||||
frame.draw(ctx!, 0, 0);
|
||||
},
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
console.log("Frame extraction complete!");
|
||||
} catch (error) {
|
||||
console.error("Frame extraction was aborted or failed:", error);
|
||||
}
|
||||
```
|
||||
|
||||
## Timeout with Promise.race
|
||||
|
||||
```tsx
|
||||
const controller = new AbortController();
|
||||
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
controller.abort();
|
||||
reject(new Error("Frame extraction timed out after 10 seconds"));
|
||||
}, 10000);
|
||||
|
||||
controller.signal.addEventListener("abort", () => clearTimeout(timeoutId), {
|
||||
once: true,
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.race([
|
||||
extractFrames({
|
||||
src: "https://remotion.media/video.mp4",
|
||||
timestampsInSeconds: [0, 1, 2, 3, 4],
|
||||
onVideoSample: (sample) => {
|
||||
using frame = sample;
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = frame.displayWidth;
|
||||
canvas.height = frame.displayHeight;
|
||||
const ctx = canvas.getContext("2d");
|
||||
frame.draw(ctx!, 0, 0);
|
||||
},
|
||||
signal: controller.signal,
|
||||
}),
|
||||
timeoutPromise,
|
||||
]);
|
||||
|
||||
console.log("Frame extraction complete!");
|
||||
} catch (error) {
|
||||
console.error("Frame extraction was aborted or failed:", error);
|
||||
}
|
||||
```
|
||||
152
.claude/skills/remotion-best-practices/rules/fonts.md
Normal file
152
.claude/skills/remotion-best-practices/rules/fonts.md
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
---
|
||||
name: fonts
|
||||
description: Loading Google Fonts and local fonts in Remotion
|
||||
metadata:
|
||||
tags: fonts, google-fonts, typography, text
|
||||
---
|
||||
|
||||
# Using fonts in Remotion
|
||||
|
||||
## Google Fonts with @remotion/google-fonts
|
||||
|
||||
The recommended way to use Google Fonts. It's type-safe and automatically blocks rendering until the font is ready.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
First, the @remotion/google-fonts package needs to be installed.
|
||||
If it is not installed, use the following command:
|
||||
|
||||
```bash
|
||||
npx remotion add @remotion/google-fonts # If project uses npm
|
||||
bunx remotion add @remotion/google-fonts # If project uses bun
|
||||
yarn remotion add @remotion/google-fonts # If project uses yarn
|
||||
pnpm exec remotion add @remotion/google-fonts # If project uses pnpm
|
||||
```
|
||||
|
||||
```tsx
|
||||
import { loadFont } from "@remotion/google-fonts/Lobster";
|
||||
|
||||
const { fontFamily } = loadFont();
|
||||
|
||||
export const MyComposition = () => {
|
||||
return <div style={{ fontFamily }}>Hello World</div>;
|
||||
};
|
||||
```
|
||||
|
||||
Preferrably, specify only needed weights and subsets to reduce file size:
|
||||
|
||||
```tsx
|
||||
import { loadFont } from "@remotion/google-fonts/Roboto";
|
||||
|
||||
const { fontFamily } = loadFont("normal", {
|
||||
weights: ["400", "700"],
|
||||
subsets: ["latin"],
|
||||
});
|
||||
```
|
||||
|
||||
### Waiting for font to load
|
||||
|
||||
Use `waitUntilDone()` if you need to know when the font is ready:
|
||||
|
||||
```tsx
|
||||
import { loadFont } from "@remotion/google-fonts/Lobster";
|
||||
|
||||
const { fontFamily, waitUntilDone } = loadFont();
|
||||
|
||||
await waitUntilDone();
|
||||
```
|
||||
|
||||
## Local fonts with @remotion/fonts
|
||||
|
||||
For local font files, use the `@remotion/fonts` package.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
First, install @remotion/fonts:
|
||||
|
||||
```bash
|
||||
npx remotion add @remotion/fonts # If project uses npm
|
||||
bunx remotion add @remotion/fonts # If project uses bun
|
||||
yarn remotion add @remotion/fonts # If project uses yarn
|
||||
pnpm exec remotion add @remotion/fonts # If project uses pnpm
|
||||
```
|
||||
|
||||
### Loading a local font
|
||||
|
||||
Place your font file in the `public/` folder and use `loadFont()`:
|
||||
|
||||
```tsx
|
||||
import { loadFont } from "@remotion/fonts";
|
||||
import { staticFile } from "remotion";
|
||||
|
||||
await loadFont({
|
||||
family: "MyFont",
|
||||
url: staticFile("MyFont-Regular.woff2"),
|
||||
});
|
||||
|
||||
export const MyComposition = () => {
|
||||
return <div style={{ fontFamily: "MyFont" }}>Hello World</div>;
|
||||
};
|
||||
```
|
||||
|
||||
### Loading multiple weights
|
||||
|
||||
Load each weight separately with the same family name:
|
||||
|
||||
```tsx
|
||||
import { loadFont } from "@remotion/fonts";
|
||||
import { staticFile } from "remotion";
|
||||
|
||||
await Promise.all([
|
||||
loadFont({
|
||||
family: "Inter",
|
||||
url: staticFile("Inter-Regular.woff2"),
|
||||
weight: "400",
|
||||
}),
|
||||
loadFont({
|
||||
family: "Inter",
|
||||
url: staticFile("Inter-Bold.woff2"),
|
||||
weight: "700",
|
||||
}),
|
||||
]);
|
||||
```
|
||||
|
||||
### Available options
|
||||
|
||||
```tsx
|
||||
loadFont({
|
||||
family: "MyFont", // Required: name to use in CSS
|
||||
url: staticFile("font.woff2"), // Required: font file URL
|
||||
format: "woff2", // Optional: auto-detected from extension
|
||||
weight: "400", // Optional: font weight
|
||||
style: "normal", // Optional: normal or italic
|
||||
display: "block", // Optional: font-display behavior
|
||||
});
|
||||
```
|
||||
|
||||
## Using in components
|
||||
|
||||
Call `loadFont()` at the top level of your component or in a separate file that's imported early:
|
||||
|
||||
```tsx
|
||||
import { loadFont } from "@remotion/google-fonts/Montserrat";
|
||||
|
||||
const { fontFamily } = loadFont("normal", {
|
||||
weights: ["400", "700"],
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const Title: React.FC<{ text: string }> = ({ text }) => {
|
||||
return (
|
||||
<h1
|
||||
style={{
|
||||
fontFamily,
|
||||
fontSize: 80,
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</h1>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
---
|
||||
name: get-audio-duration
|
||||
description: Getting the duration of an audio file in seconds with Mediabunny
|
||||
metadata:
|
||||
tags: duration, audio, length, time, seconds, mp3, wav
|
||||
---
|
||||
|
||||
# Getting audio duration with Mediabunny
|
||||
|
||||
Mediabunny can extract the duration of an audio file. It works in browser, Node.js, and Bun environments.
|
||||
|
||||
## Getting audio duration
|
||||
|
||||
```tsx
|
||||
import { Input, ALL_FORMATS, UrlSource } from "mediabunny";
|
||||
|
||||
export const getAudioDuration = async (src: string) => {
|
||||
const input = new Input({
|
||||
formats: ALL_FORMATS,
|
||||
source: new UrlSource(src, {
|
||||
getRetryDelay: () => null,
|
||||
}),
|
||||
});
|
||||
|
||||
const durationInSeconds = await input.computeDuration();
|
||||
return durationInSeconds;
|
||||
};
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
const duration = await getAudioDuration("https://remotion.media/audio.mp3");
|
||||
console.log(duration); // e.g. 180.5 (seconds)
|
||||
```
|
||||
|
||||
## Using with local files
|
||||
|
||||
For local files, use `FileSource` instead of `UrlSource`:
|
||||
|
||||
```tsx
|
||||
import { Input, ALL_FORMATS, FileSource } from "mediabunny";
|
||||
|
||||
const input = new Input({
|
||||
formats: ALL_FORMATS,
|
||||
source: new FileSource(file), // File object from input or drag-drop
|
||||
});
|
||||
|
||||
const durationInSeconds = await input.computeDuration();
|
||||
```
|
||||
|
||||
## Using with staticFile in Remotion
|
||||
|
||||
```tsx
|
||||
import { staticFile } from "remotion";
|
||||
|
||||
const duration = await getAudioDuration(staticFile("audio.mp3"));
|
||||
```
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
---
|
||||
name: get-video-dimensions
|
||||
description: Getting the width and height of a video file with Mediabunny
|
||||
metadata:
|
||||
tags: dimensions, width, height, resolution, size, video
|
||||
---
|
||||
|
||||
# Getting video dimensions with Mediabunny
|
||||
|
||||
Mediabunny can extract the width and height of a video file. It works in browser, Node.js, and Bun environments.
|
||||
|
||||
## Getting video dimensions
|
||||
|
||||
```tsx
|
||||
import { Input, ALL_FORMATS, UrlSource } from "mediabunny";
|
||||
|
||||
export const getVideoDimensions = async (src: string) => {
|
||||
const input = new Input({
|
||||
formats: ALL_FORMATS,
|
||||
source: new UrlSource(src, {
|
||||
getRetryDelay: () => null,
|
||||
}),
|
||||
});
|
||||
|
||||
const videoTrack = await input.getPrimaryVideoTrack();
|
||||
if (!videoTrack) {
|
||||
throw new Error("No video track found");
|
||||
}
|
||||
|
||||
return {
|
||||
width: videoTrack.displayWidth,
|
||||
height: videoTrack.displayHeight,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
const dimensions = await getVideoDimensions("https://remotion.media/video.mp4");
|
||||
console.log(dimensions.width); // e.g. 1920
|
||||
console.log(dimensions.height); // e.g. 1080
|
||||
```
|
||||
|
||||
## Using with local files
|
||||
|
||||
For local files, use `FileSource` instead of `UrlSource`:
|
||||
|
||||
```tsx
|
||||
import { Input, ALL_FORMATS, FileSource } from "mediabunny";
|
||||
|
||||
const input = new Input({
|
||||
formats: ALL_FORMATS,
|
||||
source: new FileSource(file), // File object from input or drag-drop
|
||||
});
|
||||
|
||||
const videoTrack = await input.getPrimaryVideoTrack();
|
||||
const width = videoTrack.displayWidth;
|
||||
const height = videoTrack.displayHeight;
|
||||
```
|
||||
|
||||
## Using with staticFile in Remotion
|
||||
|
||||
```tsx
|
||||
import { staticFile } from "remotion";
|
||||
|
||||
const dimensions = await getVideoDimensions(staticFile("video.mp4"));
|
||||
```
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
---
|
||||
name: get-video-duration
|
||||
description: Getting the duration of a video file in seconds with Mediabunny
|
||||
metadata:
|
||||
tags: duration, video, length, time, seconds
|
||||
---
|
||||
|
||||
# Getting video duration with Mediabunny
|
||||
|
||||
Mediabunny can extract the duration of a video file. It works in browser, Node.js, and Bun environments.
|
||||
|
||||
## Getting video duration
|
||||
|
||||
```tsx
|
||||
import { Input, ALL_FORMATS, UrlSource } from "mediabunny";
|
||||
|
||||
export const getVideoDuration = async (src: string) => {
|
||||
const input = new Input({
|
||||
formats: ALL_FORMATS,
|
||||
source: new UrlSource(src, {
|
||||
getRetryDelay: () => null,
|
||||
}),
|
||||
});
|
||||
|
||||
const durationInSeconds = await input.computeDuration();
|
||||
return durationInSeconds;
|
||||
};
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
const duration = await getVideoDuration("https://remotion.media/video.mp4");
|
||||
console.log(duration); // e.g. 10.5 (seconds)
|
||||
```
|
||||
|
||||
## Using with local files
|
||||
|
||||
For local files, use `FileSource` instead of `UrlSource`:
|
||||
|
||||
```tsx
|
||||
import { Input, ALL_FORMATS, FileSource } from "mediabunny";
|
||||
|
||||
const input = new Input({
|
||||
formats: ALL_FORMATS,
|
||||
source: new FileSource(file), // File object from input or drag-drop
|
||||
});
|
||||
|
||||
const durationInSeconds = await input.computeDuration();
|
||||
```
|
||||
|
||||
## Using with staticFile in Remotion
|
||||
|
||||
```tsx
|
||||
import { staticFile } from "remotion";
|
||||
|
||||
const duration = await getVideoDuration(staticFile("video.mp4"));
|
||||
```
|
||||
138
.claude/skills/remotion-best-practices/rules/gifs.md
Normal file
138
.claude/skills/remotion-best-practices/rules/gifs.md
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
---
|
||||
name: gif
|
||||
description: Displaying GIFs, APNG, AVIF and WebP in Remotion
|
||||
metadata:
|
||||
tags: gif, animation, images, animated, apng, avif, webp
|
||||
---
|
||||
|
||||
# Using Animated images in Remotion
|
||||
|
||||
## Basic usage
|
||||
|
||||
Use `<AnimatedImage>` to display a GIF, APNG, AVIF or WebP image synchronized with Remotion's timeline:
|
||||
|
||||
```tsx
|
||||
import {AnimatedImage, staticFile} from 'remotion';
|
||||
|
||||
export const MyComposition = () => {
|
||||
return <AnimatedImage src={staticFile('animation.gif')} width={500} height={500} />;
|
||||
};
|
||||
```
|
||||
|
||||
Remote URLs are also supported (must have CORS enabled):
|
||||
|
||||
```tsx
|
||||
<AnimatedImage src="https://example.com/animation.gif" width={500} height={500} />
|
||||
```
|
||||
|
||||
## Sizing and fit
|
||||
|
||||
Control how the image fills its container with the `fit` prop:
|
||||
|
||||
```tsx
|
||||
// Stretch to fill (default)
|
||||
<AnimatedImage src={staticFile("animation.gif")} width={500} height={300} fit="fill" />
|
||||
|
||||
// Maintain aspect ratio, fit inside container
|
||||
<AnimatedImage src={staticFile("animation.gif")} width={500} height={300} fit="contain" />
|
||||
|
||||
// Fill container, crop if needed
|
||||
<AnimatedImage src={staticFile("animation.gif")} width={500} height={300} fit="cover" />
|
||||
```
|
||||
|
||||
## Playback speed
|
||||
|
||||
Use `playbackRate` to control the animation speed:
|
||||
|
||||
```tsx
|
||||
<AnimatedImage src={staticFile("animation.gif")} width={500} height={500} playbackRate={2} /> {/* 2x speed */}
|
||||
<AnimatedImage src={staticFile("animation.gif")} width={500} height={500} playbackRate={0.5} /> {/* Half speed */}
|
||||
```
|
||||
|
||||
## Looping behavior
|
||||
|
||||
Control what happens when the animation finishes:
|
||||
|
||||
```tsx
|
||||
// Loop indefinitely (default)
|
||||
<AnimatedImage src={staticFile("animation.gif")} width={500} height={500} loopBehavior="loop" />
|
||||
|
||||
// Play once, show final frame
|
||||
<AnimatedImage src={staticFile("animation.gif")} width={500} height={500} loopBehavior="pause-after-finish" />
|
||||
|
||||
// Play once, then clear canvas
|
||||
<AnimatedImage src={staticFile("animation.gif")} width={500} height={500} loopBehavior="clear-after-finish" />
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
Use the `style` prop for additional CSS (use `width` and `height` props for sizing):
|
||||
|
||||
```tsx
|
||||
<AnimatedImage
|
||||
src={staticFile('animation.gif')}
|
||||
width={500}
|
||||
height={500}
|
||||
style={{
|
||||
borderRadius: 20,
|
||||
position: 'absolute',
|
||||
top: 100,
|
||||
left: 50,
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
## Getting GIF duration
|
||||
|
||||
Use `getGifDurationInSeconds()` from `@remotion/gif` to get the duration of a GIF.
|
||||
|
||||
```bash
|
||||
npx remotion add @remotion/gif # If project uses npm
|
||||
bunx remotion add @remotion/gif # If project uses bun
|
||||
yarn remotion add @remotion/gif # If project uses yarn
|
||||
pnpm exec remotion add @remotion/gif # If project uses pnpm
|
||||
```
|
||||
|
||||
```tsx
|
||||
import {getGifDurationInSeconds} from '@remotion/gif';
|
||||
import {staticFile} from 'remotion';
|
||||
|
||||
const duration = await getGifDurationInSeconds(staticFile('animation.gif'));
|
||||
console.log(duration); // e.g. 2.5
|
||||
```
|
||||
|
||||
This is useful for setting the composition duration to match the GIF:
|
||||
|
||||
```tsx
|
||||
import {getGifDurationInSeconds} from '@remotion/gif';
|
||||
import {staticFile, CalculateMetadataFunction} from 'remotion';
|
||||
|
||||
const calculateMetadata: CalculateMetadataFunction = async () => {
|
||||
const duration = await getGifDurationInSeconds(staticFile('animation.gif'));
|
||||
return {
|
||||
durationInFrames: Math.ceil(duration * 30),
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## Alternative
|
||||
|
||||
If `<AnimatedImage>` does not work (only supported in Chrome and Firefox), you can use `<Gif>` from `@remotion/gif` instead.
|
||||
|
||||
```bash
|
||||
npx remotion add @remotion/gif # If project uses npm
|
||||
bunx remotion add @remotion/gif # If project uses bun
|
||||
yarn remotion add @remotion/gif # If project uses yarn
|
||||
pnpm exec remotion add @remotion/gif # If project uses pnpm
|
||||
```
|
||||
|
||||
```tsx
|
||||
import {Gif} from '@remotion/gif';
|
||||
import {staticFile} from 'remotion';
|
||||
|
||||
export const MyComposition = () => {
|
||||
return <Gif src={staticFile('animation.gif')} width={500} height={500} />;
|
||||
};
|
||||
```
|
||||
|
||||
The `<Gif>` component has the same props as `<AnimatedImage>` but only supports GIF files.
|
||||
130
.claude/skills/remotion-best-practices/rules/images.md
Normal file
130
.claude/skills/remotion-best-practices/rules/images.md
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
---
|
||||
name: images
|
||||
description: Embedding images in Remotion using the <Img> component
|
||||
metadata:
|
||||
tags: images, img, staticFile, png, jpg, svg, webp
|
||||
---
|
||||
|
||||
# Using images in Remotion
|
||||
|
||||
## The `<Img>` component
|
||||
|
||||
Always use the `<Img>` component from `remotion` to display images:
|
||||
|
||||
```tsx
|
||||
import { Img, staticFile } from "remotion";
|
||||
|
||||
export const MyComposition = () => {
|
||||
return <Img src={staticFile("photo.png")} />;
|
||||
};
|
||||
```
|
||||
|
||||
## Important restrictions
|
||||
|
||||
**You MUST use the `<Img>` component from `remotion`.** Do not use:
|
||||
|
||||
- Native HTML `<img>` elements
|
||||
- Next.js `<Image>` component
|
||||
- CSS `background-image`
|
||||
|
||||
The `<Img>` component ensures images are fully loaded before rendering, preventing flickering and blank frames during video export.
|
||||
|
||||
## Local images with staticFile()
|
||||
|
||||
Place images in the `public/` folder and use `staticFile()` to reference them:
|
||||
|
||||
```
|
||||
my-video/
|
||||
├─ public/
|
||||
│ ├─ logo.png
|
||||
│ ├─ avatar.jpg
|
||||
│ └─ icon.svg
|
||||
├─ src/
|
||||
├─ package.json
|
||||
```
|
||||
|
||||
```tsx
|
||||
import { Img, staticFile } from "remotion";
|
||||
|
||||
<Img src={staticFile("logo.png")} />
|
||||
```
|
||||
|
||||
## Remote images
|
||||
|
||||
Remote URLs can be used directly without `staticFile()`:
|
||||
|
||||
```tsx
|
||||
<Img src="https://example.com/image.png" />
|
||||
```
|
||||
|
||||
Ensure remote images have CORS enabled.
|
||||
|
||||
For animated GIFs, use the `<Gif>` component from `@remotion/gif` instead.
|
||||
|
||||
## Sizing and positioning
|
||||
|
||||
Use the `style` prop to control size and position:
|
||||
|
||||
```tsx
|
||||
<Img
|
||||
src={staticFile("photo.png")}
|
||||
style={{
|
||||
width: 500,
|
||||
height: 300,
|
||||
position: "absolute",
|
||||
top: 100,
|
||||
left: 50,
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
## Dynamic image paths
|
||||
|
||||
Use template literals for dynamic file references:
|
||||
|
||||
```tsx
|
||||
import { Img, staticFile, useCurrentFrame } from "remotion";
|
||||
|
||||
const frame = useCurrentFrame();
|
||||
|
||||
// Image sequence
|
||||
<Img src={staticFile(`frames/frame${frame}.png`)} />
|
||||
|
||||
// Selecting based on props
|
||||
<Img src={staticFile(`avatars/${props.userId}.png`)} />
|
||||
|
||||
// Conditional images
|
||||
<Img src={staticFile(`icons/${isActive ? "active" : "inactive"}.svg`)} />
|
||||
```
|
||||
|
||||
This pattern is useful for:
|
||||
|
||||
- Image sequences (frame-by-frame animations)
|
||||
- User-specific avatars or profile images
|
||||
- Theme-based icons
|
||||
- State-dependent graphics
|
||||
|
||||
## Getting image dimensions
|
||||
|
||||
Use `getImageDimensions()` to get the dimensions of an image:
|
||||
|
||||
```tsx
|
||||
import { getImageDimensions, staticFile } from "remotion";
|
||||
|
||||
const { width, height } = await getImageDimensions(staticFile("photo.png"));
|
||||
```
|
||||
|
||||
This is useful for calculating aspect ratios or sizing compositions:
|
||||
|
||||
```tsx
|
||||
import { getImageDimensions, staticFile, CalculateMetadataFunction } from "remotion";
|
||||
|
||||
const calculateMetadata: CalculateMetadataFunction = async () => {
|
||||
const { width, height } = await getImageDimensions(staticFile("photo.png"));
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
---
|
||||
name: import-srt-captions
|
||||
description: Importing .srt subtitle files into Remotion using @remotion/captions
|
||||
metadata:
|
||||
tags: captions, subtitles, srt, import, parse
|
||||
---
|
||||
|
||||
# Importing .srt subtitles into Remotion
|
||||
|
||||
If you have an existing `.srt` subtitle file, you can import it into Remotion using `parseSrt()` from `@remotion/captions`.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
First, the @remotion/captions package needs to be installed.
|
||||
If it is not installed, use the following command:
|
||||
|
||||
```bash
|
||||
npx remotion add @remotion/captions # If project uses npm
|
||||
bunx remotion add @remotion/captions # If project uses bun
|
||||
yarn remotion add @remotion/captions # If project uses yarn
|
||||
pnpm exec remotion add @remotion/captions # If project uses pnpm
|
||||
```
|
||||
|
||||
## Reading an .srt file
|
||||
|
||||
Use `staticFile()` to reference an `.srt` file in your `public` folder, then fetch and parse it:
|
||||
|
||||
```tsx
|
||||
import {useState, useEffect, useCallback} from 'react';
|
||||
import {AbsoluteFill, staticFile, useDelayRender} from 'remotion';
|
||||
import {parseSrt} from '@remotion/captions';
|
||||
import type {Caption} from '@remotion/captions';
|
||||
|
||||
export const MyComponent: React.FC = () => {
|
||||
const [captions, setCaptions] = useState<Caption[] | null>(null);
|
||||
const {delayRender, continueRender, cancelRender} = useDelayRender();
|
||||
const [handle] = useState(() => delayRender());
|
||||
|
||||
const fetchCaptions = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch(staticFile('subtitles.srt'));
|
||||
const text = await response.text();
|
||||
const {captions: parsed} = parseSrt({input: text});
|
||||
setCaptions(parsed);
|
||||
continueRender(handle);
|
||||
} catch (e) {
|
||||
cancelRender(e);
|
||||
}
|
||||
}, [continueRender, cancelRender, handle]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCaptions();
|
||||
}, [fetchCaptions]);
|
||||
|
||||
if (!captions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <AbsoluteFill>{/* Use captions here */}</AbsoluteFill>;
|
||||
};
|
||||
```
|
||||
|
||||
Remote URLs are also supported - you can `fetch()` a remote file via URL instead of using `staticFile()`.
|
||||
|
||||
## Using imported captions
|
||||
|
||||
Once parsed, the captions are in the `Caption` format and can be used with all `@remotion/captions` utilities.
|
||||
68
.claude/skills/remotion-best-practices/rules/lottie.md
Normal file
68
.claude/skills/remotion-best-practices/rules/lottie.md
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
---
|
||||
name: lottie
|
||||
description: Embedding Lottie animations in Remotion.
|
||||
metadata:
|
||||
category: Animation
|
||||
---
|
||||
|
||||
# Using Lottie Animations in Remotion
|
||||
|
||||
## Prerequisites
|
||||
|
||||
First, the @remotion/lottie package needs to be installed.
|
||||
If it is not, use the following command:
|
||||
|
||||
```bash
|
||||
npx remotion add @remotion/lottie # If project uses npm
|
||||
bunx remotion add @remotion/lottie # If project uses bun
|
||||
yarn remotion add @remotion/lottie # If project uses yarn
|
||||
pnpm exec remotion add @remotion/lottie # If project uses pnpm
|
||||
```
|
||||
|
||||
## Displaying a Lottie file
|
||||
|
||||
To import a Lottie animation:
|
||||
|
||||
- Fetch the Lottie asset
|
||||
- Wrap the loading process in `delayRender()` and `continueRender()`
|
||||
- Save the animation data in a state
|
||||
- Render the Lottie animation using the `Lottie` component from the `@remotion/lottie` package
|
||||
|
||||
```tsx
|
||||
import {Lottie, LottieAnimationData} from '@remotion/lottie';
|
||||
import {useEffect, useState} from 'react';
|
||||
import {cancelRender, continueRender, delayRender} from 'remotion';
|
||||
|
||||
export const MyAnimation = () => {
|
||||
const [handle] = useState(() => delayRender('Loading Lottie animation'));
|
||||
|
||||
const [animationData, setAnimationData] = useState<LottieAnimationData | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('https://assets4.lottiefiles.com/packages/lf20_zyquagfl.json')
|
||||
.then((data) => data.json())
|
||||
.then((json) => {
|
||||
setAnimationData(json);
|
||||
continueRender(handle);
|
||||
})
|
||||
.catch((err) => {
|
||||
cancelRender(err);
|
||||
});
|
||||
}, [handle]);
|
||||
|
||||
if (!animationData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Lottie animationData={animationData} />;
|
||||
};
|
||||
```
|
||||
|
||||
## Styling and animating
|
||||
|
||||
Lottie supports the `style` prop to allow styles and animations:
|
||||
|
||||
```tsx
|
||||
return <Lottie animationData={animationData} style={{width: 400, height: 400}} />;
|
||||
```
|
||||
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
name: measuring-dom-nodes
|
||||
description: Measuring DOM element dimensions in Remotion
|
||||
metadata:
|
||||
tags: measure, layout, dimensions, getBoundingClientRect, scale
|
||||
---
|
||||
|
||||
# Measuring DOM nodes in Remotion
|
||||
|
||||
Remotion applies a `scale()` transform to the video container, which affects values from `getBoundingClientRect()`. Use `useCurrentScale()` to get correct measurements.
|
||||
|
||||
## Measuring element dimensions
|
||||
|
||||
```tsx
|
||||
import { useCurrentScale } from "remotion";
|
||||
import { useRef, useEffect, useState } from "react";
|
||||
|
||||
export const MyComponent = () => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const scale = useCurrentScale();
|
||||
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return;
|
||||
const rect = ref.current.getBoundingClientRect();
|
||||
setDimensions({
|
||||
width: rect.width / scale,
|
||||
height: rect.height / scale,
|
||||
});
|
||||
}, [scale]);
|
||||
|
||||
return <div ref={ref}>Content to measure</div>;
|
||||
};
|
||||
```
|
||||
|
||||
143
.claude/skills/remotion-best-practices/rules/measuring-text.md
Normal file
143
.claude/skills/remotion-best-practices/rules/measuring-text.md
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
---
|
||||
name: measuring-text
|
||||
description: Measuring text dimensions, fitting text to containers, and checking overflow
|
||||
metadata:
|
||||
tags: measure, text, layout, dimensions, fitText, fillTextBox
|
||||
---
|
||||
|
||||
# Measuring text in Remotion
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Install @remotion/layout-utils if it is not already installed:
|
||||
|
||||
```bash
|
||||
npx remotion add @remotion/layout-utils # If project uses npm
|
||||
bunx remotion add @remotion/layout-utils # If project uses bun
|
||||
yarn remotion add @remotion/layout-utils # If project uses yarn
|
||||
pnpm exec remotion add @remotion/layout-utils # If project uses pnpm
|
||||
```
|
||||
|
||||
## Measuring text dimensions
|
||||
|
||||
Use `measureText()` to calculate the width and height of text:
|
||||
|
||||
```tsx
|
||||
import { measureText } from "@remotion/layout-utils";
|
||||
|
||||
const { width, height } = measureText({
|
||||
text: "Hello World",
|
||||
fontFamily: "Arial",
|
||||
fontSize: 32,
|
||||
fontWeight: "bold",
|
||||
});
|
||||
```
|
||||
|
||||
Results are cached - duplicate calls return the cached result.
|
||||
|
||||
## Fitting text to a width
|
||||
|
||||
Use `fitText()` to find the optimal font size for a container:
|
||||
|
||||
```tsx
|
||||
import { fitText } from "@remotion/layout-utils";
|
||||
|
||||
const { fontSize } = fitText({
|
||||
text: "Hello World",
|
||||
withinWidth: 600,
|
||||
fontFamily: "Inter",
|
||||
fontWeight: "bold",
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
fontSize: Math.min(fontSize, 80), // Cap at 80px
|
||||
fontFamily: "Inter",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
Hello World
|
||||
</div>
|
||||
);
|
||||
```
|
||||
|
||||
## Checking text overflow
|
||||
|
||||
Use `fillTextBox()` to check if text exceeds a box:
|
||||
|
||||
```tsx
|
||||
import { fillTextBox } from "@remotion/layout-utils";
|
||||
|
||||
const box = fillTextBox({ maxBoxWidth: 400, maxLines: 3 });
|
||||
|
||||
const words = ["Hello", "World", "This", "is", "a", "test"];
|
||||
for (const word of words) {
|
||||
const { exceedsBox } = box.add({
|
||||
text: word + " ",
|
||||
fontFamily: "Arial",
|
||||
fontSize: 24,
|
||||
});
|
||||
if (exceedsBox) {
|
||||
// Text would overflow, handle accordingly
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best practices
|
||||
|
||||
**Load fonts first:** Only call measurement functions after fonts are loaded.
|
||||
|
||||
```tsx
|
||||
import { loadFont } from "@remotion/google-fonts/Inter";
|
||||
|
||||
const { fontFamily, waitUntilDone } = loadFont("normal", {
|
||||
weights: ["400"],
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
waitUntilDone().then(() => {
|
||||
// Now safe to measure
|
||||
const { width } = measureText({
|
||||
text: "Hello",
|
||||
fontFamily,
|
||||
fontSize: 32,
|
||||
});
|
||||
})
|
||||
```
|
||||
|
||||
**Use validateFontIsLoaded:** Catch font loading issues early:
|
||||
|
||||
```tsx
|
||||
measureText({
|
||||
text: "Hello",
|
||||
fontFamily: "MyCustomFont",
|
||||
fontSize: 32,
|
||||
validateFontIsLoaded: true, // Throws if font not loaded
|
||||
});
|
||||
```
|
||||
|
||||
**Match font properties:** Use the same properties for measurement and rendering:
|
||||
|
||||
```tsx
|
||||
const fontStyle = {
|
||||
fontFamily: "Inter",
|
||||
fontSize: 32,
|
||||
fontWeight: "bold" as const,
|
||||
letterSpacing: "0.5px",
|
||||
};
|
||||
|
||||
const { width } = measureText({
|
||||
text: "Hello",
|
||||
...fontStyle,
|
||||
});
|
||||
|
||||
return <div style={fontStyle}>Hello</div>;
|
||||
```
|
||||
|
||||
**Avoid padding and border:** Use `outline` instead of `border` to prevent layout differences:
|
||||
|
||||
```tsx
|
||||
<div style={{ outline: "2px solid red" }}>Text</div>
|
||||
```
|
||||
106
.claude/skills/remotion-best-practices/rules/sequencing.md
Normal file
106
.claude/skills/remotion-best-practices/rules/sequencing.md
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
---
|
||||
name: sequencing
|
||||
description: Sequencing patterns for Remotion - delay, trim, limit duration of items
|
||||
metadata:
|
||||
tags: sequence, series, timing, delay, trim
|
||||
---
|
||||
|
||||
Use `<Sequence>` to delay when an element appears in the timeline.
|
||||
|
||||
```tsx
|
||||
import { Sequence } from "remotion";
|
||||
|
||||
const {fps} = useVideoConfig();
|
||||
|
||||
<Sequence from={1 * fps} durationInFrames={2 * fps} premountFor={1 * fps}>
|
||||
<Title />
|
||||
</Sequence>
|
||||
<Sequence from={2 * fps} durationInFrames={2 * fps} premountFor={1 * fps}>
|
||||
<Subtitle />
|
||||
</Sequence>
|
||||
```
|
||||
|
||||
This will by default wrap the component in an absolute fill element.
|
||||
If the items should not be wrapped, use the `layout` prop:
|
||||
|
||||
```tsx
|
||||
<Sequence layout="none">
|
||||
<Title />
|
||||
</Sequence>
|
||||
```
|
||||
|
||||
## Premounting
|
||||
|
||||
This loads the component in the timeline before it is actually played.
|
||||
Always premount any `<Sequence>`!
|
||||
|
||||
```tsx
|
||||
<Sequence premountFor={1 * fps}>
|
||||
<Title />
|
||||
</Sequence>
|
||||
```
|
||||
|
||||
## Series
|
||||
|
||||
Use `<Series>` when elements should play one after another without overlap.
|
||||
|
||||
```tsx
|
||||
import {Series} from 'remotion';
|
||||
|
||||
<Series>
|
||||
<Series.Sequence durationInFrames={45}>
|
||||
<Intro />
|
||||
</Series.Sequence>
|
||||
<Series.Sequence durationInFrames={60}>
|
||||
<MainContent />
|
||||
</Series.Sequence>
|
||||
<Series.Sequence durationInFrames={30}>
|
||||
<Outro />
|
||||
</Series.Sequence>
|
||||
</Series>;
|
||||
```
|
||||
|
||||
Same as with `<Sequence>`, the items will be wrapped in an absolute fill element by default when using `<Series.Sequence>`, unless the `layout` prop is set to `none`.
|
||||
|
||||
### Series with overlaps
|
||||
|
||||
Use negative offset for overlapping sequences:
|
||||
|
||||
```tsx
|
||||
<Series>
|
||||
<Series.Sequence durationInFrames={60}>
|
||||
<SceneA />
|
||||
</Series.Sequence>
|
||||
<Series.Sequence offset={-15} durationInFrames={60}>
|
||||
{/* Starts 15 frames before SceneA ends */}
|
||||
<SceneB />
|
||||
</Series.Sequence>
|
||||
</Series>
|
||||
```
|
||||
|
||||
## Frame References Inside Sequences
|
||||
|
||||
Inside a Sequence, `useCurrentFrame()` returns the local frame (starting from 0):
|
||||
|
||||
```tsx
|
||||
<Sequence from={60} durationInFrames={30}>
|
||||
<MyComponent />
|
||||
{/* Inside MyComponent, useCurrentFrame() returns 0-29, not 60-89 */}
|
||||
</Sequence>
|
||||
```
|
||||
|
||||
## Nested Sequences
|
||||
|
||||
Sequences can be nested for complex timing:
|
||||
|
||||
```tsx
|
||||
<Sequence from={0} durationInFrames={120}>
|
||||
<Background />
|
||||
<Sequence from={15} durationInFrames={90} layout="none">
|
||||
<Title />
|
||||
</Sequence>
|
||||
<Sequence from={45} durationInFrames={60} layout="none">
|
||||
<Subtitle />
|
||||
</Sequence>
|
||||
</Sequence>
|
||||
```
|
||||
11
.claude/skills/remotion-best-practices/rules/tailwind.md
Normal file
11
.claude/skills/remotion-best-practices/rules/tailwind.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
name: tailwind
|
||||
description: Using TailwindCSS in Remotion.
|
||||
metadata:
|
||||
---
|
||||
|
||||
You can and should use TailwindCSS in Remotion, if TailwindCSS is installed in the project.
|
||||
|
||||
Don't use `transition-*` or `animate-*` classes - always animate using the `useCurrentFrame()` hook.
|
||||
|
||||
Tailwind must be installed and enabled first in a Remotion project - fetch https://www.remotion.dev/docs/tailwind using WebFetch for instructions.
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
name: text-animations
|
||||
description: Typography and text animation patterns for Remotion.
|
||||
metadata:
|
||||
tags: typography, text, typewriter, highlighter ken
|
||||
---
|
||||
|
||||
## Text animations
|
||||
|
||||
Based on `useCurrentFrame()`, reduce the string character by character to create a typewriter effect.
|
||||
|
||||
## Typewriter Effect
|
||||
|
||||
See [Typewriter](assets/text-animations-typewriter.tsx) for an advanced example with a blinking cursor and a pause after the first sentence.
|
||||
|
||||
Always use string slicing for typewriter effects. Never use per-character opacity.
|
||||
|
||||
## Word Highlighting
|
||||
|
||||
See [Word Highlight](assets/text-animations-word-highlight.tsx) for an example for how a word highlight is animated, like with a highlighter pen.
|
||||
179
.claude/skills/remotion-best-practices/rules/timing.md
Normal file
179
.claude/skills/remotion-best-practices/rules/timing.md
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
---
|
||||
name: timing
|
||||
description: Interpolation curves in Remotion - linear, easing, spring animations
|
||||
metadata:
|
||||
tags: spring, bounce, easing, interpolation
|
||||
---
|
||||
|
||||
A simple linear interpolation is done using the `interpolate` function.
|
||||
|
||||
```ts title="Going from 0 to 1 over 100 frames"
|
||||
import {interpolate} from 'remotion';
|
||||
|
||||
const opacity = interpolate(frame, [0, 100], [0, 1]);
|
||||
```
|
||||
|
||||
By default, the values are not clamped, so the value can go outside the range [0, 1].
|
||||
Here is how they can be clamped:
|
||||
|
||||
```ts title="Going from 0 to 1 over 100 frames with extrapolation"
|
||||
const opacity = interpolate(frame, [0, 100], [0, 1], {
|
||||
extrapolateRight: 'clamp',
|
||||
extrapolateLeft: 'clamp',
|
||||
});
|
||||
```
|
||||
|
||||
## Spring animations
|
||||
|
||||
Spring animations have a more natural motion.
|
||||
They go from 0 to 1 over time.
|
||||
|
||||
```ts title="Spring animation from 0 to 1 over 100 frames"
|
||||
import {spring, useCurrentFrame, useVideoConfig} from 'remotion';
|
||||
|
||||
const frame = useCurrentFrame();
|
||||
const {fps} = useVideoConfig();
|
||||
|
||||
const scale = spring({
|
||||
frame,
|
||||
fps,
|
||||
});
|
||||
```
|
||||
|
||||
### Physical properties
|
||||
|
||||
The default configuration is: `mass: 1, damping: 10, stiffness: 100`.
|
||||
This leads to the animation having a bit of bounce before it settles.
|
||||
|
||||
The config can be overwritten like this:
|
||||
|
||||
```ts
|
||||
const scale = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: {damping: 200},
|
||||
});
|
||||
```
|
||||
|
||||
The recommended configuration for a natural motion without a bounce is: `{ damping: 200 }`.
|
||||
|
||||
Here are some common configurations:
|
||||
|
||||
```tsx
|
||||
const smooth = {damping: 200}; // Smooth, no bounce (subtle reveals)
|
||||
const snappy = {damping: 20, stiffness: 200}; // Snappy, minimal bounce (UI elements)
|
||||
const bouncy = {damping: 8}; // Bouncy entrance (playful animations)
|
||||
const heavy = {damping: 15, stiffness: 80, mass: 2}; // Heavy, slow, small bounce
|
||||
```
|
||||
|
||||
### Delay
|
||||
|
||||
The animation starts immediately by default.
|
||||
Use the `delay` parameter to delay the animation by a number of frames.
|
||||
|
||||
```tsx
|
||||
const entrance = spring({
|
||||
frame: frame - ENTRANCE_DELAY,
|
||||
fps,
|
||||
delay: 20,
|
||||
});
|
||||
```
|
||||
|
||||
### Duration
|
||||
|
||||
A `spring()` has a natural duration based on the physical properties.
|
||||
To stretch the animation to a specific duration, use the `durationInFrames` parameter.
|
||||
|
||||
```tsx
|
||||
const spring = spring({
|
||||
frame,
|
||||
fps,
|
||||
durationInFrames: 40,
|
||||
});
|
||||
```
|
||||
|
||||
### Combining spring() with interpolate()
|
||||
|
||||
Map spring output (0-1) to custom ranges:
|
||||
|
||||
```tsx
|
||||
const springProgress = spring({
|
||||
frame,
|
||||
fps,
|
||||
});
|
||||
|
||||
// Map to rotation
|
||||
const rotation = interpolate(springProgress, [0, 1], [0, 360]);
|
||||
|
||||
<div style={{rotate: rotation + 'deg'}} />;
|
||||
```
|
||||
|
||||
### Adding springs
|
||||
|
||||
Springs return just numbers, so math can be performed:
|
||||
|
||||
```tsx
|
||||
const frame = useCurrentFrame();
|
||||
const {fps, durationInFrames} = useVideoConfig();
|
||||
|
||||
const inAnimation = spring({
|
||||
frame,
|
||||
fps,
|
||||
});
|
||||
const outAnimation = spring({
|
||||
frame,
|
||||
fps,
|
||||
durationInFrames: 1 * fps,
|
||||
delay: durationInFrames - 1 * fps,
|
||||
});
|
||||
|
||||
const scale = inAnimation - outAnimation;
|
||||
```
|
||||
|
||||
## Easing
|
||||
|
||||
Easing can be added to the `interpolate` function:
|
||||
|
||||
```ts
|
||||
import {interpolate, Easing} from 'remotion';
|
||||
|
||||
const value1 = interpolate(frame, [0, 100], [0, 1], {
|
||||
easing: Easing.inOut(Easing.quad),
|
||||
extrapolateLeft: 'clamp',
|
||||
extrapolateRight: 'clamp',
|
||||
});
|
||||
```
|
||||
|
||||
The default easing is `Easing.linear`.
|
||||
There are various other convexities:
|
||||
|
||||
- `Easing.in` for starting slow and accelerating
|
||||
- `Easing.out` for starting fast and slowing down
|
||||
- `Easing.inOut`
|
||||
|
||||
and curves (sorted from most linear to most curved):
|
||||
|
||||
- `Easing.quad`
|
||||
- `Easing.sin`
|
||||
- `Easing.exp`
|
||||
- `Easing.circle`
|
||||
|
||||
Convexities and curves need be combined for an easing function:
|
||||
|
||||
```ts
|
||||
const value1 = interpolate(frame, [0, 100], [0, 1], {
|
||||
easing: Easing.inOut(Easing.quad),
|
||||
extrapolateLeft: 'clamp',
|
||||
extrapolateRight: 'clamp',
|
||||
});
|
||||
```
|
||||
|
||||
Cubic bezier curves are also supported:
|
||||
|
||||
```ts
|
||||
const value1 = interpolate(frame, [0, 100], [0, 1], {
|
||||
easing: Easing.bezier(0.8, 0.22, 0.96, 0.65),
|
||||
extrapolateLeft: 'clamp',
|
||||
extrapolateRight: 'clamp',
|
||||
});
|
||||
```
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
name: transcribe-captions
|
||||
description: Transcribing audio to generate captions in Remotion
|
||||
metadata:
|
||||
tags: captions, transcribe, whisper, audio, speech-to-text
|
||||
---
|
||||
|
||||
# Transcribing audio
|
||||
|
||||
Remotion provides several built-in options for transcribing audio to generate captions:
|
||||
|
||||
- `@remotion/install-whisper-cpp` - Transcribe locally on a server using Whisper.cpp. Fast and free, but requires server infrastructure.
|
||||
https://remotion.dev/docs/install-whisper-cpp
|
||||
|
||||
- `@remotion/whisper-web` - Transcribe in the browser using WebAssembly. No server needed and free, but slower due to WASM overhead.
|
||||
https://remotion.dev/docs/whisper-web
|
||||
|
||||
- `@remotion/openai-whisper` - Use OpenAI Whisper API for cloud-based transcription. Fast and no server needed, but requires payment.
|
||||
https://remotion.dev/docs/openai-whisper/openai-whisper-api-to-captions
|
||||
122
.claude/skills/remotion-best-practices/rules/transitions.md
Normal file
122
.claude/skills/remotion-best-practices/rules/transitions.md
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
---
|
||||
name: transitions
|
||||
description: Fullscreen scene transitions for Remotion.
|
||||
metadata:
|
||||
tags: transitions, fade, slide, wipe, scenes
|
||||
---
|
||||
|
||||
## Fullscreen transitions
|
||||
|
||||
Using `<TransitionSeries>` to animate between multiple scenes or clips.
|
||||
This will absolutely position the children.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
First, the @remotion/transitions package needs to be installed.
|
||||
If it is not, use the following command:
|
||||
|
||||
```bash
|
||||
npx remotion add @remotion/transitions # If project uses npm
|
||||
bunx remotion add @remotion/transitions # If project uses bun
|
||||
yarn remotion add @remotion/transitions # If project uses yarn
|
||||
pnpm exec remotion add @remotion/transitions # If project uses pnpm
|
||||
```
|
||||
|
||||
## Example usage
|
||||
|
||||
```tsx
|
||||
import {TransitionSeries, linearTiming} from '@remotion/transitions';
|
||||
import {fade} from '@remotion/transitions/fade';
|
||||
|
||||
<TransitionSeries>
|
||||
<TransitionSeries.Sequence durationInFrames={60}>
|
||||
<SceneA />
|
||||
</TransitionSeries.Sequence>
|
||||
<TransitionSeries.Transition presentation={fade()} timing={linearTiming({durationInFrames: 15})} />
|
||||
<TransitionSeries.Sequence durationInFrames={60}>
|
||||
<SceneB />
|
||||
</TransitionSeries.Sequence>
|
||||
</TransitionSeries>;
|
||||
```
|
||||
|
||||
## Available Transition Types
|
||||
|
||||
Import transitions from their respective modules:
|
||||
|
||||
```tsx
|
||||
import {fade} from '@remotion/transitions/fade';
|
||||
import {slide} from '@remotion/transitions/slide';
|
||||
import {wipe} from '@remotion/transitions/wipe';
|
||||
import {flip} from '@remotion/transitions/flip';
|
||||
import {clockWipe} from '@remotion/transitions/clock-wipe';
|
||||
```
|
||||
|
||||
## Slide Transition with Direction
|
||||
|
||||
Specify slide direction for enter/exit animations.
|
||||
|
||||
```tsx
|
||||
import {slide} from '@remotion/transitions/slide';
|
||||
|
||||
<TransitionSeries.Transition presentation={slide({direction: 'from-left'})} timing={linearTiming({durationInFrames: 20})} />;
|
||||
```
|
||||
|
||||
Directions: `"from-left"`, `"from-right"`, `"from-top"`, `"from-bottom"`
|
||||
|
||||
## Timing Options
|
||||
|
||||
```tsx
|
||||
import {linearTiming, springTiming} from '@remotion/transitions';
|
||||
|
||||
// Linear timing - constant speed
|
||||
linearTiming({durationInFrames: 20});
|
||||
|
||||
// Spring timing - organic motion
|
||||
springTiming({config: {damping: 200}, durationInFrames: 25});
|
||||
```
|
||||
|
||||
## Duration calculation
|
||||
|
||||
Transitions overlap adjacent scenes, so the total composition length is **shorter** than the sum of all sequence durations.
|
||||
|
||||
For example, with two 60-frame sequences and a 15-frame transition:
|
||||
|
||||
- Without transitions: `60 + 60 = 120` frames
|
||||
- With transition: `60 + 60 - 15 = 105` frames
|
||||
|
||||
The transition duration is subtracted because both scenes play simultaneously during the transition.
|
||||
|
||||
### Getting the duration of a transition
|
||||
|
||||
Use the `getDurationInFrames()` method on the timing object:
|
||||
|
||||
```tsx
|
||||
import {linearTiming, springTiming} from '@remotion/transitions';
|
||||
|
||||
const linearDuration = linearTiming({durationInFrames: 20}).getDurationInFrames({fps: 30});
|
||||
// Returns 20
|
||||
|
||||
const springDuration = springTiming({config: {damping: 200}}).getDurationInFrames({fps: 30});
|
||||
// Returns calculated duration based on spring physics
|
||||
```
|
||||
|
||||
For `springTiming` without an explicit `durationInFrames`, the duration depends on `fps` because it calculates when the spring animation settles.
|
||||
|
||||
### Calculating total composition duration
|
||||
|
||||
```tsx
|
||||
import {linearTiming} from '@remotion/transitions';
|
||||
|
||||
const scene1Duration = 60;
|
||||
const scene2Duration = 60;
|
||||
const scene3Duration = 60;
|
||||
|
||||
const timing1 = linearTiming({durationInFrames: 15});
|
||||
const timing2 = linearTiming({durationInFrames: 20});
|
||||
|
||||
const transition1Duration = timing1.getDurationInFrames({fps: 30});
|
||||
const transition2Duration = timing2.getDurationInFrames({fps: 30});
|
||||
|
||||
const totalDuration = scene1Duration + scene2Duration + scene3Duration - transition1Duration - transition2Duration;
|
||||
// 60 + 60 + 60 - 15 - 20 = 145 frames
|
||||
```
|
||||
53
.claude/skills/remotion-best-practices/rules/trimming.md
Normal file
53
.claude/skills/remotion-best-practices/rules/trimming.md
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
---
|
||||
name: trimming
|
||||
description: Trimming patterns for Remotion - cut the beginning or end of animations
|
||||
metadata:
|
||||
tags: sequence, trim, clip, cut, offset
|
||||
---
|
||||
|
||||
Use `<Sequence>` with a negative `from` value to trim the start of an animation.
|
||||
|
||||
## Trim the Beginning
|
||||
|
||||
A negative `from` value shifts time backwards, making the animation start partway through:
|
||||
|
||||
```tsx
|
||||
import { Sequence, useVideoConfig } from "remotion";
|
||||
|
||||
const fps = useVideoConfig();
|
||||
|
||||
<Sequence from={-0.5 * fps}>
|
||||
<MyAnimation />
|
||||
</Sequence>
|
||||
```
|
||||
|
||||
The animation appears 15 frames into its progress - the first 15 frames are trimmed off.
|
||||
Inside `<MyAnimation>`, `useCurrentFrame()` starts at 15 instead of 0.
|
||||
|
||||
## Trim the End
|
||||
|
||||
Use `durationInFrames` to unmount content after a specified duration:
|
||||
|
||||
```tsx
|
||||
|
||||
<Sequence durationInFrames={1.5 * fps}>
|
||||
<MyAnimation />
|
||||
</Sequence>
|
||||
```
|
||||
|
||||
The animation plays for 45 frames, then the component unmounts.
|
||||
|
||||
## Trim and Delay
|
||||
|
||||
Nest sequences to both trim the beginning and delay when it appears:
|
||||
|
||||
```tsx
|
||||
<Sequence from={30}>
|
||||
<Sequence from={-15}>
|
||||
<MyAnimation />
|
||||
</Sequence>
|
||||
</Sequence>
|
||||
```
|
||||
|
||||
The inner sequence trims 15 frames from the start, and the outer sequence delays the result by 30 frames.
|
||||
|
||||
171
.claude/skills/remotion-best-practices/rules/videos.md
Normal file
171
.claude/skills/remotion-best-practices/rules/videos.md
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
---
|
||||
name: videos
|
||||
description: Embedding videos in Remotion - trimming, volume, speed, looping, pitch
|
||||
metadata:
|
||||
tags: video, media, trim, volume, speed, loop, pitch
|
||||
---
|
||||
|
||||
# Using videos in Remotion
|
||||
|
||||
## Prerequisites
|
||||
|
||||
First, the @remotion/media package needs to be installed.
|
||||
If it is not, use the following command:
|
||||
|
||||
```bash
|
||||
npx remotion add @remotion/media # If project uses npm
|
||||
bunx remotion add @remotion/media # If project uses bun
|
||||
yarn remotion add @remotion/media # If project uses yarn
|
||||
pnpm exec remotion add @remotion/media # If project uses pnpm
|
||||
```
|
||||
|
||||
Use `<Video>` from `@remotion/media` to embed videos into your composition.
|
||||
|
||||
```tsx
|
||||
import { Video } from "@remotion/media";
|
||||
import { staticFile } from "remotion";
|
||||
|
||||
export const MyComposition = () => {
|
||||
return <Video src={staticFile("video.mp4")} />;
|
||||
};
|
||||
```
|
||||
|
||||
Remote URLs are also supported:
|
||||
|
||||
```tsx
|
||||
<Video src="https://remotion.media/video.mp4" />
|
||||
```
|
||||
|
||||
## Trimming
|
||||
|
||||
Use `trimBefore` and `trimAfter` to remove portions of the video. Values are in seconds.
|
||||
|
||||
```tsx
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
return (
|
||||
<Video
|
||||
src={staticFile("video.mp4")}
|
||||
trimBefore={2 * fps} // Skip the first 2 seconds
|
||||
trimAfter={10 * fps} // End at the 10 second mark
|
||||
/>
|
||||
);
|
||||
```
|
||||
|
||||
## Delaying
|
||||
|
||||
Wrap the video in a `<Sequence>` to delay when it appears:
|
||||
|
||||
```tsx
|
||||
import { Sequence, staticFile } from "remotion";
|
||||
import { Video } from "@remotion/media";
|
||||
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
return (
|
||||
<Sequence from={1 * fps}>
|
||||
<Video src={staticFile("video.mp4")} />
|
||||
</Sequence>
|
||||
);
|
||||
```
|
||||
|
||||
The video will appear after 1 second.
|
||||
|
||||
## Sizing and Position
|
||||
|
||||
Use the `style` prop to control size and position:
|
||||
|
||||
```tsx
|
||||
<Video
|
||||
src={staticFile("video.mp4")}
|
||||
style={{
|
||||
width: 500,
|
||||
height: 300,
|
||||
position: "absolute",
|
||||
top: 100,
|
||||
left: 50,
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
## Volume
|
||||
|
||||
Set a static volume (0 to 1):
|
||||
|
||||
```tsx
|
||||
<Video src={staticFile("video.mp4")} volume={0.5} />
|
||||
```
|
||||
|
||||
Or use a callback for dynamic volume based on the current frame:
|
||||
|
||||
```tsx
|
||||
import { interpolate } from "remotion";
|
||||
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
return (
|
||||
<Video
|
||||
src={staticFile("video.mp4")}
|
||||
volume={(f) =>
|
||||
interpolate(f, [0, 1 * fps], [0, 1], { extrapolateRight: "clamp" })
|
||||
}
|
||||
/>
|
||||
);
|
||||
```
|
||||
|
||||
Use `muted` to silence the video entirely:
|
||||
|
||||
```tsx
|
||||
<Video src={staticFile("video.mp4")} muted />
|
||||
```
|
||||
|
||||
## Speed
|
||||
|
||||
Use `playbackRate` to change the playback speed:
|
||||
|
||||
```tsx
|
||||
<Video src={staticFile("video.mp4")} playbackRate={2} /> {/* 2x speed */}
|
||||
<Video src={staticFile("video.mp4")} playbackRate={0.5} /> {/* Half speed */}
|
||||
```
|
||||
|
||||
Reverse playback is not supported.
|
||||
|
||||
## Looping
|
||||
|
||||
Use `loop` to loop the video indefinitely:
|
||||
|
||||
```tsx
|
||||
<Video src={staticFile("video.mp4")} loop />
|
||||
```
|
||||
|
||||
Use `loopVolumeCurveBehavior` to control how the frame count behaves when looping:
|
||||
|
||||
- `"repeat"`: Frame count resets to 0 each loop (for `volume` callback)
|
||||
- `"extend"`: Frame count continues incrementing
|
||||
|
||||
```tsx
|
||||
<Video
|
||||
src={staticFile("video.mp4")}
|
||||
loop
|
||||
loopVolumeCurveBehavior="extend"
|
||||
volume={(f) => interpolate(f, [0, 300], [1, 0])} // Fade out over multiple loops
|
||||
/>
|
||||
```
|
||||
|
||||
## Pitch
|
||||
|
||||
Use `toneFrequency` to adjust the pitch without affecting speed. Values range from 0.01 to 2:
|
||||
|
||||
```tsx
|
||||
<Video
|
||||
src={staticFile("video.mp4")}
|
||||
toneFrequency={1.5} // Higher pitch
|
||||
/>
|
||||
<Video
|
||||
src={staticFile("video.mp4")}
|
||||
toneFrequency={0.8} // Lower pitch
|
||||
/>
|
||||
```
|
||||
|
||||
Pitch shifting only works during server-side rendering, not in the Remotion Studio preview or in the `<Player />`.
|
||||
296
.claude/skills/tpmjs-tool-creator/SKILL.md
Normal file
296
.claude/skills/tpmjs-tool-creator/SKILL.md
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
---
|
||||
name: tpmjs-tool-creator
|
||||
description: Guide for creating official TPMJS tools using the blocks CLI. Use when a user wants to create a new tool for the TPMJS registry, add a tool to packages/tools/official/, implement an AI SDK v6 tool, define a block in blocks.yml, validate a tool with `pnpm blocks run`, or publish a tool to npm with the tpmjs keyword.
|
||||
---
|
||||
|
||||
# TPMJS Tool Creator
|
||||
|
||||
Create production-ready tools for the TPMJS registry using the blocks CLI. Tools are npm packages following the AI SDK v6 pattern, validated by blocks, and automatically synced to tpmjs.com.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Define the tool block in `packages/tools/official/blocks.yml`
|
||||
2. Create the tool package directory
|
||||
3. Implement the tool using AI SDK v6 `tool()` + `jsonSchema()`
|
||||
4. Validate with `pnpm blocks run <tool-name>`
|
||||
5. Build and publish to npm
|
||||
|
||||
## Step 1: Define in blocks.yml
|
||||
|
||||
Add to the `blocks:` section of `packages/tools/official/blocks.yml`:
|
||||
|
||||
```yaml
|
||||
blocks:
|
||||
category.toolName:
|
||||
type: utility
|
||||
description: "LLM-friendly description of what the tool does"
|
||||
path: "tool-directory-name"
|
||||
domain_rules:
|
||||
- id: rule_name
|
||||
description: "What this implementation must do"
|
||||
inputs:
|
||||
- name: inputName
|
||||
type: string
|
||||
description: "Description for LLMs"
|
||||
- name: optionalInput
|
||||
type: number
|
||||
optional: true
|
||||
description: "Optional parameter"
|
||||
outputs:
|
||||
- name: result
|
||||
type: ResultType
|
||||
description: "What the tool returns"
|
||||
measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance]
|
||||
```
|
||||
|
||||
**Category prefix** (before the dot): `research`, `web`, `data`, `documentation`, `engineering`, `security`, `statistics`, `ops`, `agent`, `sandbox`, `utilities`, `html`, `compliance`, `finance`, `legal`, `hr`, `marketing`, `cx`, `edu`, `sales`.
|
||||
|
||||
For domain entities and quality measures, see [references/domain.md](references/domain.md).
|
||||
|
||||
## Step 2: Create Package Directory
|
||||
|
||||
Create `packages/tools/official/<tool-name>/`:
|
||||
|
||||
```
|
||||
<tool-name>/
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
├── tsup.config.ts
|
||||
├── README.md
|
||||
└── src/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
**package.json:**
|
||||
```json
|
||||
{
|
||||
"name": "@tpmjs/official-<tool-name>",
|
||||
"version": "0.1.0",
|
||||
"description": "Short description",
|
||||
"type": "module",
|
||||
"keywords": ["tpmjs", "<category>", "ai"],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": ["dist"],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"dev": "tsup --watch",
|
||||
"type-check": "tsc --noEmit",
|
||||
"clean": "rm -rf dist .turbo"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tpmjs/tsconfig": "workspace:*",
|
||||
"tsup": "^8.5.1",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"ai": "6.0.49"
|
||||
},
|
||||
"publishConfig": { "access": "public" },
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tpmjs/tpmjs.git",
|
||||
"directory": "packages/tools/official/<tool-name>"
|
||||
},
|
||||
"homepage": "https://tpmjs.com",
|
||||
"license": "MIT",
|
||||
"tpmjs": {
|
||||
"category": "<category>",
|
||||
"frameworks": ["vercel-ai"],
|
||||
"tools": [
|
||||
{
|
||||
"name": "toolName",
|
||||
"description": "Clear description (20+ chars)."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**tsconfig.json:**
|
||||
```json
|
||||
{
|
||||
"extends": "@tpmjs/tsconfig/react-library.json",
|
||||
"compilerOptions": { "outDir": "dist", "rootDir": "src" },
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
```
|
||||
|
||||
**tsup.config.ts:**
|
||||
```typescript
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
format: ['esm'],
|
||||
dts: true,
|
||||
clean: true,
|
||||
sourcemap: true,
|
||||
target: 'es2022',
|
||||
});
|
||||
```
|
||||
|
||||
## Step 3: Implement the Tool
|
||||
|
||||
Every tool follows this AI SDK v6 pattern in `src/index.ts`:
|
||||
|
||||
```typescript
|
||||
import { jsonSchema, tool } from 'ai';
|
||||
|
||||
interface MyToolInput {
|
||||
param1: string;
|
||||
param2?: number;
|
||||
}
|
||||
|
||||
export interface MyToolResult {
|
||||
data: string;
|
||||
metadata: { processedAt: string };
|
||||
}
|
||||
|
||||
export const myTool = tool({
|
||||
description: 'Clear LLM-friendly description of what this tool does.',
|
||||
parameters: jsonSchema<MyToolInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
param1: {
|
||||
type: 'string',
|
||||
description: 'What param1 is for',
|
||||
},
|
||||
param2: {
|
||||
type: 'number',
|
||||
description: 'Optional: what param2 is for',
|
||||
},
|
||||
},
|
||||
required: ['param1'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
execute: async (input): Promise<MyToolResult> => {
|
||||
if (!input.param1) {
|
||||
throw new Error('param1 is required and must be non-empty');
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await processData(input.param1);
|
||||
return {
|
||||
data: result,
|
||||
metadata: { processedAt: new Date().toISOString() },
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to process: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export default myTool;
|
||||
```
|
||||
|
||||
**Hard rules:**
|
||||
- No stubs, TODOs, or placeholders — every tool must be fully working
|
||||
- Single-shot: one call in, one structured result out
|
||||
- Validate inputs before processing
|
||||
- Try-catch with descriptive errors including context
|
||||
- `additionalProperties: false` on jsonSchema
|
||||
- Description on every schema property
|
||||
- Export as both named and default export
|
||||
- Output interface must be exported
|
||||
|
||||
### Multi-Tool Packages
|
||||
|
||||
For packages with multiple tools, add root-level files:
|
||||
|
||||
**block.ts:**
|
||||
```typescript
|
||||
import { toolA, toolB } from './src/index.js';
|
||||
export const block = { name: 'package-name', tools: { toolA, toolB } };
|
||||
export default block;
|
||||
```
|
||||
|
||||
**index.ts (root):**
|
||||
```typescript
|
||||
export * from './src/index.js';
|
||||
export { default } from './src/index.js';
|
||||
```
|
||||
|
||||
Each tool gets its own entry in blocks.yml (same `path`) and in `tpmjs.tools` array.
|
||||
|
||||
## Step 4: Validate
|
||||
|
||||
The blocks CLI domain validator requires an OpenAI API key. Source it from `.env.local` before running:
|
||||
|
||||
```bash
|
||||
cd packages/tools/official
|
||||
|
||||
# Load the OpenAI API key for domain validation
|
||||
source ../../../.env.local
|
||||
export OPENAI_API_KEY
|
||||
|
||||
pnpm blocks run <tool-name> # Validate (schema → shape → domain)
|
||||
pnpm blocks run <tool-name> --force # Force full validation (skip cache)
|
||||
pnpm blocks run <tool-name> --json # JSON output for debugging
|
||||
pnpm blocks run --all # Validate all tools
|
||||
```
|
||||
|
||||
**Common errors:**
|
||||
- `Tool "X" not found in exports` → Export name must match blocks.yml
|
||||
- `Required file not found` → Check package root has all required files
|
||||
- `invalid tpmjs field` → Category must be valid, tools array required
|
||||
|
||||
## Step 5: Build and Publish
|
||||
|
||||
```bash
|
||||
pnpm --filter=@tpmjs/official-<tool-name> build
|
||||
cd packages/tools/official/<tool-name> && npm publish --access public
|
||||
```
|
||||
|
||||
The tool syncs to tpmjs.com automatically via the changes feed (every 2 min) and keyword search (every 15 min). To trigger immediately:
|
||||
|
||||
```bash
|
||||
source apps/web/.env.local
|
||||
curl -X POST https://tpmjs.com/api/sync/keyword \
|
||||
-H "Authorization: Bearer $CRON_SECRET"
|
||||
```
|
||||
|
||||
## README Template
|
||||
|
||||
Every tool needs a README:
|
||||
|
||||
```markdown
|
||||
# @tpmjs/official-<tool-name>
|
||||
|
||||
Short description.
|
||||
|
||||
## Installation
|
||||
|
||||
npm install @tpmjs/official-<tool-name>
|
||||
|
||||
## Usage
|
||||
|
||||
\`\`\`typescript
|
||||
import { myTool } from '@tpmjs/official-<tool-name>';
|
||||
|
||||
const result = await myTool.execute({ param1: 'example' });
|
||||
\`\`\`
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Required | Description |
|
||||
|--------|--------|----------|--------------------|
|
||||
| param1 | string | Yes | What param1 is for |
|
||||
|
||||
## Output
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|--------|----------------------|
|
||||
| data | string | The processed result |
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
```
|
||||
58
.claude/skills/tpmjs-tool-creator/references/domain.md
Normal file
58
.claude/skills/tpmjs-tool-creator/references/domain.md
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
# Domain Reference
|
||||
|
||||
## Entities
|
||||
|
||||
Reusable output types defined in blocks.yml. Reference these in your tool's output `type` field.
|
||||
|
||||
| Entity | Fields |
|
||||
|--------|--------|
|
||||
| url | href, domain, protocol, path, query, fragment |
|
||||
| webpage | url, title, html, text, metadata |
|
||||
| text_content | raw, sentences, paragraphs, wordCount |
|
||||
| claim | statement, confidence, needsCitation, category |
|
||||
| timeline | events, dateRange, gaps, eventCount |
|
||||
| evidence | source, type, strength, relevance |
|
||||
| summary | text, keyPoints, length, compressionRatio |
|
||||
| sentiment | score, label, confidence, aspects |
|
||||
| entity | name, type, mentions, context |
|
||||
| relationship | source, target, type, strength |
|
||||
| pattern | name, frequency, examples, significance |
|
||||
| anomaly | description, severity, context, recommendation |
|
||||
| metric | name, value, unit, trend |
|
||||
| comparison | items, criteria, rankings, analysis |
|
||||
| recommendation | action, priority, rationale, impact |
|
||||
| risk | description, likelihood, impact, mitigation |
|
||||
| code_snippet | language, code, explanation, complexity |
|
||||
| api_endpoint | method, path, parameters, response |
|
||||
| data_schema | fields, types, constraints, relationships |
|
||||
| workflow_step | action, input, output, conditions |
|
||||
|
||||
## Quality Measures
|
||||
|
||||
Reference these in your output's `measures` array.
|
||||
|
||||
| Measure | Severity | What it checks |
|
||||
|---------|----------|---------------|
|
||||
| working_implementation | error | No TODOs, stubs, or placeholders. Returns actual computed values. |
|
||||
| valid_output_structure | error | Returns object matching declared interface. All required fields present. Arrays never undefined. |
|
||||
| proper_error_handling | error | Throws descriptive Error with context. Validates inputs. Catches external API errors. |
|
||||
| ai_sdk_compliance | error | Uses `tool()` + `jsonSchema()` from 'ai'. Clear description. Every property has description. |
|
||||
| npm_publishable | error | Valid package.json with tpmjs field. Named + default exports. Proper types. Semver version. |
|
||||
| readme_documentation | error | README exists. Describes tool. Usage example. Documents inputs/outputs. |
|
||||
| deterministic_output | warning | Same input produces same output (where applicable). |
|
||||
| minimal_dependencies | warning | Uses stable, well-maintained packages. Avoids unnecessary deps. |
|
||||
|
||||
## Domain Rules
|
||||
|
||||
Common domain rule categories for the `domain_rules` field in blocks.yml:
|
||||
|
||||
- **Core implementation**: working code, proper types, error handling
|
||||
- **Web & fetch**: URL validation, content extraction, timeout handling
|
||||
- **Document generation**: format compliance, template rendering
|
||||
- **Data transformation**: schema validation, type coercion, encoding
|
||||
- **Engineering/code analysis**: AST parsing, complexity metrics
|
||||
- **Security & compliance**: input sanitization, safe execution
|
||||
- **Statistical rigor**: numerical accuracy, proper rounding
|
||||
- **Workflow/recipe**: step sequencing, state management
|
||||
|
||||
Define custom rules specific to your tool's requirements. Each rule needs an `id` and `description`.
|
||||
|
|
@ -105,8 +105,8 @@ export default {
|
|||
from: {},
|
||||
to: {
|
||||
couldNotResolve: true,
|
||||
// Allow TypeScript path aliases that are resolved by the TS compiler
|
||||
pathNot: ['^~/'],
|
||||
// Allow TypeScript path aliases and workspace packages that are resolved by the TS compiler
|
||||
pathNot: ['^~/', '^@/', '^@tpmjs/'],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -143,6 +143,10 @@ export default {
|
|||
doNotFollow: {
|
||||
path: ['node_modules', '\\.next', 'dist', '\\.turbo', 'storybook-static'],
|
||||
},
|
||||
exclude: {
|
||||
// Exclude railway-executor - it's a Deno app with HTTP imports that can't be resolved
|
||||
path: '^apps/railway-executor',
|
||||
},
|
||||
tsPreCompilationDeps: true,
|
||||
tsConfig: {
|
||||
fileName: './tsconfig.json',
|
||||
|
|
|
|||
80
.env.example
Normal file
80
.env.example
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# =============================================================================
|
||||
# TPMJS Environment Variables
|
||||
# =============================================================================
|
||||
# Copy this file to .env.local and fill in the values.
|
||||
# NEVER commit .env files with real secrets!
|
||||
#
|
||||
# Required variables are marked with [REQUIRED]
|
||||
# Optional variables are marked with [OPTIONAL]
|
||||
# =============================================================================
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Database [REQUIRED]
|
||||
# -----------------------------------------------------------------------------
|
||||
# Neon PostgreSQL connection string (get from https://console.neon.tech)
|
||||
DATABASE_URL="postgresql://user:password@host/database?sslmode=require"
|
||||
DATABASE_URL_UNPOOLED="postgresql://user:password@host/database?sslmode=require"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Authentication [REQUIRED for auth features]
|
||||
# -----------------------------------------------------------------------------
|
||||
# Better Auth secret - generate with: openssl rand -base64 32
|
||||
BETTER_AUTH_SECRET="your-32-char-minimum-secret-here"
|
||||
# Base URL for auth callbacks (optional, auto-detected in most cases)
|
||||
BETTER_AUTH_URL="http://localhost:3000"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Cron Jobs [REQUIRED for sync endpoints]
|
||||
# -----------------------------------------------------------------------------
|
||||
# Secret for authenticating Vercel Cron requests - generate with: openssl rand -hex 32
|
||||
CRON_SECRET="your-64-char-hex-secret-here"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# API Key Encryption [REQUIRED for API key features]
|
||||
# -----------------------------------------------------------------------------
|
||||
# Secret for encrypting user API keys - generate with: openssl rand -base64 32
|
||||
API_KEY_ENCRYPTION_SECRET="your-encryption-secret-here"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# External Services [OPTIONAL]
|
||||
# -----------------------------------------------------------------------------
|
||||
# Resend - for sending emails (https://resend.com)
|
||||
RESEND_API_KEY="re_your_resend_api_key"
|
||||
|
||||
# OpenAI - for AI features (https://platform.openai.com)
|
||||
OPENAI_API_KEY="sk-your-openai-api-key"
|
||||
|
||||
# Vercel KV - for rate limiting (auto-configured on Vercel)
|
||||
KV_REST_API_URL="https://your-kv-instance.kv.vercel-storage.com"
|
||||
KV_REST_API_TOKEN="your-kv-token"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Executor Services [OPTIONAL]
|
||||
# -----------------------------------------------------------------------------
|
||||
# Railway executor for tool execution
|
||||
RAILWAY_EXECUTOR_URL="https://your-railway-service.up.railway.app"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Discord Integration [OPTIONAL]
|
||||
# -----------------------------------------------------------------------------
|
||||
DISCORD_SUMMARY_AGENT_ID="your-agent-id"
|
||||
DISCORD_GUILD_ID="your-guild-id"
|
||||
DISCORD_SUMMARY_CHANNEL_ID="your-channel-id"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Public Variables (safe to expose to browser)
|
||||
# -----------------------------------------------------------------------------
|
||||
NEXT_PUBLIC_APP_URL="http://localhost:3000"
|
||||
NEXT_PUBLIC_API_URL="http://localhost:3000/api"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Development/Testing [OPTIONAL]
|
||||
# -----------------------------------------------------------------------------
|
||||
NODE_ENV="development"
|
||||
|
||||
# Integration test credentials (only for test environment)
|
||||
# INTEGRATION_TEST_SESSION_TOKEN="test-session-token"
|
||||
# INTEGRATION_TEST_API_KEY="test-api-key"
|
||||
# INTEGRATION_TEST_USER_ID="test-user-id"
|
||||
# INTEGRATION_TEST_USERNAME="test-username"
|
||||
# TEST_BASE_URL="http://localhost:3000"
|
||||
12
.gitallowed
Normal file
12
.gitallowed
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Allowed patterns for git-secrets (false positive exclusions)
|
||||
# These are documentation examples, not real secrets
|
||||
|
||||
# Example API keys in documentation
|
||||
tpmjs_sk_your_api_key_here
|
||||
tpmjs_sk_your_api_key
|
||||
tpmjs_sk_xxx
|
||||
tpmjs_sk_abc123
|
||||
tpmjs_sk_xxxxxxxxxxxxxxxxxxxx
|
||||
|
||||
# Ellipsized examples in docs (e.g., "tpmjs_sk_abc1...")
|
||||
tpmjs_sk_[a-z0-9]+\.\.\.
|
||||
66
.github/workflows/auto-close-published.yml
vendored
Normal file
66
.github/workflows/auto-close-published.yml
vendored
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
name: Auto-Close Published Issues
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run every hour to check for issues to close
|
||||
- cron: '0 * * * *'
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
|
||||
jobs:
|
||||
auto-close:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- name: Close published issues older than 24h
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { data: issues } = await github.rest.issues.listForRepo({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: 'published',
|
||||
state: 'open',
|
||||
per_page: 100
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
|
||||
for (const issue of issues) {
|
||||
// Find when 'published' label was added
|
||||
const { data: events } = await github.rest.issues.listEvents({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
per_page: 100
|
||||
});
|
||||
|
||||
const publishedEvent = events
|
||||
.filter(e => e.event === 'labeled' && e.label?.name === 'published')
|
||||
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at))[0];
|
||||
|
||||
if (publishedEvent) {
|
||||
const labeledAt = new Date(publishedEvent.created_at);
|
||||
|
||||
if (labeledAt < twentyFourHoursAgo) {
|
||||
console.log(`Closing issue #${issue.number} - published ${labeledAt.toISOString()}`);
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: 'Auto-closing after 24 hours. The tool has been published successfully. Reopen if you encounter any issues.'
|
||||
});
|
||||
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
state: 'closed',
|
||||
state_reason: 'completed'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
72
.github/workflows/build-omega-mac.yml
vendored
Normal file
72
.github/workflows/build-omega-mac.yml
vendored
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
name: Build Omega Mac
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Select Xcode
|
||||
run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer
|
||||
|
||||
- name: Resolve dependencies
|
||||
working-directory: apps/omega-mac
|
||||
run: swift package resolve
|
||||
|
||||
- name: Build release
|
||||
working-directory: apps/omega-mac
|
||||
run: swift build -c release
|
||||
|
||||
- name: Package .app bundle
|
||||
working-directory: apps/omega-mac
|
||||
run: |
|
||||
mkdir -p OmegaMac.app/Contents/MacOS
|
||||
mkdir -p OmegaMac.app/Contents/Resources
|
||||
cp .build/release/OmegaMac OmegaMac.app/Contents/MacOS/OmegaMac
|
||||
|
||||
cat > OmegaMac.app/Contents/Info.plist << 'PLIST'
|
||||
<?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>CFBundleExecutable</key>
|
||||
<string>OmegaMac</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.tpmjs.omega-mac</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Omega</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Omega</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.0</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>14.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<false/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
PLIST
|
||||
|
||||
codesign --force --sign - OmegaMac.app
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: OmegaMac
|
||||
path: apps/omega-mac/OmegaMac.app
|
||||
44
.github/workflows/claude-code-review.yml
vendored
Normal file
44
.github/workflows/claude-code-review.yml
vendored
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
name: Claude Code Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, ready_for_review, reopened]
|
||||
# Optional: Only run on specific file changes
|
||||
# paths:
|
||||
# - "src/**/*.ts"
|
||||
# - "src/**/*.tsx"
|
||||
# - "src/**/*.js"
|
||||
# - "src/**/*.jsx"
|
||||
|
||||
jobs:
|
||||
claude-review:
|
||||
# Optional: Filter by PR author
|
||||
# if: |
|
||||
# github.event.pull_request.user.login == 'external-contributor' ||
|
||||
# github.event.pull_request.user.login == 'new-developer' ||
|
||||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code Review
|
||||
id: claude-review
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
|
||||
plugins: 'code-review@claude-code-plugins'
|
||||
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://code.claude.com/docs/en/cli-reference for available options
|
||||
|
||||
96
.github/workflows/claude.yml
vendored
Normal file
96
.github/workflows/claude.yml
vendored
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
name: Claude Code
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened, assigned, labeled]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
# Standard Claude trigger - responds to @claude mentions
|
||||
claude:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||
(github.event_name == 'issues' && (github.event.action == 'opened' || github.event.action == 'assigned') && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # Push branches, create commits
|
||||
pull-requests: write # Create and manage PRs
|
||||
issues: write # Manage labels, close issues
|
||||
id-token: write
|
||||
actions: read # Read CI results on PRs
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@v1
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
CRON_SECRET: ${{ secrets.CRON_SECRET }}
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
additional_permissions: |
|
||||
actions: read
|
||||
|
||||
# Label-triggered Claude - for tool-request pipeline
|
||||
claude-label-trigger:
|
||||
if: github.event_name == 'issues' && github.event.action == 'labeled' && github.event.label.name == 'claude-working'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
id-token: write
|
||||
actions: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Get prompt from issue comments
|
||||
id: get-prompt
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const comments = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
per_page: 100
|
||||
});
|
||||
|
||||
// Find the most recent comment with @claude
|
||||
const claudeComments = comments.data.filter(c => c.body.includes('@claude'));
|
||||
if (claudeComments.length > 0) {
|
||||
const latestComment = claudeComments[claudeComments.length - 1];
|
||||
core.setOutput('prompt', latestComment.body);
|
||||
core.setOutput('found', 'true');
|
||||
} else {
|
||||
core.setOutput('found', 'false');
|
||||
core.setFailed('No @claude comment found in issue');
|
||||
}
|
||||
|
||||
- name: Run Claude Code
|
||||
if: steps.get-prompt.outputs.found == 'true'
|
||||
uses: anthropics/claude-code-action@v1
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
CRON_SECRET: ${{ secrets.CRON_SECRET }}
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
additional_permissions: |
|
||||
actions: read
|
||||
prompt: ${{ steps.get-prompt.outputs.prompt }}
|
||||
# Allow gh CLI for issue management, npm for publishing
|
||||
claude_args: '--allowedTools "Bash(gh:*)" "Bash(npm:*)" "Bash(pnpm:*)" "Bash(git:*)"'
|
||||
48
.github/workflows/discord-summary.yml
vendored
Normal file
48
.github/workflows/discord-summary.yml
vendored
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
name: Discord Daily Summary
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run daily at 9 AM UTC
|
||||
- cron: '0 9 * * *'
|
||||
workflow_dispatch:
|
||||
# Allow manual trigger
|
||||
|
||||
jobs:
|
||||
post-summary:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Generate conversation ID with date
|
||||
id: conv-id
|
||||
run: |
|
||||
# Create a date-based conversation ID like "discord-summary-2026-01-11"
|
||||
CONV_ID="discord-summary-$(date -u +%Y-%m-%d)"
|
||||
echo "conv_id=$CONV_ID" >> $GITHUB_OUTPUT
|
||||
echo "Generated conversation ID: $CONV_ID"
|
||||
|
||||
- name: Trigger Discord Summary Agent
|
||||
env:
|
||||
TPMJS_API_KEY: ${{ secrets.TPMJS_API_KEY }}
|
||||
run: |
|
||||
echo "Triggering agent with conversation: ${{ steps.conv-id.outputs.conv_id }}"
|
||||
|
||||
# POST to the agent conversation endpoint
|
||||
# Uses username/agent-slug URL format: /api/{username}/agents/{agent-slug}/conversation/{conv-id}
|
||||
# Agent: ajax/tpmjs-discord
|
||||
# Requires API key with agent:chat scope
|
||||
RESPONSE=$(curl -s -X POST \
|
||||
"https://tpmjs.com/api/ajax/agents/tpmjs-discord/conversation/${{ steps.conv-id.outputs.conv_id }}" \
|
||||
-H "Authorization: Bearer $TPMJS_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"message": "Read the Discord server (guild ID 1349727923434815519) for the past 24 hours, excluding bots. Then post a detailed summary with an embed to channel 1442666515425132644. Include key discussions, announcements, and any action items."
|
||||
}' \
|
||||
--max-time 300)
|
||||
|
||||
echo "Response received"
|
||||
# The response is SSE, so we just check if we got something back
|
||||
if [ -z "$RESPONSE" ]; then
|
||||
echo "Error: No response from agent"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Summary triggered successfully"
|
||||
279
.github/workflows/endpoint-health-check.yml
vendored
Normal file
279
.github/workflows/endpoint-health-check.yml
vendored
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
name: Endpoint Health Check
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run every 5 minutes
|
||||
- cron: '*/5 * * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
verbose:
|
||||
description: 'Enable verbose output'
|
||||
required: false
|
||||
default: 'false'
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
BASE_URL: ${{ secrets.VERCEL_PRODUCTION_URL || 'https://tpmjs.com' }}
|
||||
# Test data
|
||||
TEST_USERNAME: ajax
|
||||
TEST_COLLECTION_SLUG: ajax-collection-tbc
|
||||
|
||||
jobs:
|
||||
health-check:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Setup
|
||||
run: |
|
||||
echo "Starting health checks at $(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
||||
echo "Base URL: $BASE_URL"
|
||||
|
||||
- name: Check Basic Health Endpoint
|
||||
id: basic-health
|
||||
run: |
|
||||
echo "Testing: GET /api/health"
|
||||
RESPONSE=$(curl -s -w "\n%{http_code}" "$BASE_URL/api/health" --connect-timeout 10 --max-time 30)
|
||||
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
|
||||
BODY=$(echo "$RESPONSE" | sed '$d')
|
||||
|
||||
echo "HTTP Status: $HTTP_CODE"
|
||||
if [ "${{ inputs.verbose }}" = "true" ]; then
|
||||
echo "Response: $BODY"
|
||||
fi
|
||||
|
||||
if [ "$HTTP_CODE" -eq 200 ]; then
|
||||
echo "status=pass" >> $GITHUB_OUTPUT
|
||||
echo "✅ Basic health check passed"
|
||||
else
|
||||
echo "status=fail" >> $GITHUB_OUTPUT
|
||||
echo "❌ Basic health check failed with status $HTTP_CODE"
|
||||
fi
|
||||
|
||||
- name: Check Database Health
|
||||
id: db-health
|
||||
run: |
|
||||
echo "Testing: GET /api/tools (database connectivity)"
|
||||
RESPONSE=$(curl -s -w "\n%{http_code}" "$BASE_URL/api/tools?limit=1" --connect-timeout 10 --max-time 30)
|
||||
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
|
||||
BODY=$(echo "$RESPONSE" | sed '$d')
|
||||
|
||||
echo "HTTP Status: $HTTP_CODE"
|
||||
if [ "${{ inputs.verbose }}" = "true" ]; then
|
||||
echo "Response: $BODY"
|
||||
fi
|
||||
|
||||
if [ "$HTTP_CODE" -eq 200 ]; then
|
||||
echo "status=pass" >> $GITHUB_OUTPUT
|
||||
echo "✅ Database health check passed"
|
||||
else
|
||||
echo "status=fail" >> $GITHUB_OUTPUT
|
||||
echo "❌ Database health check failed with status $HTTP_CODE"
|
||||
fi
|
||||
|
||||
- name: Check Platform Stats API
|
||||
id: stats-api
|
||||
run: |
|
||||
echo "Testing: GET /api/stats"
|
||||
RESPONSE=$(curl -s -w "\n%{http_code}" "$BASE_URL/api/stats" --connect-timeout 10 --max-time 30)
|
||||
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
|
||||
BODY=$(echo "$RESPONSE" | sed '$d')
|
||||
|
||||
echo "HTTP Status: $HTTP_CODE"
|
||||
if [ "${{ inputs.verbose }}" = "true" ]; then
|
||||
echo "Response: $BODY" | head -c 500
|
||||
fi
|
||||
|
||||
if [ "$HTTP_CODE" -eq 200 ] && echo "$BODY" | grep -q '"success":true'; then
|
||||
echo "status=pass" >> $GITHUB_OUTPUT
|
||||
echo "✅ Platform stats API check passed"
|
||||
else
|
||||
echo "status=fail" >> $GITHUB_OUTPUT
|
||||
echo "❌ Platform stats API check failed with status $HTTP_CODE"
|
||||
fi
|
||||
|
||||
- name: Check MCP HTTP Transport - Initialize
|
||||
id: mcp-http-init
|
||||
run: |
|
||||
echo "Testing: POST /api/mcp/$TEST_USERNAME/$TEST_COLLECTION_SLUG/http (initialize)"
|
||||
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
|
||||
"$BASE_URL/api/mcp/$TEST_USERNAME/$TEST_COLLECTION_SLUG/http" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer ${{ secrets.INTEGRATION_TEST_API_KEY }}" \
|
||||
-d '{"jsonrpc":"2.0","id":1,"method":"initialize"}' \
|
||||
--connect-timeout 15 --max-time 30)
|
||||
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
|
||||
BODY=$(echo "$RESPONSE" | sed '$d')
|
||||
|
||||
echo "HTTP Status: $HTTP_CODE"
|
||||
if [ "${{ inputs.verbose }}" = "true" ]; then
|
||||
echo "Response: $BODY"
|
||||
fi
|
||||
|
||||
# Check for successful JSON-RPC response
|
||||
if [ "$HTTP_CODE" -eq 200 ] && echo "$BODY" | grep -q '"result"'; then
|
||||
echo "status=pass" >> $GITHUB_OUTPUT
|
||||
echo "✅ MCP HTTP initialize check passed"
|
||||
else
|
||||
echo "status=fail" >> $GITHUB_OUTPUT
|
||||
echo "❌ MCP HTTP initialize check failed"
|
||||
fi
|
||||
|
||||
- name: Check MCP HTTP Transport - Tools List
|
||||
id: mcp-http-tools
|
||||
run: |
|
||||
echo "Testing: POST /api/mcp/$TEST_USERNAME/$TEST_COLLECTION_SLUG/http (tools/list)"
|
||||
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
|
||||
"$BASE_URL/api/mcp/$TEST_USERNAME/$TEST_COLLECTION_SLUG/http" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer ${{ secrets.INTEGRATION_TEST_API_KEY }}" \
|
||||
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
|
||||
--connect-timeout 15 --max-time 30)
|
||||
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
|
||||
BODY=$(echo "$RESPONSE" | sed '$d')
|
||||
|
||||
echo "HTTP Status: $HTTP_CODE"
|
||||
if [ "${{ inputs.verbose }}" = "true" ]; then
|
||||
echo "Response: $BODY" | head -c 500
|
||||
fi
|
||||
|
||||
# Check for successful JSON-RPC response with tools
|
||||
if [ "$HTTP_CODE" -eq 200 ] && echo "$BODY" | grep -q '"tools"'; then
|
||||
echo "status=pass" >> $GITHUB_OUTPUT
|
||||
echo "✅ MCP HTTP tools/list check passed"
|
||||
else
|
||||
echo "status=fail" >> $GITHUB_OUTPUT
|
||||
echo "❌ MCP HTTP tools/list check failed"
|
||||
fi
|
||||
|
||||
- name: Check MCP SSE Transport
|
||||
id: mcp-sse
|
||||
run: |
|
||||
echo "Testing: POST /api/mcp/$TEST_USERNAME/$TEST_COLLECTION_SLUG/sse (initialize)"
|
||||
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
|
||||
"$BASE_URL/api/mcp/$TEST_USERNAME/$TEST_COLLECTION_SLUG/sse" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer ${{ secrets.INTEGRATION_TEST_API_KEY }}" \
|
||||
-d '{"jsonrpc":"2.0","id":1,"method":"initialize"}' \
|
||||
--connect-timeout 15 --max-time 30)
|
||||
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
|
||||
BODY=$(echo "$RESPONSE" | sed '$d')
|
||||
|
||||
echo "HTTP Status: $HTTP_CODE"
|
||||
if [ "${{ inputs.verbose }}" = "true" ]; then
|
||||
echo "Response: $BODY"
|
||||
fi
|
||||
|
||||
# Check for SSE response with data prefix
|
||||
if [ "$HTTP_CODE" -eq 200 ] && echo "$BODY" | grep -q 'data:'; then
|
||||
echo "status=pass" >> $GITHUB_OUTPUT
|
||||
echo "✅ MCP SSE check passed"
|
||||
else
|
||||
echo "status=fail" >> $GITHUB_OUTPUT
|
||||
echo "❌ MCP SSE check failed"
|
||||
fi
|
||||
|
||||
- name: Check MCP Server Info (GET)
|
||||
id: mcp-info
|
||||
run: |
|
||||
echo "Testing: GET /api/mcp/$TEST_USERNAME/$TEST_COLLECTION_SLUG/http"
|
||||
RESPONSE=$(curl -s -w "\n%{http_code}" \
|
||||
"$BASE_URL/api/mcp/$TEST_USERNAME/$TEST_COLLECTION_SLUG/http" \
|
||||
--connect-timeout 10 --max-time 20)
|
||||
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
|
||||
BODY=$(echo "$RESPONSE" | sed '$d')
|
||||
|
||||
echo "HTTP Status: $HTTP_CODE"
|
||||
if [ "${{ inputs.verbose }}" = "true" ]; then
|
||||
echo "Response: $BODY"
|
||||
fi
|
||||
|
||||
if [ "$HTTP_CODE" -eq 200 ] && echo "$BODY" | grep -q '"protocol":"mcp"'; then
|
||||
echo "status=pass" >> $GITHUB_OUTPUT
|
||||
echo "✅ MCP server info check passed"
|
||||
else
|
||||
echo "status=fail" >> $GITHUB_OUTPUT
|
||||
echo "❌ MCP server info check failed"
|
||||
fi
|
||||
|
||||
- name: Check Tool Health Stats
|
||||
id: tool-health-stats
|
||||
run: |
|
||||
echo "Testing: GET /api/stats/health"
|
||||
RESPONSE=$(curl -s -w "\n%{http_code}" "$BASE_URL/api/stats/health" --connect-timeout 10 --max-time 30)
|
||||
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
|
||||
BODY=$(echo "$RESPONSE" | sed '$d')
|
||||
|
||||
echo "HTTP Status: $HTTP_CODE"
|
||||
if [ "${{ inputs.verbose }}" = "true" ]; then
|
||||
echo "Response: $BODY" | head -c 500
|
||||
fi
|
||||
|
||||
if [ "$HTTP_CODE" -eq 200 ] && echo "$BODY" | grep -q '"success":true'; then
|
||||
echo "status=pass" >> $GITHUB_OUTPUT
|
||||
echo "✅ Tool health stats check passed"
|
||||
else
|
||||
echo "status=fail" >> $GITHUB_OUTPUT
|
||||
echo "❌ Tool health stats check failed"
|
||||
fi
|
||||
|
||||
- name: Report Health Status to API
|
||||
if: always()
|
||||
run: |
|
||||
# Collect all results
|
||||
RESULTS=$(cat << EOF
|
||||
{
|
||||
"timestamp": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
|
||||
"source": "github-actions",
|
||||
"runId": "${{ github.run_id }}",
|
||||
"checks": {
|
||||
"basic_health": "${{ steps.basic-health.outputs.status }}",
|
||||
"database": "${{ steps.db-health.outputs.status }}",
|
||||
"stats_api": "${{ steps.stats-api.outputs.status }}",
|
||||
"mcp_http_init": "${{ steps.mcp-http-init.outputs.status }}",
|
||||
"mcp_http_tools": "${{ steps.mcp-http-tools.outputs.status }}",
|
||||
"mcp_sse": "${{ steps.mcp-sse.outputs.status }}",
|
||||
"mcp_info": "${{ steps.mcp-info.outputs.status }}",
|
||||
"tool_health_stats": "${{ steps.tool-health-stats.outputs.status }}"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
echo "Health Check Results:"
|
||||
echo "$RESULTS" | jq .
|
||||
|
||||
# Report to the health status API if secret is available
|
||||
if [ -n "${{ secrets.CRON_SECRET }}" ]; then
|
||||
curl -s -X POST "$BASE_URL/api/health/report" \
|
||||
-H "Authorization: Bearer ${{ secrets.CRON_SECRET }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$RESULTS" || true
|
||||
fi
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "## Health Check Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Endpoint | Status |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|----------|--------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Basic Health | ${{ steps.basic-health.outputs.status == 'pass' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Database | ${{ steps.db-health.outputs.status == 'pass' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Platform Stats | ${{ steps.stats-api.outputs.status == 'pass' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| MCP HTTP Init | ${{ steps.mcp-http-init.outputs.status == 'pass' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| MCP HTTP Tools | ${{ steps.mcp-http-tools.outputs.status == 'pass' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| MCP SSE | ${{ steps.mcp-sse.outputs.status == 'pass' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| MCP Server Info | ${{ steps.mcp-info.outputs.status == 'pass' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Tool Health Stats | ${{ steps.tool-health-stats.outputs.status == 'pass' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Fail if any check failed
|
||||
if: |
|
||||
steps.basic-health.outputs.status == 'fail' ||
|
||||
steps.db-health.outputs.status == 'fail' ||
|
||||
steps.mcp-http-init.outputs.status == 'fail' ||
|
||||
steps.mcp-http-tools.outputs.status == 'fail' ||
|
||||
steps.mcp-sse.outputs.status == 'fail'
|
||||
run: |
|
||||
echo "One or more critical health checks failed!"
|
||||
exit 1
|
||||
18
.github/workflows/health-check.yml
vendored
Normal file
18
.github/workflows/health-check.yml
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
name: Daily Health Check
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run daily at 2am UTC
|
||||
- cron: '0 2 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
health-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger health check sync
|
||||
run: |
|
||||
curl -X POST "${{ secrets.VERCEL_PRODUCTION_URL }}/api/sync/health-check" \
|
||||
-H "Authorization: Bearer ${{ secrets.CRON_SECRET }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-f -s -S -w "\nHTTP Status: %{http_code}\n"
|
||||
135
.github/workflows/integration-tests.yml
vendored
Normal file
135
.github/workflows/integration-tests.yml
vendored
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
name: Integration Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
verbose:
|
||||
description: 'Run tests in verbose mode'
|
||||
required: false
|
||||
default: 'false'
|
||||
type: choice
|
||||
options:
|
||||
- 'true'
|
||||
- 'false'
|
||||
|
||||
concurrency:
|
||||
group: integration-tests-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
integration-tests:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.14.0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build packages
|
||||
run: pnpm build
|
||||
|
||||
- name: Cleanup orphaned test data (pre-test)
|
||||
env:
|
||||
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||
INTEGRATION_TEST_USER_ID: ${{ secrets.INTEGRATION_TEST_USER_ID }}
|
||||
run: pnpm --filter=@tpmjs/web test:cleanup-orphans
|
||||
|
||||
- name: Setup OpenAI key for test user
|
||||
env:
|
||||
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||
API_KEY_ENCRYPTION_SECRET: ${{ secrets.API_KEY_ENCRYPTION_SECRET }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
INTEGRATION_TEST_USER_ID: ${{ secrets.INTEGRATION_TEST_USER_ID }}
|
||||
run: pnpm --filter=@tpmjs/web test:setup-openai-key
|
||||
|
||||
- name: Wait for API
|
||||
run: |
|
||||
echo "Checking if API is available at $TEST_BASE_URL..."
|
||||
for i in {1..30}; do
|
||||
if curl -sf "$TEST_BASE_URL/api/health" > /dev/null 2>&1; then
|
||||
echo "✅ API is available"
|
||||
exit 0
|
||||
fi
|
||||
echo "Attempt $i/30: API not ready yet, waiting..."
|
||||
sleep 2
|
||||
done
|
||||
echo "❌ API is not available after 60 seconds"
|
||||
exit 1
|
||||
env:
|
||||
TEST_BASE_URL: ${{ secrets.TEST_BASE_URL }}
|
||||
|
||||
- name: Run integration tests
|
||||
env:
|
||||
INTEGRATION_TESTS: 'true'
|
||||
TEST_BASE_URL: ${{ secrets.TEST_BASE_URL }}
|
||||
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||
INTEGRATION_TEST_USER_ID: ${{ secrets.INTEGRATION_TEST_USER_ID }}
|
||||
INTEGRATION_TEST_USERNAME: ${{ secrets.INTEGRATION_TEST_USERNAME }}
|
||||
INTEGRATION_TEST_SESSION_TOKEN: ${{ secrets.INTEGRATION_TEST_SESSION_TOKEN }}
|
||||
INTEGRATION_TEST_API_KEY: ${{ secrets.INTEGRATION_TEST_API_KEY }}
|
||||
CRON_SECRET: ${{ secrets.CRON_SECRET }}
|
||||
run: |
|
||||
if [ "${{ github.event.inputs.verbose }}" = "true" ]; then
|
||||
pnpm --filter=@tpmjs/web test:integration -- --reporter=verbose
|
||||
else
|
||||
pnpm --filter=@tpmjs/web test:integration
|
||||
fi
|
||||
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: integration-test-results
|
||||
path: |
|
||||
apps/web/test-results/
|
||||
apps/web/coverage/
|
||||
retention-days: 7
|
||||
|
||||
cleanup:
|
||||
runs-on: ubuntu-latest
|
||||
needs: integration-tests
|
||||
if: always()
|
||||
timeout-minutes: 5
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.14.0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build @tpmjs/db
|
||||
run: pnpm --filter=@tpmjs/db build
|
||||
|
||||
- name: Cleanup orphaned test data
|
||||
env:
|
||||
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||
INTEGRATION_TEST_USER_ID: ${{ secrets.INTEGRATION_TEST_USER_ID }}
|
||||
run: pnpm --filter=@tpmjs/web test:cleanup-orphans
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -23,7 +23,7 @@ jobs:
|
|||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 21
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
|
|
|
|||
18
.github/workflows/sync-changes.yml
vendored
Normal file
18
.github/workflows/sync-changes.yml
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
name: Sync NPM Changes Feed
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run every 2 minutes
|
||||
- cron: '*/2 * * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
sync-changes:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger changes feed sync
|
||||
run: |
|
||||
curl -X POST "${{ secrets.VERCEL_PRODUCTION_URL }}/api/sync/changes" \
|
||||
-H "Authorization: Bearer ${{ secrets.CRON_SECRET }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-f -s -S -w "\nHTTP Status: %{http_code}\n"
|
||||
114
.github/workflows/sync-enrich.yml
vendored
Normal file
114
.github/workflows/sync-enrich.yml
vendored
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
name: Sync Tool Enrichment
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run every 2 minutes
|
||||
- cron: '*/2 * * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
sync-enrich:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger enrichment sync
|
||||
id: sync
|
||||
run: |
|
||||
# Call the sync API and capture response
|
||||
response=$(curl -X POST "${{ secrets.VERCEL_PRODUCTION_URL }}/api/sync/enrich" \
|
||||
-H "Authorization: Bearer ${{ secrets.CRON_SECRET }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-f -s -S)
|
||||
|
||||
echo "Response: $response"
|
||||
|
||||
# Extract data using jq
|
||||
enriched=$(echo "$response" | jq -r '.data.enriched')
|
||||
discovered=$(echo "$response" | jq -r '.data.discovered')
|
||||
skipped=$(echo "$response" | jq -r '.data.skipped')
|
||||
errors=$(echo "$response" | jq -r '.data.errors')
|
||||
durationMs=$(echo "$response" | jq -r '.data.durationMs')
|
||||
|
||||
# Extract and display error messages
|
||||
errorMessages=$(echo "$response" | jq -r '.data.errorMessages[]?' 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$errorMessages" ]; then
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "⚠️ ENRICHMENT ERRORS ($errors total):"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "$response" | jq -r '.data.errorMessages[]?' | while IFS= read -r error; do
|
||||
echo " • $error"
|
||||
done
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
fi
|
||||
|
||||
# Set outputs for Discord notification
|
||||
echo "enriched=$enriched" >> $GITHUB_OUTPUT
|
||||
echo "discovered=$discovered" >> $GITHUB_OUTPUT
|
||||
echo "skipped=$skipped" >> $GITHUB_OUTPUT
|
||||
echo "errors=$errors" >> $GITHUB_OUTPUT
|
||||
echo "durationMs=$durationMs" >> $GITHUB_OUTPUT
|
||||
|
||||
# Store error messages for Discord (first 3, truncated)
|
||||
if [ "$errors" -gt 0 ]; then
|
||||
errorSummary=$(echo "$response" | jq -r '.data.errorMessages[0:3]? | join("\n• ")' 2>/dev/null || echo "")
|
||||
if [ -n "$errorSummary" ]; then
|
||||
echo "• $errorSummary" > /tmp/error_summary.txt
|
||||
fi
|
||||
fi
|
||||
|
||||
# Determine status emoji
|
||||
if [ "$errors" -gt 0 ]; then
|
||||
echo "status_emoji=⚠️" >> $GITHUB_OUTPUT
|
||||
echo "status_color=16776960" >> $GITHUB_OUTPUT # Yellow
|
||||
else
|
||||
echo "status_emoji=✅" >> $GITHUB_OUTPUT
|
||||
echo "status_color=5763719" >> $GITHUB_OUTPUT # Green
|
||||
fi
|
||||
|
||||
- name: Send Discord notification
|
||||
if: always()
|
||||
run: |
|
||||
# Format duration
|
||||
duration_sec=$(echo "scale=2; ${{ steps.sync.outputs.durationMs }} / 1000" | bc)
|
||||
|
||||
# Build Discord payload using jq for proper JSON escaping
|
||||
error_text=""
|
||||
|
||||
if [ -f /tmp/error_summary.txt ] && [ ${{ steps.sync.outputs.errors }} -gt 0 ]; then
|
||||
error_text=$(cat /tmp/error_summary.txt | head -c 800)
|
||||
fi
|
||||
|
||||
# Build fields array dynamically
|
||||
base_fields='[
|
||||
{ "name": "🔧 Enriched", "value": "${{ steps.sync.outputs.enriched }}", "inline": true },
|
||||
{ "name": "🔍 Discovered", "value": "${{ steps.sync.outputs.discovered }}", "inline": true },
|
||||
{ "name": "⏭️ Skipped", "value": "${{ steps.sync.outputs.skipped }}", "inline": true },
|
||||
{ "name": "❌ Errors", "value": "${{ steps.sync.outputs.errors }}", "inline": true },
|
||||
{ "name": "⏱️ Duration", "value": "'"${duration_sec}s"'", "inline": true },
|
||||
{ "name": "🔗 Run", "value": "[View Logs](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})", "inline": true }
|
||||
]'
|
||||
|
||||
# Create payload with dynamic fields
|
||||
payload=$(jq -n \
|
||||
--arg title "${{ steps.sync.outputs.status_emoji }} Tool Enrichment Sync" \
|
||||
--argjson color ${{ steps.sync.outputs.status_color }} \
|
||||
--argjson baseFields "$base_fields" \
|
||||
--arg error_text "$error_text" \
|
||||
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" \
|
||||
'
|
||||
{
|
||||
embeds: [{
|
||||
title: $title,
|
||||
color: $color,
|
||||
fields: (
|
||||
$baseFields +
|
||||
(if $error_text != "" then [{ name: "🔍 Error Details", value: ("```\n" + $error_text + "\n```"), inline: false }] else [] end)
|
||||
),
|
||||
timestamp: $timestamp
|
||||
}]
|
||||
}')
|
||||
|
||||
# Send to Discord
|
||||
curl -X POST "${{ secrets.DISCORD_WEBHOOK }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload"
|
||||
131
.github/workflows/sync-keyword.yml
vendored
Normal file
131
.github/workflows/sync-keyword.yml
vendored
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
name: Sync NPM Keyword Search
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run every 15 minutes
|
||||
- cron: '*/15 * * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
sync-keyword:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger keyword search sync
|
||||
id: sync
|
||||
run: |
|
||||
# Call the sync API and capture response
|
||||
response=$(curl -X POST "${{ secrets.VERCEL_PRODUCTION_URL }}/api/sync/keyword" \
|
||||
-H "Authorization: Bearer ${{ secrets.CRON_SECRET }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-f -s -S)
|
||||
|
||||
echo "Response: $response"
|
||||
|
||||
# Extract data using jq
|
||||
processed=$(echo "$response" | jq -r '.data.processed')
|
||||
skipped=$(echo "$response" | jq -r '.data.skipped')
|
||||
errors=$(echo "$response" | jq -r '.data.errors')
|
||||
packagesFound=$(echo "$response" | jq -r '.data.packagesFound')
|
||||
durationMs=$(echo "$response" | jq -r '.data.durationMs')
|
||||
|
||||
# Extract and display error messages
|
||||
errorMessages=$(echo "$response" | jq -r '.data.errorMessages[]?' 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$errorMessages" ]; then
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "⚠️ SYNC ERRORS ($errors total):"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "$response" | jq -r '.data.errorMessages[]?' | while IFS= read -r error; do
|
||||
echo " • $error"
|
||||
done
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
fi
|
||||
|
||||
# Set outputs for Discord notification
|
||||
echo "processed=$processed" >> $GITHUB_OUTPUT
|
||||
echo "skipped=$skipped" >> $GITHUB_OUTPUT
|
||||
echo "errors=$errors" >> $GITHUB_OUTPUT
|
||||
echo "packagesFound=$packagesFound" >> $GITHUB_OUTPUT
|
||||
echo "durationMs=$durationMs" >> $GITHUB_OUTPUT
|
||||
|
||||
# Store error messages for Discord (first 3, truncated)
|
||||
if [ "$errors" -gt 0 ]; then
|
||||
errorSummary=$(echo "$response" | jq -r '.data.errorMessages[0:3]? | join("\n• ")' 2>/dev/null || echo "")
|
||||
if [ -n "$errorSummary" ]; then
|
||||
# Save to file to preserve newlines
|
||||
echo "• $errorSummary" > /tmp/error_summary.txt
|
||||
fi
|
||||
fi
|
||||
|
||||
# Store skipped packages for Discord
|
||||
if [ "$skipped" -gt 0 ]; then
|
||||
skippedList=$(echo "$response" | jq -r '.data.skippedPackages[]? | "\(.name) (by \(.author)) - \(.reason)"' 2>/dev/null | paste -sd "\n" - || echo "")
|
||||
if [ -n "$skippedList" ]; then
|
||||
echo "$skippedList" > /tmp/skipped_packages.txt
|
||||
fi
|
||||
fi
|
||||
|
||||
# Determine status emoji
|
||||
if [ "$errors" -gt 0 ]; then
|
||||
echo "status_emoji=⚠️" >> $GITHUB_OUTPUT
|
||||
echo "status_color=16776960" >> $GITHUB_OUTPUT # Yellow
|
||||
else
|
||||
echo "status_emoji=✅" >> $GITHUB_OUTPUT
|
||||
echo "status_color=5763719" >> $GITHUB_OUTPUT # Green
|
||||
fi
|
||||
|
||||
- name: Send Discord notification
|
||||
if: always()
|
||||
run: |
|
||||
# Format duration
|
||||
duration_sec=$(echo "scale=2; ${{ steps.sync.outputs.durationMs }} / 1000" | bc)
|
||||
|
||||
# Build Discord payload using jq for proper JSON escaping
|
||||
# Read optional data
|
||||
error_text=""
|
||||
skipped_text=""
|
||||
|
||||
if [ -f /tmp/error_summary.txt ] && [ ${{ steps.sync.outputs.errors }} -gt 0 ]; then
|
||||
error_text=$(cat /tmp/error_summary.txt | head -c 800)
|
||||
fi
|
||||
|
||||
if [ -f /tmp/skipped_packages.txt ] && [ ${{ steps.sync.outputs.skipped }} -gt 0 ]; then
|
||||
skipped_text=$(cat /tmp/skipped_packages.txt)
|
||||
fi
|
||||
|
||||
# Build fields array dynamically
|
||||
base_fields='[
|
||||
{ "name": "📦 Packages Found", "value": "${{ steps.sync.outputs.packagesFound }}", "inline": true },
|
||||
{ "name": "✨ Processed", "value": "${{ steps.sync.outputs.processed }}", "inline": true },
|
||||
{ "name": "⏭️ Skipped", "value": "${{ steps.sync.outputs.skipped }}", "inline": true },
|
||||
{ "name": "❌ Errors", "value": "${{ steps.sync.outputs.errors }}", "inline": true },
|
||||
{ "name": "⏱️ Duration", "value": "'"${duration_sec}s"'", "inline": true },
|
||||
{ "name": "🔗 Run", "value": "[View Logs](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})", "inline": true }
|
||||
]'
|
||||
|
||||
# Create payload with dynamic fields
|
||||
payload=$(jq -n \
|
||||
--arg title "${{ steps.sync.outputs.status_emoji }} NPM Keyword Search Sync" \
|
||||
--argjson color ${{ steps.sync.outputs.status_color }} \
|
||||
--argjson baseFields "$base_fields" \
|
||||
--arg error_text "$error_text" \
|
||||
--arg skipped_text "$skipped_text" \
|
||||
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" \
|
||||
'
|
||||
{
|
||||
embeds: [{
|
||||
title: $title,
|
||||
color: $color,
|
||||
fields: (
|
||||
$baseFields +
|
||||
(if $skipped_text != "" then [{ name: "📋 Skipped Packages", value: $skipped_text, inline: false }] else [] end) +
|
||||
(if $error_text != "" then [{ name: "🔍 Error Details", value: ("```\n" + $error_text + "\n```"), inline: false }] else [] end)
|
||||
),
|
||||
timestamp: $timestamp
|
||||
}]
|
||||
}')
|
||||
|
||||
# Send to Discord
|
||||
curl -X POST "${{ secrets.DISCORD_WEBHOOK }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload"
|
||||
97
.github/workflows/sync-manual.yml
vendored
Normal file
97
.github/workflows/sync-manual.yml
vendored
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
name: Sync Manual Tools
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run daily at midnight UTC
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
# Run on pushes to main that modify manual-tools.ts
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'manual-tools.ts'
|
||||
- 'sync-manual-tools.ts'
|
||||
|
||||
jobs:
|
||||
sync-manual:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Generate Prisma Client
|
||||
run: pnpm --filter=@tpmjs/db db:generate
|
||||
|
||||
- name: Run manual tools sync
|
||||
id: sync
|
||||
run: |
|
||||
# Run the sync script and capture output
|
||||
output=$(pnpm tsx sync-manual-tools.ts 2>&1)
|
||||
echo "$output"
|
||||
|
||||
# Extract statistics from output
|
||||
processed=$(echo "$output" | grep "Processed:" | awk '{print $2}')
|
||||
skipped=$(echo "$output" | grep "Skipped:" | awk '{print $2}')
|
||||
errors=$(echo "$output" | grep "Errors:" | awk '{print $2}')
|
||||
total=$(echo "$output" | grep "Total manual tools:" | awk '{print $4}')
|
||||
|
||||
# Set outputs for Discord notification
|
||||
echo "processed=${processed:-0}" >> $GITHUB_OUTPUT
|
||||
echo "skipped=${skipped:-0}" >> $GITHUB_OUTPUT
|
||||
echo "errors=${errors:-0}" >> $GITHUB_OUTPUT
|
||||
echo "total=${total:-0}" >> $GITHUB_OUTPUT
|
||||
|
||||
# Determine status
|
||||
if [ "${errors:-0}" -gt 0 ]; then
|
||||
echo "status_emoji=⚠️" >> $GITHUB_OUTPUT
|
||||
echo "status_color=16776960" >> $GITHUB_OUTPUT # Yellow
|
||||
else
|
||||
echo "status_emoji=✅" >> $GITHUB_OUTPUT
|
||||
echo "status_color=5763719" >> $GITHUB_OUTPUT # Green
|
||||
fi
|
||||
env:
|
||||
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||
|
||||
- name: Send Discord notification
|
||||
if: always()
|
||||
run: |
|
||||
# Build Discord payload
|
||||
payload=$(jq -n \
|
||||
--arg title "${{ steps.sync.outputs.status_emoji }} Manual Tools Sync" \
|
||||
--argjson color ${{ steps.sync.outputs.status_color }} \
|
||||
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" \
|
||||
'
|
||||
{
|
||||
embeds: [{
|
||||
title: $title,
|
||||
color: $color,
|
||||
fields: [
|
||||
{ name: "📦 Total Tools", value: "${{ steps.sync.outputs.total }}", inline: true },
|
||||
{ name: "✨ Processed", value: "${{ steps.sync.outputs.processed }}", inline: true },
|
||||
{ name: "⏭️ Skipped", value: "${{ steps.sync.outputs.skipped }}", inline: true },
|
||||
{ name: "❌ Errors", value: "${{ steps.sync.outputs.errors }}", inline: true },
|
||||
{ name: "🔗 Run", value: "[View Logs](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})", inline: true }
|
||||
],
|
||||
timestamp: $timestamp
|
||||
}]
|
||||
}')
|
||||
|
||||
# Send to Discord
|
||||
curl -X POST "${{ secrets.DISCORD_WEBHOOK }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload"
|
||||
18
.github/workflows/sync-metrics.yml
vendored
Normal file
18
.github/workflows/sync-metrics.yml
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
name: Sync NPM Metrics
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run every hour
|
||||
- cron: '0 * * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
sync-metrics:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger metrics sync
|
||||
run: |
|
||||
curl -X POST "${{ secrets.VERCEL_PRODUCTION_URL }}/api/sync/metrics" \
|
||||
-H "Authorization: Bearer ${{ secrets.CRON_SECRET }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-f -s -S -w "\nHTTP Status: %{http_code}\n"
|
||||
95
.github/workflows/sync-package.yml
vendored
Normal file
95
.github/workflows/sync-package.yml
vendored
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
name: Sync Single Package
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
packageName:
|
||||
description: 'NPM package name to sync (e.g., fbx2vrma-converter)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
sync-package:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Sync package
|
||||
id: sync
|
||||
run: |
|
||||
echo "Syncing package: ${{ inputs.packageName }}"
|
||||
|
||||
# Call the sync API and capture response
|
||||
response=$(curl -X POST "${{ secrets.VERCEL_PRODUCTION_URL }}/api/sync/package" \
|
||||
-H "Authorization: Bearer ${{ secrets.CRON_SECRET }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"packageName": "${{ inputs.packageName }}"}' \
|
||||
-s -S)
|
||||
|
||||
echo "Response: $response"
|
||||
|
||||
# Check if sync was successful
|
||||
success=$(echo "$response" | jq -r '.success')
|
||||
|
||||
if [ "$success" = "true" ]; then
|
||||
echo "status_emoji=✅" >> $GITHUB_OUTPUT
|
||||
echo "status_color=5763719" >> $GITHUB_OUTPUT
|
||||
echo "status_text=Success" >> $GITHUB_OUTPUT
|
||||
|
||||
# Extract data
|
||||
packageId=$(echo "$response" | jq -r '.data.packageId')
|
||||
version=$(echo "$response" | jq -r '.data.version')
|
||||
toolCount=$(echo "$response" | jq -r '.data.toolCount')
|
||||
tools=$(echo "$response" | jq -r '.data.tools | join(", ")')
|
||||
author=$(echo "$response" | jq -r '.data.author')
|
||||
|
||||
echo "packageId=$packageId" >> $GITHUB_OUTPUT
|
||||
echo "version=$version" >> $GITHUB_OUTPUT
|
||||
echo "toolCount=$toolCount" >> $GITHUB_OUTPUT
|
||||
echo "tools=$tools" >> $GITHUB_OUTPUT
|
||||
echo "author=$author" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "status_emoji=❌" >> $GITHUB_OUTPUT
|
||||
echo "status_color=15158332" >> $GITHUB_OUTPUT
|
||||
echo "status_text=Failed" >> $GITHUB_OUTPUT
|
||||
|
||||
error=$(echo "$response" | jq -r '.error // "Unknown error"')
|
||||
echo "error=$error" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Send Discord notification
|
||||
if: always()
|
||||
run: |
|
||||
if [ "${{ steps.sync.outputs.status_text }}" = "Success" ]; then
|
||||
fields='[
|
||||
{ "name": "📦 Package", "value": "${{ inputs.packageName }}", "inline": true },
|
||||
{ "name": "🏷️ Version", "value": "${{ steps.sync.outputs.version }}", "inline": true },
|
||||
{ "name": "👤 Author", "value": "${{ steps.sync.outputs.author }}", "inline": true },
|
||||
{ "name": "🔧 Tools", "value": "${{ steps.sync.outputs.toolCount }}", "inline": true },
|
||||
{ "name": "📋 Tool Names", "value": "${{ steps.sync.outputs.tools }}", "inline": false },
|
||||
{ "name": "🔗 Run", "value": "[View Logs](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})", "inline": true }
|
||||
]'
|
||||
else
|
||||
fields='[
|
||||
{ "name": "📦 Package", "value": "${{ inputs.packageName }}", "inline": true },
|
||||
{ "name": "❌ Error", "value": "${{ steps.sync.outputs.error }}", "inline": false },
|
||||
{ "name": "🔗 Run", "value": "[View Logs](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})", "inline": true }
|
||||
]'
|
||||
fi
|
||||
|
||||
payload=$(jq -n \
|
||||
--arg title "${{ steps.sync.outputs.status_emoji }} Package Sync: ${{ inputs.packageName }}" \
|
||||
--argjson color ${{ steps.sync.outputs.status_color }} \
|
||||
--argjson fields "$fields" \
|
||||
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" \
|
||||
'
|
||||
{
|
||||
embeds: [{
|
||||
title: $title,
|
||||
color: $color,
|
||||
fields: $fields,
|
||||
timestamp: $timestamp
|
||||
}]
|
||||
}')
|
||||
|
||||
curl -X POST "${{ secrets.DISCORD_WEBHOOK }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload"
|
||||
267
.github/workflows/sync-vercel-registry.yml
vendored
Normal file
267
.github/workflows/sync-vercel-registry.yml
vendored
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
name: Sync Vercel AI Registry
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run every hour
|
||||
- cron: '0 * * * *'
|
||||
workflow_dispatch:
|
||||
# Run on pushes to main that modify the sync script
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'sync-vercel-registry.ts'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
sync-vercel:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.14.0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
echo "📦 Installing dependencies..."
|
||||
pnpm install --frozen-lockfile
|
||||
echo "✅ Dependencies installed"
|
||||
|
||||
- name: Run Vercel registry sync
|
||||
id: sync
|
||||
run: |
|
||||
echo "════════════════════════════════════════"
|
||||
echo "🚀 Starting Vercel AI Registry Sync"
|
||||
echo "════════════════════════════════════════"
|
||||
echo ""
|
||||
echo "📅 Time: $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
|
||||
echo "🔑 OpenAI API Key: ${OPENAI_API_KEY:0:8}..."
|
||||
echo ""
|
||||
|
||||
# Run the sync script and capture output
|
||||
output=$(pnpm tsx sync-vercel-registry.ts 2>&1)
|
||||
exit_code=$?
|
||||
|
||||
echo "$output"
|
||||
echo ""
|
||||
|
||||
# Extract statistics from output
|
||||
processed=$(echo "$output" | grep "Processed:" | tail -1 | awk '{print $2}')
|
||||
skipped=$(echo "$output" | grep "Skipped:" | tail -1 | awk '{print $2}')
|
||||
errors=$(echo "$output" | grep "Errors:" | tail -1 | awk '{print $2}')
|
||||
total=$(echo "$output" | grep "Total:" | tail -1 | awk '{print $2}')
|
||||
|
||||
# Set default values if extraction failed
|
||||
processed=${processed:-0}
|
||||
skipped=${skipped:-0}
|
||||
errors=${errors:-0}
|
||||
total=${total:-0}
|
||||
|
||||
echo "════════════════════════════════════════"
|
||||
echo "📊 Sync Statistics"
|
||||
echo "════════════════════════════════════════"
|
||||
echo "✨ Processed: $processed"
|
||||
echo "⏭️ Skipped: $skipped"
|
||||
echo "❌ Errors: $errors"
|
||||
echo "📦 Total: $total"
|
||||
echo "════════════════════════════════════════"
|
||||
echo ""
|
||||
|
||||
# Set outputs for later steps
|
||||
echo "processed=$processed" >> $GITHUB_OUTPUT
|
||||
echo "skipped=$skipped" >> $GITHUB_OUTPUT
|
||||
echo "errors=$errors" >> $GITHUB_OUTPUT
|
||||
echo "total=$total" >> $GITHUB_OUTPUT
|
||||
echo "exit_code=$exit_code" >> $GITHUB_OUTPUT
|
||||
|
||||
# Check if manual-tools.ts was modified
|
||||
if git diff --quiet manual-tools.ts; then
|
||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||
echo "ℹ️ No changes to manual-tools.ts"
|
||||
else
|
||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||
echo "✅ manual-tools.ts was modified"
|
||||
echo ""
|
||||
echo "📝 Changes preview:"
|
||||
git diff --stat manual-tools.ts
|
||||
echo ""
|
||||
git diff manual-tools.ts | head -50
|
||||
fi
|
||||
|
||||
# Determine status for notifications
|
||||
if [ "$exit_code" -ne 0 ]; then
|
||||
echo "status_emoji=❌" >> $GITHUB_OUTPUT
|
||||
echo "status_color=15158332" >> $GITHUB_OUTPUT # Red
|
||||
echo "status_text=Failed" >> $GITHUB_OUTPUT
|
||||
elif [ "$errors" -gt 0 ]; then
|
||||
echo "status_emoji=⚠️" >> $GITHUB_OUTPUT
|
||||
echo "status_color=16776960" >> $GITHUB_OUTPUT # Yellow
|
||||
echo "status_text=Completed with errors" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "status_emoji=✅" >> $GITHUB_OUTPUT
|
||||
echo "status_color=5763719" >> $GITHUB_OUTPUT # Green
|
||||
echo "status_text=Success" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# Exit with the original exit code
|
||||
exit $exit_code
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
|
||||
- name: Commit and push changes
|
||||
if: steps.sync.outputs.has_changes == 'true'
|
||||
run: |
|
||||
echo "════════════════════════════════════════"
|
||||
echo "📝 Committing changes to manual-tools.ts"
|
||||
echo "════════════════════════════════════════"
|
||||
echo ""
|
||||
|
||||
# Configure git
|
||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
|
||||
# Show what's being committed
|
||||
echo "📋 Files to commit:"
|
||||
git status --short
|
||||
echo ""
|
||||
|
||||
# Commit changes
|
||||
git add manual-tools.ts
|
||||
|
||||
# Create commit message
|
||||
COMMIT_MSG="chore: sync ${{ steps.sync.outputs.processed }} new tools from Vercel AI registry
|
||||
|
||||
Added ${{ steps.sync.outputs.processed }} tools from Vercel AI SDK registry:
|
||||
- Total tools in registry: ${{ steps.sync.outputs.total }}
|
||||
- Already synced: ${{ steps.sync.outputs.skipped }}
|
||||
- Newly added: ${{ steps.sync.outputs.processed }}
|
||||
- Errors: ${{ steps.sync.outputs.errors }}
|
||||
|
||||
🤖 Automated by GitHub Actions
|
||||
Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
|
||||
git commit -m "$COMMIT_MSG"
|
||||
|
||||
echo "✅ Changes committed"
|
||||
echo ""
|
||||
|
||||
# Push changes
|
||||
echo "📤 Pushing to remote..."
|
||||
git push
|
||||
|
||||
echo "✅ Changes pushed successfully"
|
||||
echo "════════════════════════════════════════"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Send Discord notification
|
||||
if: always()
|
||||
run: |
|
||||
echo "════════════════════════════════════════"
|
||||
echo "📢 Sending Discord notification"
|
||||
echo "════════════════════════════════════════"
|
||||
|
||||
# Build fields array
|
||||
base_fields='[
|
||||
{ "name": "📦 Total Tools", "value": "${{ steps.sync.outputs.total }}", "inline": true },
|
||||
{ "name": "✨ Processed", "value": "${{ steps.sync.outputs.processed }}", "inline": true },
|
||||
{ "name": "⏭️ Skipped", "value": "${{ steps.sync.outputs.skipped }}", "inline": true },
|
||||
{ "name": "❌ Errors", "value": "${{ steps.sync.outputs.errors }}", "inline": true },
|
||||
{ "name": "📝 Changes", "value": "${{ steps.sync.outputs.has_changes }}", "inline": true },
|
||||
{ "name": "🔗 Run", "value": "[View Logs](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})", "inline": true }
|
||||
]'
|
||||
|
||||
# Add commit info if changes were made
|
||||
if [ "${{ steps.sync.outputs.has_changes }}" = "true" ]; then
|
||||
commit_sha=$(git rev-parse HEAD)
|
||||
commit_url="https://github.com/${{ github.repository }}/commit/${commit_sha}"
|
||||
additional_fields='[
|
||||
{ "name": "💾 Commit", "value": "['"${commit_sha:0:7}"']('"$commit_url"')", "inline": false }
|
||||
]'
|
||||
|
||||
# Merge fields
|
||||
all_fields=$(jq -n --argjson base "$base_fields" --argjson additional "$additional_fields" '$base + $additional')
|
||||
else
|
||||
all_fields="$base_fields"
|
||||
fi
|
||||
|
||||
# Create Discord embed
|
||||
payload=$(jq -n \
|
||||
--arg title "${{ steps.sync.outputs.status_emoji }} Vercel AI Registry Sync - ${{ steps.sync.outputs.status_text }}" \
|
||||
--argjson color ${{ steps.sync.outputs.status_color }} \
|
||||
--argjson fields "$all_fields" \
|
||||
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" \
|
||||
--arg description "Synced Vercel AI SDK tools registry with TPMJS manual tools" \
|
||||
'
|
||||
{
|
||||
embeds: [{
|
||||
title: $title,
|
||||
description: $description,
|
||||
color: $color,
|
||||
fields: $fields,
|
||||
timestamp: $timestamp,
|
||||
footer: {
|
||||
text: "Vercel AI Registry Sync"
|
||||
}
|
||||
}]
|
||||
}')
|
||||
|
||||
echo "📤 Sending payload to Discord..."
|
||||
|
||||
# Send to Discord
|
||||
response=$(curl -X POST "${{ secrets.DISCORD_WEBHOOK }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
-w "\nHTTP Status: %{http_code}\n" \
|
||||
-s)
|
||||
|
||||
echo "$response"
|
||||
|
||||
if echo "$response" | grep -q "HTTP Status: 2"; then
|
||||
echo "✅ Discord notification sent successfully"
|
||||
else
|
||||
echo "⚠️ Discord notification may have failed"
|
||||
fi
|
||||
|
||||
echo "════════════════════════════════════════"
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo ""
|
||||
echo "════════════════════════════════════════"
|
||||
echo "📊 Workflow Summary"
|
||||
echo "════════════════════════════════════════"
|
||||
echo ""
|
||||
echo "Status: ${{ steps.sync.outputs.status_text }}"
|
||||
echo "Tools Processed: ${{ steps.sync.outputs.processed }}"
|
||||
echo "Tools Skipped: ${{ steps.sync.outputs.skipped }}"
|
||||
echo "Errors: ${{ steps.sync.outputs.errors }}"
|
||||
echo "Total in Registry: ${{ steps.sync.outputs.total }}"
|
||||
echo "Changes Made: ${{ steps.sync.outputs.has_changes }}"
|
||||
echo ""
|
||||
|
||||
if [ "${{ steps.sync.outputs.has_changes }}" = "true" ]; then
|
||||
echo "✅ New tools added to manual-tools.ts and committed"
|
||||
else
|
||||
echo "ℹ️ No new tools found - manual-tools.ts is up to date"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "════════════════════════════════════════"
|
||||
89
.github/workflows/tool-request.yml
vendored
Normal file
89
.github/workflows/tool-request.yml
vendored
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
name: Tool Request Pipeline
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
issue_number:
|
||||
description: 'Issue number to process'
|
||||
required: true
|
||||
type: number
|
||||
|
||||
jobs:
|
||||
trigger-claude:
|
||||
# Only run when 'tool-request' label is added or manual dispatch
|
||||
if: github.event.label.name == 'tool-request' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- name: Add working label
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const issueNumber = context.issue?.number || ${{ inputs.issue_number || 0 }};
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
labels: ['claude-working']
|
||||
});
|
||||
|
||||
- name: Comment to trigger Claude
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const issueNumber = context.issue?.number || ${{ inputs.issue_number || 0 }};
|
||||
const { data: issue } = await github.rest.issues.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber
|
||||
});
|
||||
|
||||
const lines = [
|
||||
'@claude Please implement this tool request.',
|
||||
'',
|
||||
'## Instructions',
|
||||
'1. Read the pipeline specification at `.claude/pipelines/tool-request.md`',
|
||||
'2. Follow all steps: analyze, design, implement, validate, test, publish',
|
||||
'3. Update labels as you progress (remove `claude-working`, add `published` or `validation-failed`)',
|
||||
'4. Post full changelog when complete',
|
||||
'5. This issue will auto-close 24h after successful publish',
|
||||
'',
|
||||
'## Issue Context',
|
||||
`- Issue #${issueNumber}`,
|
||||
`- Author: @${issue.user.login}`,
|
||||
`- Created: ${issue.created_at}`,
|
||||
'',
|
||||
'Begin implementation.'
|
||||
];
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
body: lines.join('\n')
|
||||
});
|
||||
|
||||
# Auto-close published issues after 24 hours
|
||||
auto-close:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.label.name == 'published'
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Schedule auto-close
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: 'This issue will auto-close in 24 hours. Reply if you have feedback or issues with the published tool.'
|
||||
});
|
||||
|
||||
# Separate workflow handles the actual auto-close via scheduled job
|
||||
# See: .github/workflows/auto-close-published.yml
|
||||
69
.github/workflows/update-docs.yml
vendored
Normal file
69
.github/workflows/update-docs.yml
vendored
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
name: Update Documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'packages/**'
|
||||
- 'apps/**'
|
||||
- 'templates/**'
|
||||
- '!**/*.md'
|
||||
|
||||
jobs:
|
||||
update-docs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Get changed files
|
||||
id: changed
|
||||
run: |
|
||||
echo "files=$(git diff --name-only HEAD~1 HEAD | tr '\n' ' ')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run Claude Code
|
||||
uses: anthropics/claude-code-action@beta
|
||||
continue-on-error: true
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
prompt: |
|
||||
Analyze the recent code changes and update any relevant documentation.
|
||||
|
||||
Changed files: ${{ steps.changed.outputs.files }}
|
||||
|
||||
Tasks:
|
||||
1. Read the changed files to understand what was modified
|
||||
2. Check if any README files, doc pages, or code comments need updating
|
||||
3. Update documentation to reflect the code changes
|
||||
4. Keep docs concise and accurate
|
||||
|
||||
Focus on:
|
||||
- API changes that affect usage examples
|
||||
- New features that need documentation
|
||||
- Changed behavior that affects existing docs
|
||||
- Executor template documentation (templates/vercel-executor/README.md)
|
||||
- Package READMEs in packages/
|
||||
|
||||
Only make changes if documentation is actually out of sync with code.
|
||||
If no documentation updates are needed, do nothing.
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v5
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: 'docs: auto-update documentation based on code changes'
|
||||
title: 'docs: Auto-update documentation'
|
||||
body: |
|
||||
This PR was automatically generated by Claude Code to update documentation based on recent code changes.
|
||||
|
||||
Please review the changes before merging.
|
||||
branch: auto-docs-update
|
||||
delete-branch: true
|
||||
52
.gitignore
vendored
52
.gitignore
vendored
|
|
@ -15,7 +15,12 @@ dist
|
|||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# video files
|
||||
*.mp4
|
||||
*.webm
|
||||
*.mov
|
||||
*.avi
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
|
|
@ -23,18 +28,57 @@ yarn-debug.log*
|
|||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
# environment files - NEVER commit secrets
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
.env.local
|
||||
.env.development
|
||||
.env.development.local
|
||||
.env.test
|
||||
.env.test.local
|
||||
.env.production
|
||||
.env.production.local
|
||||
.env.staging
|
||||
.env.vercel*
|
||||
|
||||
# secret files
|
||||
*.pem
|
||||
*.key
|
||||
*.p12
|
||||
*.pfx
|
||||
credentials.json
|
||||
secrets.json
|
||||
*_secret*
|
||||
*_credentials*
|
||||
|
||||
# turbo
|
||||
.turbo
|
||||
|
||||
# ai sdk devtools
|
||||
.devtools
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
# ide
|
||||
.idea
|
||||
.agent/
|
||||
.agents/
|
||||
.continue/
|
||||
.cursor/
|
||||
.windsurf/
|
||||
|
||||
# temporary analysis docs
|
||||
COMPREHENSIVE_ANALYSIS.md
|
||||
PLAN.md
|
||||
REGISTRY_TOOLS_ANALYSIS.md
|
||||
USER_ACCOUNT_ANALYSIS_*.md
|
||||
*.skill
|
||||
|
||||
# symlinked skill dirs (source is .agents/ which is gitignored)
|
||||
/skills/
|
||||
.claude/skills/skill-creator
|
||||
|
||||
# storybook
|
||||
storybook-static
|
||||
|
|
@ -43,3 +87,5 @@ storybook-static
|
|||
.changeset/*.md
|
||||
!.changeset/README.md
|
||||
.vercel
|
||||
packages/tool-ideas/data/tools-export.json
|
||||
.env*.local
|
||||
|
|
|
|||
49
.gitsecrets
Normal file
49
.gitsecrets
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# Secret patterns for git-secrets
|
||||
# Run `git secrets --add-provider -- cat .gitsecrets` to load these patterns
|
||||
# Or manually add with `git secrets --add '<pattern>'`
|
||||
|
||||
# =============================================================================
|
||||
# TPMJS-specific patterns
|
||||
# =============================================================================
|
||||
|
||||
# TPMJS API keys (format: tpmjs_sk_<base64>)
|
||||
tpmjs_sk_[A-Za-z0-9_-]+
|
||||
|
||||
# =============================================================================
|
||||
# Database credentials
|
||||
# =============================================================================
|
||||
|
||||
# Neon database passwords (format: npg_<alphanumeric>)
|
||||
npg_[A-Za-z0-9]+
|
||||
|
||||
# PostgreSQL connection strings with embedded passwords
|
||||
postgresql://[^:]+:[^@]+@.*neon
|
||||
|
||||
# Generic database URLs with passwords
|
||||
DATABASE_URL=.*://[^:]+:[^@]+@
|
||||
|
||||
# =============================================================================
|
||||
# Generic secret patterns
|
||||
# =============================================================================
|
||||
|
||||
# Long hex strings (API keys, tokens) - 64 chars like CRON_SECRET
|
||||
[a-f0-9]{64}
|
||||
|
||||
# JWT tokens (common format)
|
||||
eyJ[A-Za-z0-9_-]*\.eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*
|
||||
|
||||
# Generic API key patterns
|
||||
[Aa][Pp][Ii][-_]?[Kk][Ee][Yy].*['"][A-Za-z0-9_-]{20,}['"]
|
||||
|
||||
# =============================================================================
|
||||
# Cloud provider patterns (via --register-aws)
|
||||
# =============================================================================
|
||||
# AWS patterns are automatically registered with `git secrets --register-aws`
|
||||
# - AWS Access Key IDs: AKIA[0-9A-Z]{16}
|
||||
# - AWS Secret Access Keys
|
||||
|
||||
# =============================================================================
|
||||
# Allowed patterns (false positive exclusions)
|
||||
# =============================================================================
|
||||
# Add allowed patterns with: git secrets --add --allowed '<pattern>'
|
||||
# Example: git secrets --add --allowed 'example\.com'
|
||||
31
.ignore
Normal file
31
.ignore
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# Ignore patterns for OpenCode
|
||||
# These directories are excluded from search to reduce noise and improve relevance
|
||||
|
||||
# Build outputs and caches
|
||||
**/dist/**
|
||||
**/.next/**
|
||||
**/.turbo/**
|
||||
**/coverage/**
|
||||
**/.cache/**
|
||||
**/node_modules/**
|
||||
|
||||
# Generated files
|
||||
**/.DS_Store/**
|
||||
**/*.log
|
||||
**/tmp/**
|
||||
|
||||
# Lock files (unless explicitly requested)
|
||||
**/pnpm-lock.yaml
|
||||
**/package-lock.json
|
||||
**/yarn.lock
|
||||
|
||||
# Environment files
|
||||
**/.env*
|
||||
**/.envrc
|
||||
|
||||
# IDE files
|
||||
**/.vscode/**
|
||||
**/.idea/**
|
||||
|
||||
# OS files
|
||||
**/Thumbs.db
|
||||
7
.vercelignore
Normal file
7
.vercelignore
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
node_modules
|
||||
.turbo
|
||||
.next
|
||||
dist
|
||||
*.log
|
||||
.env*
|
||||
!.env.example
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
|
@ -2,8 +2,7 @@
|
|||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "biomejs.biome",
|
||||
"editor.codeActionsOnSave": {
|
||||
"quickfix.biome": "explicit",
|
||||
"source.organizeImports.biome": "explicit"
|
||||
"source.fixAll.biome": "explicit"
|
||||
},
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true
|
||||
|
|
|
|||
|
|
@ -1,275 +0,0 @@
|
|||
# 2025 Best Practices for TPMJS Monorepo
|
||||
|
||||
This document outlines recommendations to make TPMJS a cutting-edge 2025 monorepo optimized for both human and agentic development (Claude Code, Cursor, etc.).
|
||||
|
||||
## High-Impact Additions
|
||||
|
||||
### 1. Agent-First Documentation
|
||||
|
||||
```
|
||||
packages/docs/
|
||||
├── architecture-decisions/ # ADRs in markdown
|
||||
├── patterns/ # Common patterns with examples
|
||||
├── schemas/ # JSON schemas for all data structures
|
||||
└── examples/ # Working code examples per feature
|
||||
```
|
||||
|
||||
**Why:** Claude Code and other agents work better with:
|
||||
- Explicit decision documentation (ADRs)
|
||||
- Pattern libraries showing "the right way"
|
||||
- Machine-readable schemas
|
||||
- Real working examples to reference
|
||||
|
||||
### 2. Automated Testing Pyramid
|
||||
|
||||
```bash
|
||||
# Add to package.json scripts
|
||||
"test:unit": "vitest" # ✅ Already have this
|
||||
"test:integration": "vitest -c vitest.integration.config.ts" # Add
|
||||
"test:e2e": "playwright test" # Add
|
||||
"test:visual": "playwright test --grep @visual" # Add
|
||||
"test:contracts": "pactum" # Add for API testing
|
||||
```
|
||||
|
||||
**Packages to add:**
|
||||
- `@playwright/test` - E2E testing
|
||||
- `@playwright/experimental-ct-react` - Component testing
|
||||
- `pactum` or `msw` integration tests (you have mocks setup)
|
||||
- `chromatic` or `percy` - Visual regression
|
||||
|
||||
### 3. Type Coverage & Quality Gates
|
||||
|
||||
```json
|
||||
// Add to root package.json
|
||||
{
|
||||
"scripts": {
|
||||
"type-check": "tsc --noEmit",
|
||||
"type-coverage": "type-coverage --at-least 95",
|
||||
"find-deadcode": "knip",
|
||||
"check-architecture": "depcruiser --validate"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Add packages:**
|
||||
- `type-coverage` - Ensure no implicit `any`
|
||||
- `knip` - Find unused files/exports/dependencies
|
||||
- `dependency-cruiser` - Enforce architecture rules
|
||||
- `@total-typescript/ts-reset` - Better built-in types
|
||||
|
||||
### 4. Development Containers
|
||||
|
||||
```json
|
||||
// .devcontainer/devcontainer.json
|
||||
{
|
||||
"name": "TPMJS Dev",
|
||||
"dockerComposeFile": "docker-compose.yml",
|
||||
"service": "dev",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {},
|
||||
"ghcr.io/devcontainers-contrib/features/pnpm:2": {}
|
||||
},
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"biomejs.biome",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"lokalise.i18n-ally"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why:** Agents like Claude Code work better when environment is reproducible. This also helps human developers.
|
||||
|
||||
### 5. Code Generation & Scaffolding
|
||||
|
||||
```typescript
|
||||
// packages/cli/ - Internal dev tool
|
||||
import { scaffold } from '@tpmjs/cli';
|
||||
|
||||
// Commands:
|
||||
pnpm gen:component ButtonGroup
|
||||
pnpm gen:package @tpmjs/new-package
|
||||
pnpm gen:app marketing-site
|
||||
```
|
||||
|
||||
**Create:**
|
||||
- `plop` or `hygen` templates
|
||||
- Component scaffolding (with tests, stories, exports)
|
||||
- Package scaffolding (with tsconfig, package.json, exports)
|
||||
- Consistent file structure generation
|
||||
|
||||
**Why:** Agents can use these commands to create new code following your exact patterns.
|
||||
|
||||
### 6. Enhanced Strict Mode TypeScript
|
||||
|
||||
```json
|
||||
// packages/tsconfig/base.json - Add these
|
||||
{
|
||||
"compilerOptions": {
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"allowUnusedLabels": false,
|
||||
"allowUnreachableCode": false,
|
||||
"noImplicitOverride": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Bundle Analysis & Performance
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"analyze": "turbo run build --filter=@tpmjs/web -- --analyze",
|
||||
"lighthouse": "lhci autorun",
|
||||
"bundle-size": "size-limit"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Add:**
|
||||
- `@next/bundle-analyzer`
|
||||
- `@lhci/cli` - Lighthouse CI
|
||||
- `size-limit` - Bundle size tracking in CI
|
||||
|
||||
### 8. Smart Dependency Management
|
||||
|
||||
```json
|
||||
// .github/renovate.json
|
||||
{
|
||||
"extends": ["config:base"],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchPackagePatterns": ["*"],
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"groupName": "all non-major dependencies",
|
||||
"groupSlug": "all-minor-patch"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Use:** Renovate or Dependabot with auto-merge for passing tests
|
||||
|
||||
### 9. API Documentation Generation
|
||||
|
||||
```bash
|
||||
pnpm add -D -w typedoc typedoc-plugin-markdown
|
||||
```
|
||||
|
||||
Auto-generate API docs from TSDoc comments that both humans and agents can read.
|
||||
|
||||
### 10. Schema-First Development
|
||||
|
||||
```typescript
|
||||
// packages/schemas/ - Central schema definitions
|
||||
export * from './tool-schema';
|
||||
export * from './registry-api-schema';
|
||||
export * from './event-schema';
|
||||
|
||||
// Use Zod for runtime + type generation
|
||||
// Agents can read schemas to understand contracts
|
||||
```
|
||||
|
||||
## Monorepo-Specific Improvements
|
||||
|
||||
### 11. Better Local Development
|
||||
|
||||
```typescript
|
||||
// turbo.json
|
||||
{
|
||||
"pipeline": {
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true,
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["dist/**", ".next/**"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 12. Workspace Protocols & Constraints
|
||||
|
||||
```yaml
|
||||
# .pnpm-workspace.yaml
|
||||
packages:
|
||||
- 'apps/*'
|
||||
- 'packages/*'
|
||||
|
||||
# Add constraints
|
||||
pnpm-workspace-constraints:
|
||||
dependencies:
|
||||
'@tpmjs/ui': 'workspace:*'
|
||||
'@tpmjs/utils': 'workspace:*'
|
||||
```
|
||||
|
||||
## Recommended Final Structure
|
||||
|
||||
```
|
||||
.
|
||||
├── .devcontainer/ # Dev containers config
|
||||
├── .github/
|
||||
│ ├── workflows/ # CI/CD
|
||||
│ └── renovate.json # Dependency automation
|
||||
├── apps/
|
||||
│ └── web/
|
||||
├── packages/
|
||||
│ ├── cli/ # ⭐ NEW: Dev tooling
|
||||
│ ├── schemas/ # ⭐ NEW: Central schemas
|
||||
│ └── ...existing
|
||||
├── docs/
|
||||
│ ├── adr/ # ⭐ NEW: Architecture decisions
|
||||
│ ├── patterns/ # ⭐ NEW: Code patterns
|
||||
│ └── examples/ # ⭐ NEW: Working examples
|
||||
├── scripts/
|
||||
│ ├── scaffold.ts # ⭐ NEW: Code generation
|
||||
│ └── validate-deps.ts # ⭐ NEW: Architecture validation
|
||||
├── playwright.config.ts # ⭐ NEW: E2E testing
|
||||
├── .lighthouserc.json # ⭐ NEW: Performance
|
||||
└── knip.json # ⭐ NEW: Dead code detection
|
||||
```
|
||||
|
||||
## Priority Order for Implementation
|
||||
|
||||
### Phase 1 (Foundation)
|
||||
1. **Knip + type-coverage** - Catch issues early
|
||||
2. **Code generation scripts** - Ensure consistency
|
||||
3. **ADR documentation structure** - Decision tracking
|
||||
|
||||
### Phase 2 (Quality)
|
||||
4. **E2E testing with Playwright** - Full user flow coverage
|
||||
5. **Bundle analysis + performance budgets** - Keep app fast
|
||||
6. **Stricter TypeScript settings** - Catch more bugs at compile time
|
||||
|
||||
### Phase 3 (DX)
|
||||
7. **Dev containers** - Reproducible environments
|
||||
8. **API documentation generation** - Auto-generated from code
|
||||
9. **Renovate automation** - Keep dependencies fresh
|
||||
|
||||
## Benefits for Agent-Driven Development
|
||||
|
||||
1. **Explicit Patterns** - Agents can reference documented patterns instead of guessing
|
||||
2. **Code Generation** - Consistent scaffolding commands agents can use
|
||||
3. **Machine-Readable Schemas** - JSON schemas help agents understand data structures
|
||||
4. **Quality Gates** - Automated checks catch agent mistakes early
|
||||
5. **Working Examples** - Agents can copy-paste-adapt proven patterns
|
||||
6. **Architecture Enforcement** - Dependency rules prevent agents from creating invalid imports
|
||||
|
||||
## Next Steps
|
||||
|
||||
Start with the highest ROI items:
|
||||
1. Install Knip to find dead code
|
||||
2. Set up code generation for components/packages
|
||||
3. Create docs/patterns/ with common examples
|
||||
4. Add stricter TypeScript compiler options
|
||||
5. Set up Playwright for E2E testing
|
||||
|
||||
These changes will make the codebase more maintainable and significantly improve the experience of working with AI coding agents.
|
||||
210
AGENTS.md
Normal file
210
AGENTS.md
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
# TPMJS OpenCode Configuration
|
||||
|
||||
This file contains project-specific rules and guidance for OpenCode agents working in the TPMJS monorepo.
|
||||
|
||||
## Repository Overview
|
||||
|
||||
TPMJS is a Turborepo monorepo for AI tool discovery and registry. Key characteristics:
|
||||
- **Package Manager**: pnpm with workspace configuration
|
||||
- **Build System**: Turborepo for task orchestration
|
||||
- **Main App**: Next.js 16 App Router (`apps/web`)
|
||||
- **Component Library**: `.ts`-only React components (`packages/ui`)
|
||||
- **Database**: Prisma with PostgreSQL (`packages/db`)
|
||||
- **Tool Registry**: npm package discovery and metadata sync
|
||||
|
||||
## Core Commands (Always Use These)
|
||||
|
||||
```bash
|
||||
# Development
|
||||
pnpm dev # Start all dev servers
|
||||
pnpm --filter=@tpmjs/web dev # Start web app only
|
||||
|
||||
# Building (Respects Dependencies)
|
||||
pnpm build # Build all packages
|
||||
pnpm --filter=@tpmjs/ui build # Build specific package
|
||||
pnpm --filter=@tpmjs/web... build # Build web + all dependencies
|
||||
|
||||
# Testing & Quality
|
||||
pnpm test # Run all tests
|
||||
pnpm lint # Lint all packages
|
||||
pnpm format # Format with Biome
|
||||
pnpm type-check # TypeScript checking
|
||||
```
|
||||
|
||||
## Architecture Rules (Critical)
|
||||
|
||||
### Module Boundaries
|
||||
- **Apps** (`apps/*`) can only import from published packages (`@tpmjs/*`)
|
||||
- **Packages** (`packages/*`) cannot import from apps
|
||||
- **UI Package** (`packages/ui`) cannot import from utils (stays dependency-free)
|
||||
- **No barrel exports** - always import directly: `@tpmjs/ui/Button/Button`
|
||||
|
||||
### Component Usage
|
||||
**ALWAYS use `@tpmjs/ui` components instead of raw HTML:**
|
||||
```typescript
|
||||
// Good
|
||||
import { Button } from '@tpmjs/ui/Button/Button';
|
||||
import { Input } from '@tpmjs/ui/Input/Input';
|
||||
|
||||
// Bad
|
||||
<button onClick={handleClick}>Submit</button>
|
||||
<input value={value} onChange={onChange} />
|
||||
```
|
||||
|
||||
### TypeScript Configuration
|
||||
- All packages extend from `@tpmjs/tsconfig`
|
||||
- Strict mode enabled
|
||||
- Composite projects for proper dependency resolution
|
||||
|
||||
## Package Structure
|
||||
|
||||
### Published Packages (@tpmjs scope)
|
||||
- `@tpmjs/ui` - React component library (.ts-only, createElement)
|
||||
- `@tpmjs/utils` - Utility functions (cn, format, etc.)
|
||||
- `@tpmjs/types` - Shared TypeScript types and Zod schemas
|
||||
- `@tpmjs/env` - Environment variable validation with Zod
|
||||
|
||||
### Internal Tooling (Private)
|
||||
- `@tpmjs/config` - Shared configurations (Biome, ESLint, Tailwind, TypeScript)
|
||||
- `@tpmjs/test` - Vitest shared configuration
|
||||
- `@tpmjs/mocks` - MSW mock server for testing
|
||||
- `@tpmjs/storybook` - Component documentation
|
||||
|
||||
### Applications
|
||||
- `@tpmjs/web` - Next.js 16 App Router (main website)
|
||||
- `@tpmjs/playground` - Tool testing playground
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Before Making Changes
|
||||
1. Run `pnpm type-check` to ensure clean state
|
||||
2. Check existing patterns in similar files
|
||||
3. Use `@tpmjs/ui` components for any UI changes
|
||||
|
||||
### After Making Changes
|
||||
1. `pnpm lint` - Check linting
|
||||
2. `pnpm type-check` - Verify TypeScript
|
||||
3. `pnpm test` - Run tests if applicable
|
||||
4. `pnpm format` - Auto-format with Biome
|
||||
|
||||
### Database Changes
|
||||
If modifying Prisma schema:
|
||||
```bash
|
||||
pnpm --filter=@tpmjs/db db:generate # Regenerate client
|
||||
pnpm --filter=@tpmjs/db db:push # Apply changes (dev)
|
||||
```
|
||||
|
||||
## Tool Development
|
||||
|
||||
### Tool Package Structure
|
||||
Tools live in `packages/tools/*` with this pattern:
|
||||
```
|
||||
packages/tools/tool-name/
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
├── src/
|
||||
│ ├── index.ts # Main export
|
||||
│ ├── tool.ts # Tool definition
|
||||
│ └── implementation.ts # Actual logic
|
||||
├── README.md
|
||||
└── examples/
|
||||
└── basic.ts
|
||||
```
|
||||
|
||||
### Tool Metadata
|
||||
Tools must have proper `tpmjs` field in package.json:
|
||||
```json
|
||||
{
|
||||
"tpmjs": {
|
||||
"category": "text-analysis",
|
||||
"tier": "rich",
|
||||
"description": "Tool description"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Quality Standards
|
||||
|
||||
### Code Quality
|
||||
- No `any` types or `@ts-ignore`
|
||||
- Strict TypeScript compliance
|
||||
- Proper error handling with try/catch
|
||||
- Meaningful variable names
|
||||
|
||||
### Testing
|
||||
- Unit tests for utilities
|
||||
- Integration tests for API routes
|
||||
- Component tests for UI changes
|
||||
- Use Vitest + Testing Library
|
||||
|
||||
### Documentation
|
||||
- README for all packages
|
||||
- JSDoc for public APIs
|
||||
- Examples for tool usage
|
||||
- Type definitions for all public interfaces
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### API Routes
|
||||
```typescript
|
||||
import { NextResponse } from 'next/server';
|
||||
import { prisma } from '@tpmjs/db';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
export const dynamic = 'force-dynamic';
|
||||
export const maxDuration = 60;
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
// Implementation
|
||||
return NextResponse.json({ success: true, data });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Component Pattern
|
||||
```typescript
|
||||
import { createElement } from 'react';
|
||||
import { cn } from '@tpmjs/utils';
|
||||
|
||||
interface ButtonProps {
|
||||
onClick?: () => void;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Button({ onClick, children, className }: ButtonProps) {
|
||||
return createElement('button', {
|
||||
onClick,
|
||||
className: cn('default-styles', className),
|
||||
}, children);
|
||||
}
|
||||
```
|
||||
|
||||
## What NOT to Do
|
||||
|
||||
- **Never edit lockfiles** unless explicitly requested
|
||||
- **Never use barrel exports** (`index.ts` files)
|
||||
- **Never suppress TypeScript errors** with `as any` or `@ts-ignore`
|
||||
- **Never use raw HTML elements** when `@tpmjs/ui` components exist
|
||||
- **Never import from apps** in packages
|
||||
- **Never commit without running** `pnpm lint` and `pnpm type-check`
|
||||
|
||||
## Deployment & CI
|
||||
|
||||
- Vercel deployment requires all CI checks to pass
|
||||
- Pre-commit hooks run `format`, `lint`, and `type-check`
|
||||
- Use `vercel inspect` to debug deployments
|
||||
- Check `/api/health` to verify production deployments
|
||||
|
||||
## Getting Help
|
||||
|
||||
- Check existing implementations in similar packages
|
||||
- Use `pnpm --filter=<package> dev` for package-specific development
|
||||
- Refer to `CLAUDE.md` for detailed architectural decisions
|
||||
- Look at `packages/tools/*` for tool development examples
|
||||
|
|
@ -1,932 +0,0 @@
|
|||
# API Routes Timeout Issue - Complete Investigation Report
|
||||
|
||||
## Problem Statement
|
||||
|
||||
API routes deployed to Vercel are timing out with no response. The Next.js application pages work perfectly, but all API endpoints at `/api/*` return timeouts or "Redirecting..." messages.
|
||||
|
||||
**Affected URLs:**
|
||||
- `https://tpmjs.com/api/health` - Returns "Redirecting..."
|
||||
- `https://tpmjs.com/api/tools` - Returns "Redirecting..."
|
||||
- `https://tpmjs-1chh44d1u-tpmjs.vercel.app/api/health` - Timeouts (exit code 28)
|
||||
- `https://tpmjs-1chh44d1u-tpmjs.vercel.app/api/tools` - Timeouts (exit code 28)
|
||||
|
||||
**Working:**
|
||||
- All page routes work correctly (e.g., `/`, `/tool/[slug]`)
|
||||
- UI navigation and client-side routing function normally
|
||||
- Local development API routes work perfectly
|
||||
|
||||
## Environment Details
|
||||
|
||||
### Project Structure
|
||||
- **Monorepo:** Turborepo setup with pnpm workspaces
|
||||
- **Framework:** Next.js 16.0.4 (App Router)
|
||||
- **Node Version:** 24.x (on Vercel)
|
||||
- **Deployment Platform:** Vercel
|
||||
- **Custom Domains:** tpmjs.com, www.tpmjs.com
|
||||
|
||||
### Repository Structure
|
||||
```
|
||||
tpmjs/
|
||||
├── apps/
|
||||
│ └── web/ # Next.js 16 App Router application
|
||||
│ ├── src/
|
||||
│ │ └── app/
|
||||
│ │ ├── api/
|
||||
│ │ │ ├── health/route.ts
|
||||
│ │ │ ├── stats/route.ts
|
||||
│ │ │ ├── tools/
|
||||
│ │ │ │ ├── route.ts
|
||||
│ │ │ │ ├── [id]/route.ts
|
||||
│ │ │ │ ├── [slug]/route.ts
|
||||
│ │ │ │ └── validate/route.ts
|
||||
│ │ │ └── sync/
|
||||
│ │ │ ├── changes/route.ts
|
||||
│ │ │ ├── keyword/route.ts
|
||||
│ │ │ └── metrics/route.ts
|
||||
│ │ ├── page.tsx
|
||||
│ │ └── tool/[slug]/page.tsx
|
||||
│ ├── next.config.ts
|
||||
│ └── vercel.json
|
||||
├── packages/
|
||||
│ ├── db/ # Prisma client
|
||||
│ ├── types/ # Shared TypeScript types
|
||||
│ ├── utils/ # Utility functions
|
||||
│ ├── env/ # Environment validation
|
||||
│ └── ui/ # React component library
|
||||
├── vercel.json # Root Vercel configuration
|
||||
└── turbo.json
|
||||
```
|
||||
|
||||
## API Route Examples
|
||||
|
||||
### `/apps/web/src/app/api/health/route.ts`
|
||||
```typescript
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
export const dynamic = 'force-dynamic';
|
||||
export const maxDuration = 60;
|
||||
|
||||
/**
|
||||
* GET /api/health
|
||||
* Simple health check endpoint that doesn't touch the database
|
||||
*/
|
||||
export async function GET() {
|
||||
return NextResponse.json({
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
env: {
|
||||
hasDatabase: !!process.env.DATABASE_URL,
|
||||
nodeEnv: process.env.NODE_ENV,
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### `/apps/web/src/app/api/tools/route.ts`
|
||||
```typescript
|
||||
import { NextResponse } from 'next/server';
|
||||
import { prisma } from '@tpmjs/db/client';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
// ... query string parsing
|
||||
|
||||
const [tools, totalCount] = await Promise.all([
|
||||
prisma.tool.findMany({
|
||||
where,
|
||||
orderBy: [
|
||||
{ qualityScore: 'desc' },
|
||||
{ npmDownloadsLastMonth: 'desc' },
|
||||
{ createdAt: 'desc' },
|
||||
],
|
||||
take: limit,
|
||||
skip: offset,
|
||||
}),
|
||||
prisma.tool.count({ where }),
|
||||
]);
|
||||
|
||||
return NextResponse.json({
|
||||
data: tools,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total: totalCount,
|
||||
totalPages: Math.ceil(totalCount / limit),
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Files
|
||||
|
||||
### `/apps/web/next.config.ts` (Current)
|
||||
```typescript
|
||||
import type { NextConfig } from 'next';
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
transpilePackages: ['@tpmjs/ui', '@tpmjs/utils', '@tpmjs/db', '@tpmjs/types', '@tpmjs/env'],
|
||||
reactStrictMode: true,
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
```
|
||||
|
||||
### `/apps/web/vercel.json` (Current)
|
||||
```json
|
||||
{
|
||||
"$schema": "https://openapi.vercel.sh/vercel.json",
|
||||
"buildCommand": "cd ../.. && pnpm --filter=@tpmjs/web build",
|
||||
"installCommand": "pnpm install"
|
||||
}
|
||||
```
|
||||
|
||||
### `/vercel.json` (Root)
|
||||
```json
|
||||
{
|
||||
"$schema": "https://openapi.vercel.sh/vercel.json",
|
||||
"git": {
|
||||
"deploymentEnabled": {
|
||||
"main": true
|
||||
}
|
||||
},
|
||||
"github": {
|
||||
"silent": false,
|
||||
"autoJobCancelation": true
|
||||
},
|
||||
"crons": [
|
||||
{
|
||||
"path": "/api/sync/changes",
|
||||
"schedule": "*/2 * * * *"
|
||||
},
|
||||
{
|
||||
"path": "/api/sync/keyword",
|
||||
"schedule": "*/15 * * * *"
|
||||
},
|
||||
{
|
||||
"path": "/api/sync/metrics",
|
||||
"schedule": "0 * * * *"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Local Build Verification
|
||||
|
||||
### Local Build Output Structure
|
||||
```bash
|
||||
$ ls -R /Users/ajaxdavis/repos/tpmjs/tpmjs/apps/web/.next/server/app/api/
|
||||
|
||||
health/
|
||||
stats/
|
||||
sync/
|
||||
tools/
|
||||
|
||||
/apps/web/.next/server/app/api/health:
|
||||
route
|
||||
route.js
|
||||
route.js.map
|
||||
route.js.nft.json
|
||||
route_client-reference-manifest.js
|
||||
|
||||
/apps/web/.next/server/app/api/stats:
|
||||
route
|
||||
route.js
|
||||
route.js.map
|
||||
route.js.nft.json
|
||||
route_client-reference-manifest.js
|
||||
|
||||
/apps/web/.next/server/app/api/tools:
|
||||
[id]/
|
||||
[slug]/
|
||||
validate/
|
||||
route
|
||||
route.js
|
||||
route.js.map
|
||||
route.js.nft.json
|
||||
route_client-reference-manifest.js
|
||||
```
|
||||
|
||||
### Routes Manifest Confirmation
|
||||
```bash
|
||||
$ cat /apps/web/.next/routes-manifest.json | jq '.staticRoutes[] | select(.page | contains("api"))'
|
||||
|
||||
{
|
||||
"page": "/api/health",
|
||||
"regex": "^/api/health(?:/)?$",
|
||||
"routeKeys": {},
|
||||
"namedRegex": "^/api/health(?:/)?$"
|
||||
}
|
||||
{
|
||||
"page": "/api/stats",
|
||||
"regex": "^/api/stats(?:/)?$",
|
||||
"routeKeys": {},
|
||||
"namedRegex": "^/api/stats(?:/)?$"
|
||||
}
|
||||
{
|
||||
"page": "/api/sync/changes",
|
||||
"regex": "^/api/sync/changes(?:/)?$",
|
||||
"routeKeys": {},
|
||||
"namedRegex": "^/api/sync/changes(?:/)?$"
|
||||
}
|
||||
{
|
||||
"page": "/api/sync/keyword",
|
||||
"regex": "^/api/sync/keyword(?:/)?$",
|
||||
"routeKeys": {},
|
||||
"namedRegex": "^/api/sync/keyword(?:/)?$"
|
||||
}
|
||||
{
|
||||
"page": "/api/sync/metrics",
|
||||
"regex": "^/api/sync/metrics(?:/)?$",
|
||||
"routeKeys": {},
|
||||
"namedRegex": "^/api/sync/metrics(?:/)?$"
|
||||
}
|
||||
```
|
||||
|
||||
### Node File Trace (NFT) Verification
|
||||
```bash
|
||||
$ cat /apps/web/.next/server/app/api/health/route.js.nft.json
|
||||
|
||||
{
|
||||
"version": 1,
|
||||
"files": [
|
||||
"../../../../../../../node_modules/.pnpm/next@16.0.4_@babel+core@7.28.5_react-dom@19.2.0_react@19.2.0__react@19.2.0/node_modules/next/dist/client/components/app-router-headers.js",
|
||||
"../../../../../../../node_modules/.pnpm/next@16.0.4_@babel+core@7.28.5_react-dom@19.2.0_react@19.2.0__react@19.2.0/node_modules/next/dist/compiled/@opentelemetry/api/index.js",
|
||||
// ... many more dependencies
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Conclusion:** API routes build correctly locally with all dependencies properly traced.
|
||||
|
||||
## Vercel Deployment Analysis
|
||||
|
||||
### Deployment Inspection Output
|
||||
```bash
|
||||
$ vercel inspect https://tpmjs-1chh44d1u-tpmjs.vercel.app
|
||||
|
||||
General
|
||||
id dpl_2FHyiTWtBZzvT8EcohwYEZmdb8rb
|
||||
name tpmjs-web
|
||||
target production
|
||||
status ● Ready
|
||||
url https://tpmjs-1chh44d1u-tpmjs.vercel.app
|
||||
created Fri Nov 28 2025 20:44:03 GMT+1000
|
||||
|
||||
Aliases
|
||||
╶ https://www.tpmjs.com
|
||||
╶ https://tpmjs-web.vercel.app
|
||||
╶ https://tpmjs-web-tpmjs.vercel.app
|
||||
╶ https://tpmjs-web-git-main-tpmjs.vercel.app
|
||||
╶ https://tpmjs.com
|
||||
|
||||
Builds
|
||||
┌ . [0ms]
|
||||
├── λ tool/[slug] (562.92KB) [iad1]
|
||||
├── λ tool/[slug].rsc (562.92KB) [iad1]
|
||||
├── λ _global-error (642.48KB) [iad1]
|
||||
├── λ _global-error.rsc (642.48KB) [iad1]
|
||||
├── λ _global-error.segments/__PAGE__.segment.rsc (642.48KB) [iad1]
|
||||
└── 56 output items hidden
|
||||
```
|
||||
|
||||
**CRITICAL FINDING:** No API routes are listed in the build output. Only pages (`tool/[slug]`, `_global-error`, etc.) appear as serverless functions (`λ`).
|
||||
|
||||
Expected API routes that should appear:
|
||||
- `λ api/health`
|
||||
- `λ api/tools`
|
||||
- `λ api/tools/[id]`
|
||||
- `λ api/tools/[slug]`
|
||||
- `λ api/sync/changes`
|
||||
- etc.
|
||||
|
||||
### Testing Results
|
||||
```bash
|
||||
# Direct Vercel URL - Timeouts
|
||||
$ curl -s -m 10 https://tpmjs-1chh44d1u-tpmjs.vercel.app/api/health
|
||||
# Exit code 28 (timeout)
|
||||
|
||||
# Custom Domain - Returns "Redirecting..."
|
||||
$ curl -s -m 10 https://tpmjs.com/api/health
|
||||
Redirecting...
|
||||
|
||||
# Custom Domain - Returns "Redirecting..."
|
||||
$ curl -s -m 10 https://tpmjs.com/api/tools
|
||||
Redirecting...
|
||||
|
||||
# Check redirect headers
|
||||
$ curl -I https://tpmjs.com
|
||||
HTTP/2 307
|
||||
cache-control: public, max-age=0, must-revalidate
|
||||
content-type: text/plain
|
||||
date: Fri, 28 Nov 2025 10:38:12 GMT
|
||||
location: https://www.tpmjs.com/
|
||||
server: Vercel
|
||||
```
|
||||
|
||||
## Vercel Configuration Details
|
||||
|
||||
### User-Confirmed Settings
|
||||
- **Root Directory:** `apps/web` (set in Vercel dashboard)
|
||||
- **DATABASE_URL:** Configured in Vercel environment variables (Production)
|
||||
- **Framework Preset:** (Unknown - needs verification)
|
||||
- **Build Output Directory:** (Unknown - using default `.next`)
|
||||
|
||||
### Project List
|
||||
```bash
|
||||
$ vercel project ls | grep -i tpmjs
|
||||
|
||||
tpmjs -- 16h 24.x
|
||||
v0-tool-registry-page https://tpmjs.com 3d 22.x
|
||||
```
|
||||
|
||||
**NOTE:** Two projects exist:
|
||||
1. `tpmjs` - Current project (Node 24.x)
|
||||
2. `v0-tool-registry-page` - Also has tpmjs.com domain (Node 22.x)
|
||||
|
||||
This could indicate a domain routing conflict or outdated project.
|
||||
|
||||
## Investigation Timeline & Attempts
|
||||
|
||||
### Attempt 1: Remove Root-Level Redirects
|
||||
**Hypothesis:** The redirect in `/vercel.json` was intercepting API requests.
|
||||
|
||||
**Original `/vercel.json`:**
|
||||
```json
|
||||
{
|
||||
"redirects": [
|
||||
{
|
||||
"source": "/:path*",
|
||||
"has": [
|
||||
{
|
||||
"type": "host",
|
||||
"value": "www.tpmjs.com"
|
||||
}
|
||||
],
|
||||
"destination": "https://tpmjs.com/:path*",
|
||||
"permanent": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Action:** Removed the `redirects` array from root `/vercel.json`.
|
||||
|
||||
**Result:** ❌ API routes still timeout. Redirect rule was not the root cause.
|
||||
|
||||
**Commit:** `9be3af7 fix(routing): move www redirect from vercel.json to Next.js config`
|
||||
|
||||
### Attempt 2: Move Redirects to Next.js Config
|
||||
**Hypothesis:** Next.js should handle redirects after routing.
|
||||
|
||||
**Action:** Added `async redirects()` to `apps/web/next.config.ts`:
|
||||
```typescript
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
source: '/:path*',
|
||||
has: [
|
||||
{
|
||||
type: 'host',
|
||||
value: 'www.tpmjs.com',
|
||||
},
|
||||
],
|
||||
destination: 'https://tpmjs.com/:path*',
|
||||
permanent: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
**Result:** ❌ API routes returned "Redirecting..." instead of executing. Next.js `async redirects()` applies to ALL routes including API routes.
|
||||
|
||||
**Commit:** `9be3af7 fix(routing): move www redirect from vercel.json to Next.js config`
|
||||
|
||||
### Attempt 3: Exclude API Routes from Redirect
|
||||
**Hypothesis:** Use regex to exclude `/api/*` from redirects.
|
||||
|
||||
**Action:** Modified redirect pattern:
|
||||
```typescript
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
source: '/((?!api).*)', // Negative lookahead to exclude /api/*
|
||||
has: [
|
||||
{
|
||||
type: 'host',
|
||||
value: 'www.tpmjs.com',
|
||||
},
|
||||
],
|
||||
destination: 'https://tpmjs.com/$1',
|
||||
permanent: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
**Result:** ❌ API routes back to timing out (not redirecting anymore, but still not working).
|
||||
|
||||
**Commit:** `c42bfb6 fix(redirects): exclude API routes from www redirect`
|
||||
|
||||
### Attempt 4: Remove All Redirects
|
||||
**Hypothesis:** Eliminate redirect loop causing ERR_TOO_MANY_REDIRECTS.
|
||||
|
||||
**Action:** Removed `async redirects()` entirely from `next.config.ts`.
|
||||
|
||||
**Result:** ✅ Redirect loop fixed. ❌ API routes still timeout.
|
||||
|
||||
**Commit:** `b93cd42 fix: remove redirects to resolve redirect loop`
|
||||
|
||||
### Attempt 5: Add Vercel Functions Configuration
|
||||
**Hypothesis:** Vercel needs explicit configuration to detect API routes.
|
||||
|
||||
**Action:** Added to `apps/web/vercel.json`:
|
||||
```json
|
||||
{
|
||||
"functions": {
|
||||
"app/api/**/*.ts": {
|
||||
"maxDuration": 60
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Result:** ❌ No change. API routes still timeout.
|
||||
|
||||
**Commit:** `748ca4d fix(api): configure Vercel functions for API routes with maxDuration`
|
||||
|
||||
### Attempt 6: Add maxDuration to Route Files
|
||||
**Hypothesis:** Export configuration directly in route handlers.
|
||||
|
||||
**Action:** Added to `apps/web/src/app/api/health/route.ts`:
|
||||
```typescript
|
||||
export const maxDuration = 60;
|
||||
```
|
||||
|
||||
**Result:** ❌ No change. API routes still timeout.
|
||||
|
||||
**Commit:** `8281f8f fix(build): disable Turbopack for Vercel deployment`
|
||||
|
||||
### Attempt 7: Disable Turbopack
|
||||
**Hypothesis:** Turbopack (Next.js 16 default) has compatibility issues with Vercel.
|
||||
|
||||
**Action:** Added `--webpack` flag to build command:
|
||||
```json
|
||||
{
|
||||
"buildCommand": "cd ../.. && turbo build --filter=@tpmjs/web -- --webpack"
|
||||
}
|
||||
```
|
||||
|
||||
**Result:** ❌ Build failed completely. Invalid flag syntax.
|
||||
|
||||
**Commit:** `8281f8f fix(build): disable Turbopack for Vercel deployment`
|
||||
|
||||
### Attempt 8: Simplify Build Command
|
||||
**Hypothesis:** Use direct pnpm build instead of Turbo wrapper.
|
||||
|
||||
**Action:** Changed to:
|
||||
```json
|
||||
{
|
||||
"buildCommand": "cd ../.. && pnpm --filter=@tpmjs/web build"
|
||||
}
|
||||
```
|
||||
|
||||
**Result:** ⏳ Pending deployment test.
|
||||
|
||||
**Commit:** `065196d fix(build): simplify Vercel build command`
|
||||
|
||||
## Root Cause Analysis
|
||||
|
||||
### What We Know FOR SURE
|
||||
|
||||
1. ✅ **API routes build correctly locally**
|
||||
- All 9 API routes compile to `.next/server/app/api/`
|
||||
- NFT (Node File Trace) files are generated with proper dependencies
|
||||
- Routes manifest includes all API routes
|
||||
|
||||
2. ✅ **Next.js configuration is correct**
|
||||
- `export const runtime = 'nodejs'` set correctly
|
||||
- `export const dynamic = 'force-dynamic'` set correctly
|
||||
- `transpilePackages` includes all workspace packages
|
||||
|
||||
3. ✅ **Pages deploy and work perfectly**
|
||||
- `/tool/[slug]` renders correctly
|
||||
- Homepage loads
|
||||
- Client-side navigation works
|
||||
|
||||
4. ❌ **API routes are NOT deployed as serverless functions**
|
||||
- `vercel inspect` shows NO API routes in build output
|
||||
- Only pages appear as `λ` (lambda) functions
|
||||
- This is the PRIMARY issue
|
||||
|
||||
5. ❌ **Direct Vercel URLs timeout**
|
||||
- Not just a custom domain issue
|
||||
- Affects `*.vercel.app` URLs
|
||||
- Exit code 28 (timeout) - no response at all
|
||||
|
||||
6. ❌ **Custom domain shows "Redirecting..."**
|
||||
- Even with all redirects removed from config
|
||||
- Suggests a redirect at Vercel platform level OR DNS level
|
||||
- Could be from the `v0-tool-registry-page` project conflict
|
||||
|
||||
### Possible Root Causes
|
||||
|
||||
#### Theory 1: Vercel Project Misconfiguration
|
||||
**Likelihood:** HIGH
|
||||
|
||||
**Evidence:**
|
||||
- Two projects with same domain (`tpmjs` and `v0-tool-registry-page`)
|
||||
- Framework Preset might not be set to "Next.js"
|
||||
- Root Directory is `apps/web` but Vercel might not be detecting Next.js properly
|
||||
|
||||
**What to Check:**
|
||||
1. Vercel Dashboard → Project Settings → General
|
||||
- Framework Preset: Should be "Next.js"
|
||||
- Root Directory: Should be "apps/web"
|
||||
- Build Command: Should match vercel.json
|
||||
- Output Directory: Should be blank (default `.next`)
|
||||
|
||||
2. Vercel Dashboard → Domains
|
||||
- Check if both projects have tpmjs.com
|
||||
- Remove domain from `v0-tool-registry-page` if present
|
||||
|
||||
3. Vercel Dashboard → Deployments → Build Logs
|
||||
- Search for "API" or "route"
|
||||
- Look for errors about missing functions
|
||||
- Check if Next.js is detected correctly
|
||||
|
||||
#### Theory 2: Monorepo Detection Issue
|
||||
**Likelihood:** MEDIUM
|
||||
|
||||
**Evidence:**
|
||||
- Build command uses `cd ../.. && pnpm --filter=@tpmjs/web build`
|
||||
- Vercel might not be correctly detecting workspace structure
|
||||
- `transpilePackages` includes workspace packages
|
||||
|
||||
**What to Check:**
|
||||
1. Build logs for workspace resolution errors
|
||||
2. Check if `node_modules` is being created in correct location
|
||||
3. Verify pnpm workspace configuration
|
||||
|
||||
**Potential Fix:**
|
||||
Try setting `installCommand` to:
|
||||
```json
|
||||
{
|
||||
"installCommand": "pnpm install --shamefully-hoist"
|
||||
}
|
||||
```
|
||||
|
||||
#### Theory 3: Next.js 16 + Vercel Incompatibility
|
||||
**Likelihood:** MEDIUM
|
||||
|
||||
**Evidence:**
|
||||
- Next.js 16 released recently (November 2024)
|
||||
- Turbopack is default (might have Vercel issues)
|
||||
- App Router API routes behave differently than Pages Router
|
||||
|
||||
**What to Check:**
|
||||
1. Vercel build logs for Next.js version detection
|
||||
2. Any warnings about incompatible features
|
||||
3. Check Vercel's Next.js 16 support status
|
||||
|
||||
**Potential Fix:**
|
||||
Downgrade to Next.js 15.x temporarily to test:
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"next": "^15.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Theory 4: Environment Variable Issue
|
||||
**Likelihood:** LOW
|
||||
|
||||
**Evidence:**
|
||||
- DATABASE_URL is configured
|
||||
- Pages work (they might not need env vars)
|
||||
- API routes use Prisma (requires DATABASE_URL)
|
||||
|
||||
**What to Check:**
|
||||
1. Vercel Dashboard → Settings → Environment Variables
|
||||
- Verify DATABASE_URL is set for Production
|
||||
- Verify it's not blocked or empty
|
||||
- Check if other vars are needed
|
||||
|
||||
2. Build logs for Prisma generation errors
|
||||
|
||||
**Potential Fix:**
|
||||
None - user confirmed DATABASE_URL is set.
|
||||
|
||||
#### Theory 5: Build Output Issue
|
||||
**Likelihood:** MEDIUM-HIGH
|
||||
|
||||
**Evidence:**
|
||||
- `vercel inspect` doesn't show API routes
|
||||
- Only pages are listed as functions
|
||||
- Build completes successfully (38-41 seconds)
|
||||
|
||||
**What to Check:**
|
||||
1. Build logs: Does Next.js report building API routes?
|
||||
- Look for "Route (app)" or "λ" indicators for API routes
|
||||
- Compare to local build output
|
||||
|
||||
2. Check if Vercel is using correct build output structure
|
||||
- App Router uses `.next/server/app/`
|
||||
- Pages Router uses `.next/server/pages/`
|
||||
|
||||
**Potential Fix:**
|
||||
Try forcing Vercel to recognize the build:
|
||||
```json
|
||||
{
|
||||
"builds": [
|
||||
{
|
||||
"src": "package.json",
|
||||
"use": "@vercel/next"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
(Note: `builds` is legacy, modern Next.js should auto-detect)
|
||||
|
||||
## Recommended Next Steps
|
||||
|
||||
### Immediate Actions (High Priority)
|
||||
|
||||
1. **Check Vercel Project Settings**
|
||||
- Go to Vercel Dashboard → tpmjs-web project
|
||||
- Verify Framework Preset is "Next.js"
|
||||
- Verify Root Directory is `apps/web`
|
||||
- Screenshot settings for reference
|
||||
|
||||
2. **Review Build Logs**
|
||||
- Go to latest deployment
|
||||
- Download complete build logs
|
||||
- Search for:
|
||||
- "Route (app)" - should show API routes
|
||||
- "λ" - should show API functions
|
||||
- "api" - any mentions
|
||||
- Errors or warnings
|
||||
|
||||
3. **Check Domain Configuration**
|
||||
- Verify only ONE project has tpmjs.com domain
|
||||
- Remove domain from `v0-tool-registry-page` project if present
|
||||
- Check DNS settings aren't redirecting
|
||||
|
||||
4. **Test Simple API Route**
|
||||
- Create minimal API route:
|
||||
```typescript
|
||||
// apps/web/src/app/api/test/route.ts
|
||||
export async function GET() {
|
||||
return new Response('Hello from API', { status: 200 });
|
||||
}
|
||||
```
|
||||
- Deploy and test
|
||||
- If this doesn't work, confirms platform issue
|
||||
|
||||
### Investigation Actions (Medium Priority)
|
||||
|
||||
5. **Compare Working vs Non-Working**
|
||||
- Find a deployment where pages DO work
|
||||
- Compare build output between page routes and API routes
|
||||
- Look for differences in how they're compiled
|
||||
|
||||
6. **Test Vercel CLI Deploy**
|
||||
- Deploy directly via CLI: `vercel --prod`
|
||||
- Check if behavior differs from Git-based deploy
|
||||
- Might reveal configuration issues
|
||||
|
||||
7. **Check Vercel Function Logs**
|
||||
- Even though functions aren't in build output, try:
|
||||
- `vercel logs <deployment-url> --since 1h`
|
||||
- Look for any API route invocations or errors
|
||||
|
||||
8. **Review Turbo Configuration**
|
||||
```bash
|
||||
# Check turbo.json for Next.js build config
|
||||
cat /turbo.json
|
||||
|
||||
# Verify build runs correctly locally
|
||||
pnpm --filter=@tpmjs/web build
|
||||
```
|
||||
|
||||
### Alternative Approaches (If Above Fails)
|
||||
|
||||
9. **Create New Vercel Project**
|
||||
- Import from Git fresh
|
||||
- Use identical settings
|
||||
- Test if fresh project works
|
||||
|
||||
10. **Contact Vercel Support**
|
||||
- This may be a platform bug with Next.js 16
|
||||
- Provide this document as context
|
||||
- Ask specifically why API routes aren't in build output
|
||||
|
||||
11. **Temporary Workaround**
|
||||
- Deploy API routes separately (different service)
|
||||
- Use Vercel proxy to route `/api/*` to separate deployment
|
||||
- Not ideal but unblocks development
|
||||
|
||||
## Environment Variables Needed
|
||||
|
||||
```bash
|
||||
# Required for API routes
|
||||
DATABASE_URL="postgresql://..."
|
||||
|
||||
# Optional (check if needed)
|
||||
NODE_ENV="production"
|
||||
NEXT_PUBLIC_* # Any public env vars
|
||||
```
|
||||
|
||||
## Build Commands Reference
|
||||
|
||||
### Local Development
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Generate Prisma client
|
||||
pnpm --filter=@tpmjs/db db:generate
|
||||
|
||||
# Run development server
|
||||
pnpm --filter=@tpmjs/web dev
|
||||
|
||||
# Build for production
|
||||
pnpm --filter=@tpmjs/web build
|
||||
|
||||
# Test build locally
|
||||
pnpm --filter=@tpmjs/web start
|
||||
```
|
||||
|
||||
### Vercel Configuration
|
||||
**Current:**
|
||||
```json
|
||||
{
|
||||
"buildCommand": "cd ../.. && pnpm --filter=@tpmjs/web build",
|
||||
"installCommand": "pnpm install"
|
||||
}
|
||||
```
|
||||
|
||||
**Alternative to try:**
|
||||
```json
|
||||
{
|
||||
"buildCommand": "cd ../.. && turbo build --filter=@tpmjs/web",
|
||||
"installCommand": "pnpm install",
|
||||
"framework": "nextjs"
|
||||
}
|
||||
```
|
||||
|
||||
## Key Files to Review
|
||||
|
||||
1. `/apps/web/next.config.ts` - Next.js configuration
|
||||
2. `/apps/web/vercel.json` - Vercel app-level config
|
||||
3. `/vercel.json` - Vercel root config
|
||||
4. `/turbo.json` - Turborepo configuration
|
||||
5. `/apps/web/.next/routes-manifest.json` - Route definitions
|
||||
6. `/apps/web/.next/build-manifest.json` - Build output
|
||||
7. Vercel build logs (from dashboard)
|
||||
|
||||
## Questions for Vercel Support
|
||||
|
||||
If escalating to Vercel support, ask:
|
||||
|
||||
1. Why are API routes not appearing in the build output (`vercel inspect`) when pages are deploying correctly?
|
||||
|
||||
2. Is there a known issue with Next.js 16 App Router API routes in Turborepo monorepos?
|
||||
|
||||
3. What's the correct way to configure `vercel.json` for a Next.js 16 app in a monorepo with custom build commands?
|
||||
|
||||
4. Could having two projects (`tpmjs` and `v0-tool-registry-page`) with the same domain cause routing issues?
|
||||
|
||||
5. Are there any specific requirements for deploying Next.js 16 API routes that differ from Next.js 15?
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Next.js 16 Upgrade Guide](https://nextjs.org/docs/app/guides/upgrading/version-16)
|
||||
- [Vercel Next.js Deployment](https://vercel.com/docs/frameworks/nextjs)
|
||||
- [Vercel Functions Configuration](https://vercel.com/docs/functions/configuring-functions)
|
||||
- [Turborepo with Vercel](https://vercel.com/docs/monorepos/turborepo)
|
||||
- [Next.js App Router API Routes](https://nextjs.org/docs/app/api-reference/file-conventions/route)
|
||||
|
||||
## Recent Commits Related to This Issue
|
||||
|
||||
```
|
||||
065196d fix(build): simplify Vercel build command
|
||||
8281f8f fix(build): disable Turbopack for Vercel deployment
|
||||
748ca4d fix(api): configure Vercel functions for API routes with maxDuration
|
||||
b93cd42 fix: remove redirects to resolve redirect loop
|
||||
c42bfb6 fix(redirects): exclude API routes from www redirect
|
||||
9be3af7 fix(routing): move www redirect from vercel.json to Next.js config
|
||||
a92dfff fix(build): add workspace packages to Next.js transpilePackages
|
||||
cc6c824 fix(vercel): configure Turborepo monorepo build for apps/web
|
||||
```
|
||||
|
||||
## ✅ CONCLUSION - ROOT CAUSE IDENTIFIED
|
||||
|
||||
### The Real Problem
|
||||
|
||||
**Vercel is NOT detecting this project as a Next.js application.**
|
||||
|
||||
When Vercel doesn't detect Next.js, it:
|
||||
- Uses `@vercel/static-builder` instead of `@vercel/next`
|
||||
- Treats the deployment as a static site
|
||||
- Deploys pages (static HTML) successfully
|
||||
- **Completely drops all App Router API routes**
|
||||
- Never generates serverless functions for `/api/*` routes
|
||||
|
||||
This explains EVERY symptom:
|
||||
- ✅ Pages work (they're static files)
|
||||
- ❌ API routes timeout (they were never deployed)
|
||||
- ❌ No `λ api/*` in build output (functions don't exist)
|
||||
- ❌ Direct Vercel URLs timeout (not a DNS issue)
|
||||
- ❌ "Redirecting..." on custom domain (wrong project owns the domain)
|
||||
|
||||
### Why Vercel Doesn't Detect Next.js
|
||||
|
||||
**1. Wrong Root Directory**
|
||||
- Vercel project likely has Root Directory set to `.` or empty
|
||||
- Should be exactly: `apps/web`
|
||||
- A single character difference breaks Next.js detection
|
||||
|
||||
**2. Wrong Framework Preset**
|
||||
- When Vercel can't auto-detect Next.js (due to monorepo + wrong root)
|
||||
- It defaults to Framework Preset = "Other"
|
||||
- "Other" uses static builder, not Next.js builder
|
||||
|
||||
**3. Domain Conflict**
|
||||
- Two projects exist: `tpmjs` and `v0-tool-registry-page`
|
||||
- Both have `tpmjs.com` domain assigned
|
||||
- Requests randomly route to wrong project
|
||||
- "Redirecting..." comes from the old project, not your code
|
||||
|
||||
**4. Custom Build Commands**
|
||||
- Custom build commands can bypass Vercel's auto-detection
|
||||
- Should let Vercel auto-detect and use default commands
|
||||
|
||||
### Evidence
|
||||
|
||||
**Local build works:**
|
||||
```bash
|
||||
$ ls apps/web/.next/server/app/api/
|
||||
health/ stats/ sync/ tools/
|
||||
# All API routes compile correctly
|
||||
```
|
||||
|
||||
**Vercel deployment missing API routes:**
|
||||
```bash
|
||||
$ vercel inspect https://tpmjs-1chh44d1u-tpmjs.vercel.app
|
||||
Builds
|
||||
├── λ tool/[slug] # Pages present
|
||||
├── λ _global-error # Pages present
|
||||
└── 56 output items hidden
|
||||
# NO api/health, NO api/tools, NO api/* at all
|
||||
```
|
||||
|
||||
This is **not a Next.js bug, not a Turborepo bug, not a code issue**.
|
||||
This is **pure Vercel project misconfiguration**.
|
||||
|
||||
### The Fix (4 Steps, 5 Minutes)
|
||||
|
||||
See `VERCEL_FIX_CHECKLIST.md` for detailed instructions.
|
||||
|
||||
**Quick version:**
|
||||
1. Set Root Directory to `apps/web` (exactly)
|
||||
2. Set Framework Preset to "Next.js"
|
||||
3. Remove `tpmjs.com` domain from `v0-tool-registry-page` project
|
||||
4. Clear custom build commands (let Vercel auto-detect)
|
||||
|
||||
**No code changes needed. Just fix Vercel dashboard settings.**
|
||||
|
||||
### After the Fix
|
||||
|
||||
Next deployment will show:
|
||||
```bash
|
||||
$ vercel inspect <new-deployment>
|
||||
Builds
|
||||
├── λ api/health
|
||||
├── λ api/tools
|
||||
├── λ api/sync/changes
|
||||
├── λ api/tools/[id]
|
||||
├── λ api/tools/[slug]
|
||||
├── λ tool/[slug]
|
||||
...
|
||||
```
|
||||
|
||||
And API routes will work instantly:
|
||||
```bash
|
||||
$ curl https://tpmjs.com/api/health
|
||||
{"status":"ok","timestamp":"..."}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary for ChatGPT
|
||||
|
||||
The core issue is that **Vercel is not detecting the project as Next.js** due to incorrect Root Directory, Framework Preset set to "Other", domain conflicts between two projects, and custom build commands bypassing auto-detection.
|
||||
|
||||
When Vercel doesn't detect Next.js, it uses a static site builder which deploys pages but **completely drops all App Router API routes** from the build. This is why pages work but all `/api/*` requests timeout - the API route serverless functions were never created.
|
||||
|
||||
**The fix is purely configuration** - no code changes needed. Set Root Directory to `apps/web`, Framework Preset to "Next.js", remove the domain from the old project, and clear custom build commands. See `VERCEL_FIX_CHECKLIST.md` for step-by-step instructions.
|
||||
844
ARCHITECTURE.md
Normal file
844
ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,844 @@
|
|||
# TPMJS Architecture Documentation
|
||||
|
||||
A comprehensive guide to the TPMJS platform architecture - from tool discovery to sandboxed execution, collections, agents, and custom executors.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Platform Overview](#1-platform-overview)
|
||||
2. [Monorepo Structure](#2-monorepo-structure)
|
||||
3. [Database Layer](#3-database-layer)
|
||||
4. [Tool Execution System](#4-tool-execution-system)
|
||||
5. [MCP Protocol Implementation](#5-mcp-protocol-implementation)
|
||||
6. [Agent System](#6-agent-system)
|
||||
7. [Collection System](#7-collection-system)
|
||||
8. [NPM Sync System](#8-npm-sync-system)
|
||||
9. [API Layer](#9-api-layer)
|
||||
10. [SDK Packages](#10-sdk-packages)
|
||||
11. [UI & Frontend](#11-ui--frontend)
|
||||
12. [Security & Authentication](#12-security--authentication)
|
||||
|
||||
---
|
||||
|
||||
## 1. Platform Overview
|
||||
|
||||
TPMJS is a **tool registry platform** that automatically discovers, validates, and executes npm packages as AI agent tools. The platform supports multiple AI providers (OpenAI, Anthropic, Google, Groq, Mistral) and exposes tools via MCP (Model Context Protocol) for use with Claude Desktop, Cursor, and other MCP clients.
|
||||
|
||||
### High-Level Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ USER PRODUCTS │
|
||||
├─────────────────────┬─────────────────────┬─────────────────────────────────┤
|
||||
│ tpmjs.com │ SDK Packages │ MCP Protocol │
|
||||
│ ───────────────── │ ───────────────── │ ───────────────────────────── │
|
||||
│ • Dashboard │ • @tpmjs/types │ • Claude Desktop │
|
||||
│ • Tool Browser │ • registry-search │ • Cursor │
|
||||
│ • Collection Editor│ • registry-execute │ • Claude Code │
|
||||
│ • Agent Builder │ │ • Any MCP Client │
|
||||
│ • Playground │ │ │
|
||||
└─────────────────────┴─────────────────────┴─────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ API LAYER (Next.js 16) │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ /api/tools /api/agents /api/collections /api/mcp/* /api/sync/* │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ INFRASTRUCTURE │
|
||||
├───────────────────────┬───────────────────────┬─────────────────────────────┤
|
||||
│ Database │ Execution │ External │
|
||||
│ ─────────────────── │ ─────────────────── │ ───────────────────────── │
|
||||
│ • PostgreSQL (Neon) │ • Vercel Sandbox │ • npm Registry │
|
||||
│ • Prisma ORM │ • Custom Executors │ • esm.sh CDN │
|
||||
│ │ │ • GitHub API │
|
||||
└───────────────────────┴───────────────────────┴─────────────────────────────┘
|
||||
```
|
||||
|
||||
### Key Concepts
|
||||
|
||||
| Concept | Description |
|
||||
|---------|-------------|
|
||||
| **Tool** | A single executable function from an npm package |
|
||||
| **Package** | An npm package containing one or more tools |
|
||||
| **Collection** | A user-curated bundle of tools exposed via MCP |
|
||||
| **Agent** | An AI assistant with access to tools and collections |
|
||||
| **Executor** | A sandboxed environment for running tool code |
|
||||
|
||||
---
|
||||
|
||||
## 2. Monorepo Structure
|
||||
|
||||
TPMJS uses **Turborepo** with **pnpm** workspaces. The codebase is organized into packages and applications.
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
tpmjs/
|
||||
├── apps/
|
||||
│ ├── web/ # Main Next.js 16 application
|
||||
│ ├── playground/ # Interactive tool testing
|
||||
│ ├── tutorial/ # Tutorial application
|
||||
│ └── railway-executor/ # Deno executor service
|
||||
│
|
||||
├── packages/
|
||||
│ ├── ui/ # React component library (@tpmjs/ui)
|
||||
│ ├── types/ # TypeScript types & Zod schemas (@tpmjs/types)
|
||||
│ ├── utils/ # Utility functions (@tpmjs/utils)
|
||||
│ ├── env/ # Environment validation (@tpmjs/env)
|
||||
│ ├── db/ # Prisma database client (@tpmjs/db)
|
||||
│ ├── npm-client/ # NPM Registry API client
|
||||
│ ├── package-executor/ # Tool execution client
|
||||
│ ├── config/ # Shared configs (Biome, ESLint, Tailwind, TS)
|
||||
│ └── tools/ # 150+ official TPMJS tools
|
||||
│ └── official/ # @tpmjs/tools-* packages
|
||||
│
|
||||
├── turbo.json # Turborepo task configuration
|
||||
├── pnpm-workspace.yaml # Workspace definitions
|
||||
└── vercel.json # Deployment & cron configuration
|
||||
```
|
||||
|
||||
### Published Packages (npm @tpmjs scope)
|
||||
|
||||
| Package | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| `@tpmjs/types` | 0.2.0 | TypeScript types and Zod validation schemas |
|
||||
| `@tpmjs/utils` | 0.1.1 | Utility functions (cn, format helpers) |
|
||||
| `@tpmjs/ui` | 0.1.3 | React component library (30+ components) |
|
||||
| `@tpmjs/env` | 0.1.1 | Environment variable validation |
|
||||
|
||||
### Internal Packages
|
||||
|
||||
| Package | Purpose |
|
||||
|---------|---------|
|
||||
| `@tpmjs/db` | Prisma client and database schema |
|
||||
| `@tpmjs/npm-client` | NPM Registry API client for syncing |
|
||||
| `@tpmjs/package-executor` | Remote executor HTTP client |
|
||||
| `@tpmjs/config` | Shared Biome, ESLint, Tailwind, TypeScript configs |
|
||||
|
||||
### Key Architecture Principles
|
||||
|
||||
1. **No Barrel Exports**: Components imported directly (`@tpmjs/ui/Button/Button`)
|
||||
2. **Strict Module Boundaries**: Apps import from packages, not vice versa
|
||||
3. **TypeScript Everywhere**: Strict mode with composite projects
|
||||
4. **Shared Configurations**: Centralized in `packages/config/`
|
||||
|
||||
---
|
||||
|
||||
## 3. Database Layer
|
||||
|
||||
The database layer uses **Prisma ORM** with **PostgreSQL** (Neon) as the data store.
|
||||
|
||||
### Core Models
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ TOOL REGISTRY │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Package (1) ──────────────────────► (N) Tool │
|
||||
│ ├── npmPackageName (unique) ├── id (PK) │
|
||||
│ ├── npmVersion ├── name │
|
||||
│ ├── category ├── description │
|
||||
│ ├── tier (minimal|rich) ├── inputSchema (JSON) │
|
||||
│ ├── npmDownloadsLastMonth ├── qualityScore │
|
||||
│ └── githubStars ├── importHealth │
|
||||
│ └── executionHealth │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ USER & SOCIAL │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ User (1) ──────► (N) Agent ──────► (N) Conversation ──────► (N) Message │
|
||||
│ │ │ │
|
||||
│ │ └──────► (N) AgentTool │
|
||||
│ │ └──────► (N) AgentCollection │
|
||||
│ │ │
|
||||
│ └──────► (N) Collection ──────► (N) CollectionTool │
|
||||
│ │ │
|
||||
│ └──────► (N) ToolLike, CollectionLike, AgentLike │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ SYNC & MONITORING │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ SyncCheckpoint SyncLog HealthCheck │
|
||||
│ ├── source (unique) ├── source ├── toolId │
|
||||
│ └── checkpoint (JSON) ├── status ├── importStatus │
|
||||
│ ├── processed ├── executionStatus │
|
||||
│ └── errors └── checkType │
|
||||
│ │
|
||||
│ Simulation TokenUsage StatsSnapshot │
|
||||
│ ├── toolId ├── simulationId ├── date (unique) │
|
||||
│ ├── status ├── inputTokens ├── totalTools │
|
||||
│ └── output └── totalTokens └── healthStats │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Key Query Patterns
|
||||
|
||||
**1. Pagination without COUNT (limit+1 technique):**
|
||||
```typescript
|
||||
const tools = await prisma.tool.findMany({
|
||||
take: limit + 1, // Fetch one extra to check hasMore
|
||||
skip: offset,
|
||||
});
|
||||
const hasMore = tools.length > limit;
|
||||
const actualTools = hasMore ? tools.slice(0, limit) : tools;
|
||||
```
|
||||
|
||||
**2. Atomic Like/Unlike with Transactions:**
|
||||
```typescript
|
||||
const [like, updatedTool] = await prisma.$transaction([
|
||||
prisma.toolLike.create({ data: { userId, toolId } }),
|
||||
prisma.tool.update({
|
||||
where: { id: toolId },
|
||||
data: { likeCount: { increment: 1 } }
|
||||
})
|
||||
]);
|
||||
```
|
||||
|
||||
**3. Upsert for Idempotent Sync Operations:**
|
||||
```typescript
|
||||
await prisma.package.upsert({
|
||||
where: { npmPackageName: pkg.name },
|
||||
create: { /* ... */ },
|
||||
update: { /* ... */ }
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Tool Execution System
|
||||
|
||||
The execution system provides sandboxed environments for safely running npm package tools.
|
||||
|
||||
### Execution Flow
|
||||
|
||||
```
|
||||
┌───────────────┐ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐
|
||||
│ 1. REQUEST │────►│ 2. RESOLVE │────►│ 3. EXECUTE │────►│ 4. RESPONSE │
|
||||
├───────────────┤ ├───────────────┤ ├───────────────┤ ├───────────────┤
|
||||
│ SDK: │ │ Lookup tool │ │ npm install │ │ output: any │
|
||||
│ registryExec │ │ by ID │ │ pkg │ │ │
|
||||
│ │ │ │ │ │ │ executionTime │
|
||||
│ MCP: │ │ Resolve │ │ tool.execute │ │ Ms │
|
||||
│ tools/call │ │ executor │ │ (params) │ │ │
|
||||
│ │ │ config │ │ │ │ success: │
|
||||
│ Agent: │ │ │ │ Return │ │ boolean │
|
||||
│ tool_call │ │ Build import │ │ result │ │ │
|
||||
│ │ │ URL │ │ │ │ │
|
||||
└───────────────┘ └───────────────┘ └───────────────┘ └───────────────┘
|
||||
```
|
||||
|
||||
### Executor Types
|
||||
|
||||
**1. Default Executor (Vercel Sandbox)**
|
||||
- Pre-configured sandbox environment
|
||||
- Node.js 22, 2 vCPUs, 2 minute timeout
|
||||
- Network isolated, per-request env injection
|
||||
- Automatic npm install
|
||||
|
||||
**2. Custom URL Executor**
|
||||
- User-deployed executor service
|
||||
- Deploy to Vercel, Railway, AWS Lambda, or self-host
|
||||
- Custom dependencies pre-installed
|
||||
- Your own API keys built-in
|
||||
|
||||
### Executor Config Cascade
|
||||
|
||||
```
|
||||
┌─────────────────────┐
|
||||
│ System Default │ ◄─── Vercel Sandbox
|
||||
│ (lowest priority) │
|
||||
└─────────┬───────────┘
|
||||
│ overridden by
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ Collection Config │ ◄─── executorConfig on Collection
|
||||
│ │
|
||||
└─────────┬───────────┘
|
||||
│ overridden by
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ Agent Config │ ◄─── executorConfig on Agent
|
||||
│ (highest priority) │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
### Executor API Contract
|
||||
|
||||
All executors must implement:
|
||||
|
||||
**POST /execute-tool**
|
||||
```typescript
|
||||
interface ExecuteToolRequest {
|
||||
packageName: string; // "@tpmjs/hello"
|
||||
name: string; // "helloWorldTool"
|
||||
version?: string; // "1.0.0" or "latest"
|
||||
params: Record<string, unknown>;
|
||||
env?: Record<string, string>;
|
||||
}
|
||||
|
||||
interface ExecuteToolResponse {
|
||||
success: boolean;
|
||||
output?: unknown;
|
||||
error?: string;
|
||||
executionTimeMs: number;
|
||||
}
|
||||
```
|
||||
|
||||
**GET /health**
|
||||
```typescript
|
||||
interface HealthResponse {
|
||||
status: 'ok' | 'degraded' | 'error';
|
||||
version?: string;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. MCP Protocol Implementation
|
||||
|
||||
TPMJS implements the **Model Context Protocol (MCP)** to expose collections as tool servers for AI clients.
|
||||
|
||||
### MCP Endpoints
|
||||
|
||||
| Transport | Endpoint | Purpose |
|
||||
|-----------|----------|---------|
|
||||
| HTTP | `/api/mcp/{username}/{slug}/http` | Request-response |
|
||||
| SSE | `/api/mcp/{username}/{slug}/sse` | Streaming |
|
||||
|
||||
### JSON-RPC Methods
|
||||
|
||||
**initialize** - Returns server capabilities
|
||||
```json
|
||||
{
|
||||
"protocolVersion": "2024-11-05",
|
||||
"serverInfo": { "name": "TPMJS: My Collection", "version": "1.0.0" },
|
||||
"capabilities": { "tools": {} }
|
||||
}
|
||||
```
|
||||
|
||||
**tools/list** - Returns available tools in collection
|
||||
```json
|
||||
{
|
||||
"tools": [{
|
||||
"name": "tpmjs-hello--helloWorldTool",
|
||||
"description": "A simple hello world tool",
|
||||
"inputSchema": { "type": "object", "properties": { ... } }
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
**tools/call** - Executes a tool
|
||||
```json
|
||||
{
|
||||
"content": [{ "type": "text", "text": "Hello World!" }]
|
||||
}
|
||||
```
|
||||
|
||||
### Tool Name Format
|
||||
|
||||
MCP tool names are sanitized from npm package names:
|
||||
|
||||
```
|
||||
@tpmjs/hello + helloWorldTool → tpmjs-hello--helloWorldTool
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Agent System
|
||||
|
||||
Agents are AI-powered assistants with multi-turn conversations and tool access.
|
||||
|
||||
### Agent Configuration
|
||||
|
||||
```typescript
|
||||
interface Agent {
|
||||
// Identity
|
||||
id: string;
|
||||
uid: string; // URL-friendly ID
|
||||
name: string;
|
||||
description?: string;
|
||||
|
||||
// Model Configuration
|
||||
provider: 'OPENAI' | 'ANTHROPIC' | 'GOOGLE' | 'GROQ' | 'MISTRAL';
|
||||
modelId: string; // e.g., "gpt-4o", "claude-3-5-sonnet"
|
||||
systemPrompt?: string;
|
||||
temperature: number; // 0-2, default 0.7
|
||||
|
||||
// Behavior
|
||||
maxToolCallsPerTurn: number; // 1-100, default 20
|
||||
maxMessagesInContext: number; // 1-100, default 10
|
||||
|
||||
// Visibility
|
||||
isPublic: boolean;
|
||||
|
||||
// Executor Override
|
||||
executorType?: 'default' | 'custom_url';
|
||||
executorConfig?: { url: string; apiKey?: string };
|
||||
|
||||
// Relations
|
||||
collections: AgentCollection[];
|
||||
tools: AgentTool[];
|
||||
}
|
||||
```
|
||||
|
||||
### Conversation Flow
|
||||
|
||||
```
|
||||
User Message
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────┐
|
||||
│ Save MESSAGE (role=USER) │
|
||||
└────────────────┬───────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────┐
|
||||
│ Fetch message history │
|
||||
│ (maxMessagesInContext) │
|
||||
└────────────────┬───────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────┐
|
||||
│ Build AI SDK messages + tools │
|
||||
│ • System prompt │
|
||||
│ • Conversation history │
|
||||
│ • Tool definitions │
|
||||
└────────────────┬───────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────┐
|
||||
│ streamText() with tool use │
|
||||
│ • SSE chunks to client │
|
||||
│ • Tool calls executed │
|
||||
│ • Results fed back to model │
|
||||
└────────────────┬───────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────┐
|
||||
│ Save MESSAGE (role=ASSISTANT) │
|
||||
│ Save MESSAGE (role=TOOL) for each │
|
||||
│ tool call result │
|
||||
└────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### SSE Event Types
|
||||
|
||||
| Event | Description |
|
||||
|-------|-------------|
|
||||
| `chunk` | Text token from AI |
|
||||
| `tool_call` | AI decided to call a tool |
|
||||
| `tool_result` | Tool execution completed |
|
||||
| `tokens` | Token usage statistics |
|
||||
| `complete` | Conversation finished |
|
||||
| `error` | Error occurred |
|
||||
|
||||
---
|
||||
|
||||
## 7. Collection System
|
||||
|
||||
Collections are user-curated bundles of tools that can be shared and exposed via MCP.
|
||||
|
||||
### Collection Structure
|
||||
|
||||
```typescript
|
||||
interface Collection {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string; // URL-friendly, unique per user
|
||||
description?: string;
|
||||
isPublic: boolean;
|
||||
|
||||
// Executor Override (applies to all tools)
|
||||
executorType?: 'default' | 'custom_url';
|
||||
executorConfig?: { url: string; apiKey?: string };
|
||||
|
||||
// Relations
|
||||
tools: CollectionTool[]; // Junction table with position, notes
|
||||
}
|
||||
|
||||
interface CollectionTool {
|
||||
toolId: string;
|
||||
position: number; // User-defined ordering
|
||||
note?: string; // User notes about the tool
|
||||
}
|
||||
```
|
||||
|
||||
### Collection Limits
|
||||
|
||||
| Limit | Value |
|
||||
|-------|-------|
|
||||
| Max collections per user | 50 |
|
||||
| Max tools per collection | 100 |
|
||||
| Max name length | 100 chars |
|
||||
| Max description length | 500 chars |
|
||||
|
||||
### MCP Access URLs
|
||||
|
||||
Public collections can be accessed via MCP:
|
||||
|
||||
```
|
||||
HTTP: https://tpmjs.com/api/mcp/{username}/{slug}/http
|
||||
SSE: https://tpmjs.com/api/mcp/{username}/{slug}/sse
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. NPM Sync System
|
||||
|
||||
TPMJS automatically discovers tools from npm using multiple sync strategies.
|
||||
|
||||
### Sync Jobs
|
||||
|
||||
| Job | Schedule | Purpose |
|
||||
|-----|----------|---------|
|
||||
| Changes Feed | Every 2 min | Monitor npm real-time updates |
|
||||
| Keyword Search | Every 15 min | Search for `tpmjs` keyword |
|
||||
| Metrics | Every hour | Update downloads & quality scores |
|
||||
| Health Check | Daily | Verify tool import/execution |
|
||||
| Stats Snapshot | Daily | Capture historical statistics |
|
||||
|
||||
### Discovery Flow
|
||||
|
||||
```
|
||||
npm Registry
|
||||
│
|
||||
├──► Changes Feed (/api/sync/changes)
|
||||
│ • Polls /_changes endpoint
|
||||
│ • 30 packages per run
|
||||
│ • Checkpoint-based (lastSeq)
|
||||
│
|
||||
└──► Keyword Search (/api/sync/keyword)
|
||||
• Searches for keyword:tpmjs
|
||||
• 250 packages per run
|
||||
• Backup discovery
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────┐
|
||||
│ Validate tpmjs field │
|
||||
│ • Multi-tool format (new) │
|
||||
│ • Legacy rich format │
|
||||
│ • Legacy minimal format │
|
||||
└────────────────┬───────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────┐
|
||||
│ Auto-discover tools │
|
||||
│ • If tools[] missing/empty │
|
||||
│ • Call executor listToolExports │
|
||||
│ • Extract JSON schemas │
|
||||
└────────────────┬───────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────┐
|
||||
│ Update database │
|
||||
│ • Upsert Package record │
|
||||
│ • Upsert Tool records │
|
||||
│ • Trigger health checks │
|
||||
└────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Quality Score Calculation
|
||||
|
||||
```typescript
|
||||
qualityScore = tierScore + downloadsScore + starsScore + richnessScore
|
||||
|
||||
// tierScore: 0.6 (rich) or 0.4 (minimal)
|
||||
// downloadsScore: log10(downloads) / 15, max 0.2
|
||||
// starsScore: log10(stars) / 10, max 0.1
|
||||
// richnessScore: +0.04 (params) +0.03 (returns) +0.03 (aiAgent)
|
||||
|
||||
// Range: 0.00 - 1.00
|
||||
```
|
||||
|
||||
### tpmjs Field Specification
|
||||
|
||||
**Multi-Tool Format (Recommended):**
|
||||
```json
|
||||
{
|
||||
"tpmjs": {
|
||||
"category": "utilities",
|
||||
"tools": [
|
||||
{
|
||||
"name": "helloWorld",
|
||||
"description": "Greets a user by name"
|
||||
},
|
||||
{
|
||||
"name": "goodbye",
|
||||
"description": "Says goodbye to a user"
|
||||
}
|
||||
],
|
||||
"frameworks": ["vercel-ai"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Valid Categories:**
|
||||
- Core: `research`, `web`, `data`, `documentation`, `engineering`, `security`, `statistics`, `ops`, `agent`, `utilities`, `html`, `compliance`
|
||||
- Legacy: `web-scraping`, `data-processing`, `file-operations`, `communication`, `database`, `api-integration`, `image-processing`, `text-analysis`, `automation`, `ai-ml`, `monitoring`
|
||||
|
||||
---
|
||||
|
||||
## 9. API Layer
|
||||
|
||||
The API is built on Next.js 16 App Router with standardized response formats.
|
||||
|
||||
### Response Format
|
||||
|
||||
**Success:**
|
||||
```typescript
|
||||
{
|
||||
success: true,
|
||||
data: T,
|
||||
meta: {
|
||||
version: "1.0.0",
|
||||
timestamp: "2025-01-11T...",
|
||||
requestId: "uuid"
|
||||
},
|
||||
pagination?: {
|
||||
limit: number,
|
||||
offset: number,
|
||||
count: number,
|
||||
hasMore: boolean
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Error:**
|
||||
```typescript
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
code: "VALIDATION_ERROR" | "NOT_FOUND" | "UNAUTHORIZED" | ...,
|
||||
message: "Human-readable message",
|
||||
details?: { ... }
|
||||
},
|
||||
meta: { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### Key Endpoints
|
||||
|
||||
| Category | Endpoint | Purpose |
|
||||
|----------|----------|---------|
|
||||
| **Tools** | `GET /api/tools` | List/search tools |
|
||||
| | `POST /api/tools/execute/[...slug]` | Execute tool (SSE) |
|
||||
| **Agents** | `GET /api/agents` | List user agents |
|
||||
| | `POST /api/{username}/agents/{uid}/conversation/{convId}` | Chat with agent (SSE) |
|
||||
| **Collections** | `GET /api/collections` | List user collections |
|
||||
| | `POST /api/collections/[id]/tools` | Add tool to collection |
|
||||
| **MCP** | `POST /api/mcp/{username}/{slug}/{transport}` | MCP protocol |
|
||||
| **Sync** | `POST /api/sync/changes` | Cron: npm changes |
|
||||
| **Stats** | `GET /api/stats` | Registry statistics |
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
| Endpoint Type | Limit | Window |
|
||||
|---------------|-------|--------|
|
||||
| Default | 100 requests | 1 minute |
|
||||
| Strict | 20 requests | 1 minute |
|
||||
| Tool Execute | 10 requests | 1 hour |
|
||||
| Conversation | 30 requests | 1 minute |
|
||||
|
||||
### Authentication
|
||||
|
||||
- **Library:** `better-auth` with Prisma adapter
|
||||
- **Session:** 7-day expiry, cookie-based
|
||||
- **Email:** Verification required for login
|
||||
- **Protected Routes:** Check `auth.api.getSession()`
|
||||
|
||||
---
|
||||
|
||||
## 10. SDK Packages
|
||||
|
||||
### @tpmjs/types
|
||||
|
||||
Core TypeScript types and Zod validation schemas.
|
||||
|
||||
**Exports:**
|
||||
- `./tool` - Tool and ToolParameter schemas
|
||||
- `./registry` - Search result schemas
|
||||
- `./tpmjs` - tpmjs field validation (validateTpmjsField)
|
||||
- `./agent` - Agent configuration schemas
|
||||
- `./collection` - Collection schemas
|
||||
- `./user` - User profile schemas
|
||||
- `./executor` - Executor request/response types
|
||||
|
||||
### @tpmjs/npm-client (Internal)
|
||||
|
||||
NPM Registry API client for sync operations.
|
||||
|
||||
**Functions:**
|
||||
- `fetchChanges()` - Poll changes feed
|
||||
- `searchByKeyword()` - Search packages
|
||||
- `fetchLatestPackageWithMetadata()` - Get package info
|
||||
- `fetchDownloadStats()` - Get npm downloads
|
||||
- `fetchGitHubStars()` - Get GitHub stars
|
||||
|
||||
### @tpmjs/package-executor (Internal)
|
||||
|
||||
Remote executor client for tool execution.
|
||||
|
||||
**Functions:**
|
||||
- `executePackage(packageName, functionName, params)` - Execute tool
|
||||
- `clearCache()` - Clear executor cache
|
||||
- `checkHealth()` - Check executor health
|
||||
|
||||
---
|
||||
|
||||
## 11. UI & Frontend
|
||||
|
||||
### Component Library (@tpmjs/ui)
|
||||
|
||||
30+ React components with no-barrel-exports architecture.
|
||||
|
||||
**Categories:**
|
||||
- **Form:** Button, Input, Select, Checkbox, Radio, Switch, Textarea, Slider
|
||||
- **Layout:** Card, Container, Section, GridContainer, Header
|
||||
- **Display:** Badge, ProgressBar, Spinner, Icon, CodeBlock, Table
|
||||
- **Advanced:** Tabs, AnimatedCounter, StatCard, ActivityStream, FlowDiagram
|
||||
|
||||
### Design System
|
||||
|
||||
**Color System (CSS Variables):**
|
||||
```css
|
||||
/* Backgrounds */
|
||||
--background, --surface, --surface-secondary, --surface-elevated
|
||||
|
||||
/* Text */
|
||||
--foreground, --foreground-secondary, --foreground-tertiary, --foreground-muted
|
||||
|
||||
/* Interactive */
|
||||
--primary, --secondary, --accent
|
||||
|
||||
/* Status */
|
||||
--success, --error, --warning, --info
|
||||
|
||||
/* Borders */
|
||||
--border, --border-strong
|
||||
```
|
||||
|
||||
**Theme Support:**
|
||||
- Light mode (default)
|
||||
- Dark mode (Vercel/Cursor aesthetic)
|
||||
- `next-themes` provider
|
||||
|
||||
### Dashboard Structure
|
||||
|
||||
```
|
||||
/dashboard
|
||||
├── Overview # Quick actions, profile, activity
|
||||
├── Agents # Create/manage AI agents
|
||||
│ └── [id]/chat # Chat interface
|
||||
├── Collections # Organize tools
|
||||
├── Settings
|
||||
│ └── api-keys # Manage API keys
|
||||
└── Likes
|
||||
├── tools
|
||||
├── collections
|
||||
└── agents
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Security & Authentication
|
||||
|
||||
### Authentication Flow
|
||||
|
||||
```
|
||||
Sign Up → Email Verification → Sign In → Session Cookie → Protected Routes
|
||||
```
|
||||
|
||||
### API Key Storage
|
||||
|
||||
User API keys (OpenAI, Anthropic, etc.) are stored encrypted:
|
||||
- AES-256-CBC encryption
|
||||
- Unique IV per key
|
||||
- Only hint (last 4 chars) visible in UI
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
- **Distributed:** Vercel KV with in-memory fallback
|
||||
- **Per-IP:** Based on `x-forwarded-for`, `x-real-ip`, or `cf-connecting-ip`
|
||||
- **Headers:** `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `Retry-After`
|
||||
|
||||
### Cron Security
|
||||
|
||||
All sync endpoints require:
|
||||
```
|
||||
Authorization: Bearer {CRON_SECRET}
|
||||
```
|
||||
|
||||
Vercel Cron automatically adds this header.
|
||||
|
||||
### Executor Verification
|
||||
|
||||
Custom executor URLs are verified:
|
||||
1. HTTPS required in production
|
||||
2. Private IP ranges blocked
|
||||
3. Health endpoint checked
|
||||
4. Test tool execution validated
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Required | Purpose |
|
||||
|----------|----------|---------|
|
||||
| `DATABASE_URL` | Yes | PostgreSQL connection |
|
||||
| `BETTER_AUTH_SECRET` | Yes | Session encryption (32+ chars) |
|
||||
| `CRON_SECRET` | Yes | Cron job auth (32+ chars) |
|
||||
| `SANDBOX_EXECUTOR_URL` | No | Default executor URL |
|
||||
| `GITHUB_TOKEN` | No | GitHub API for stars |
|
||||
|
||||
### Commands
|
||||
|
||||
```bash
|
||||
# Development
|
||||
pnpm dev # Run all dev servers
|
||||
pnpm --filter=@tpmjs/web dev # Run web app only
|
||||
|
||||
# Database
|
||||
pnpm --filter=@tpmjs/db db:generate # Generate Prisma client
|
||||
pnpm --filter=@tpmjs/db db:push # Push schema changes
|
||||
pnpm --filter=@tpmjs/db db:studio # Open Prisma Studio
|
||||
|
||||
# Testing
|
||||
pnpm test # Run all tests
|
||||
pnpm type-check # Type-check all packages
|
||||
pnpm lint # Lint all packages
|
||||
|
||||
# Building
|
||||
pnpm build # Build all packages
|
||||
```
|
||||
|
||||
### Tech Stack
|
||||
|
||||
| Category | Technology |
|
||||
|----------|------------|
|
||||
| Framework | Next.js 16 (App Router) |
|
||||
| Language | TypeScript 5.9 (strict) |
|
||||
| Database | PostgreSQL + Prisma 6.19 |
|
||||
| Auth | better-auth 1.4 |
|
||||
| AI SDK | Vercel AI SDK 6.0 |
|
||||
| Styling | Tailwind CSS 4.1 |
|
||||
| Build | Turborepo + pnpm |
|
||||
| Testing | Vitest + Testing Library |
|
||||
| Deployment | Vercel |
|
||||
|
||||
---
|
||||
|
||||
*This documentation was auto-generated from codebase exploration. Last updated: January 2025*
|
||||
741
CLAUDE.md
741
CLAUDE.md
|
|
@ -1,681 +1,144 @@
|
|||
## Project Overview
|
||||
|
||||
## Monorepo Setup
|
||||
Turborepo monorepo. pnpm workspaces. Next.js 16 App Router (`apps/web`). PostgreSQL via Prisma (`packages/db`). Deployed on Vercel. Database on Neon (via Railway for some services).
|
||||
|
||||
This project uses a Turborepo monorepo architecture with the following structure:
|
||||
## Architecture Rules
|
||||
|
||||
### Packages
|
||||
1. **Use `@tpmjs/ui` components** — never raw HTML `<button>`, `<input>`, `<table>`, etc.
|
||||
2. **No barrel exports** — import directly: `@tpmjs/ui/Button/Button`, not `@tpmjs/ui`
|
||||
3. **Module boundaries** — apps import packages, never the reverse. UI has no deps on utils.
|
||||
4. **Avoid `count()` in API routes** — use `take: limit + 1` technique for pagination (Prisma cold start is slow in serverless)
|
||||
5. **All API routes need** `export const runtime = 'nodejs'` and `export const maxDuration = 60`
|
||||
|
||||
**Published to npm (@tpmjs scope):**
|
||||
- `@tpmjs/ui` - React component library with .ts-only components
|
||||
- `@tpmjs/utils` - Utility functions (cn, format, etc.)
|
||||
- `@tpmjs/types` - Shared TypeScript types and Zod schemas
|
||||
- `@tpmjs/env` - Environment variable validation with Zod
|
||||
|
||||
**Internal tooling (private):**
|
||||
- `@tpmjs/config` - Shared configurations (Biome, ESLint, Tailwind, TypeScript)
|
||||
- `@tpmjs/eslint-config` - ESLint configuration with module boundary rules
|
||||
- `@tpmjs/tailwind-config` - Tailwind configuration with design tokens
|
||||
- `@tpmjs/tsconfig` - TypeScript configurations (base, nextjs, react-library)
|
||||
- `@tpmjs/test` - Vitest shared configuration
|
||||
- `@tpmjs/mocks` - MSW mock server for testing
|
||||
- `@tpmjs/storybook` - Component documentation and showcase
|
||||
|
||||
### Applications
|
||||
|
||||
- `@tpmjs/web` - Next.js 16 App Router application (main website)
|
||||
|
||||
### Architecture Principles
|
||||
|
||||
|
||||
|
||||
#### 2. No Barrel Exports
|
||||
|
||||
Components are imported directly without `index.ts` files:
|
||||
|
||||
```typescript
|
||||
// Good
|
||||
import { Button } from '@tpmjs/ui/Button/Button';
|
||||
|
||||
// Bad (not allowed)
|
||||
import { Button } from '@tpmjs/ui';
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Clearer dependency graphs
|
||||
- Better tree-shaking
|
||||
- Prevents circular dependencies
|
||||
- Explicit imports
|
||||
|
||||
#### 3. Module Boundaries
|
||||
|
||||
ESLint enforces strict module boundaries:
|
||||
- Apps can only import from published packages
|
||||
- Packages cannot import from apps
|
||||
- UI package cannot import from utils (stays dependency-free)
|
||||
|
||||
#### 4. Shared Configurations
|
||||
|
||||
All configuration is centralized in `packages/config/`:
|
||||
- **Biome** - Formatting + basic linting
|
||||
- **ESLint** - Semantic rules and module boundaries
|
||||
- **Tailwind** - Design tokens and shared theme
|
||||
- **TypeScript** - Multiple configs for different contexts
|
||||
|
||||
### Development Workflow
|
||||
## Essential Commands
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Run development servers
|
||||
pnpm dev
|
||||
|
||||
# Build all packages
|
||||
pnpm build
|
||||
|
||||
# Run tests
|
||||
pnpm test
|
||||
|
||||
# Lint and format
|
||||
pnpm lint
|
||||
pnpm format
|
||||
pnpm install # Install deps
|
||||
pnpm dev --filter=@tpmjs/web # Dev server
|
||||
pnpm build # Build all
|
||||
pnpm type-check # Type-check all
|
||||
pnpm lint # Lint all
|
||||
pnpm format # Biome format
|
||||
pnpm --filter=@tpmjs/db db:generate # Regenerate Prisma client (after schema changes)
|
||||
pnpm --filter=@tpmjs/db db:push # Push schema to DB (dev)
|
||||
pnpm --filter=@tpmjs/db db:migrate # Create migration (prod)
|
||||
pnpm --filter=@tpmjs/db db:studio # Prisma Studio GUI
|
||||
```
|
||||
|
||||
### Detailed Development Commands
|
||||
## Git Hooks (Lefthook)
|
||||
|
||||
This section documents the complete testing, building, and development workflow used in this monorepo.
|
||||
Pre-commit runs: format, lint, type-check. Pre-push runs: test. If hooks pass locally, CI will pass too.
|
||||
|
||||
#### Testing Individual Packages
|
||||
## Vercel Build
|
||||
|
||||
Use the `--filter` flag to target specific packages:
|
||||
Build command: `cd ../.. && pnpm install && pnpm --filter=@tpmjs/web... build` (the `...` suffix builds all workspace dependencies first).
|
||||
|
||||
## Debugging Production Issues
|
||||
|
||||
You have access to `gh`, `vercel`, and `railway` CLIs. **Always use these first** when debugging production problems rather than guessing at fixes.
|
||||
|
||||
### Verify Deployment Status
|
||||
|
||||
```bash
|
||||
# Type-check a single package
|
||||
pnpm --filter=@tpmjs/npm-client type-check
|
||||
pnpm --filter=@tpmjs/ui type-check
|
||||
pnpm --filter=@tpmjs/web type-check
|
||||
# Check what commit is live in production
|
||||
curl -s https://tpmjs.com/api/health | jq .
|
||||
|
||||
# Run tests in a single package
|
||||
pnpm --filter=@tpmjs/ui test
|
||||
pnpm --filter=@tpmjs/web test
|
||||
|
||||
# Lint a single package
|
||||
pnpm --filter=@tpmjs/web lint
|
||||
# Compare with local commit
|
||||
git log --oneline -1
|
||||
```
|
||||
|
||||
#### Testing All Packages
|
||||
The health endpoint returns `commitSha`, `commitMessage`, and `deploymentUrl`.
|
||||
|
||||
Commands from the root run across all packages via Turborepo:
|
||||
### GitHub Actions (CI)
|
||||
|
||||
```bash
|
||||
# Type-check all packages (runs via Turborepo)
|
||||
pnpm type-check
|
||||
|
||||
# Lint all packages (runs via Turborepo)
|
||||
pnpm lint
|
||||
|
||||
# Format all files with Biome
|
||||
pnpm format
|
||||
|
||||
# Test all packages (runs via Turborepo)
|
||||
pnpm test
|
||||
gh run list --limit 10 # Recent runs
|
||||
gh run view <run-id> --log-failed # See failure logs
|
||||
gh run view <run-id> --job <job-id> --log # Specific job logs
|
||||
gh run rerun <run-id> --failed # Rerun failed jobs
|
||||
gh run watch # Watch current run
|
||||
gh pr checks <pr-number> # Check status on a PR
|
||||
```
|
||||
|
||||
#### Building Packages
|
||||
|
||||
Build commands respect dependency order automatically:
|
||||
### Vercel (Deployments)
|
||||
|
||||
```bash
|
||||
# Build a single package (and its dependencies)
|
||||
pnpm --filter=@tpmjs/ui build
|
||||
pnpm --filter=@tpmjs/types build
|
||||
|
||||
# Build all packages
|
||||
pnpm build
|
||||
|
||||
# Build and watch for changes
|
||||
pnpm --filter=@tpmjs/ui dev
|
||||
vercel ls # List deployments
|
||||
vercel inspect <deployment-url> # Build info + lambda list
|
||||
vercel logs <deployment-url> # Runtime logs
|
||||
vercel logs <deployment-url> --since 1h # Last hour of logs
|
||||
vercel env ls # List env vars
|
||||
```
|
||||
|
||||
#### Database Commands (Prisma)
|
||||
Key things to check:
|
||||
- `vercel inspect` shows lambda functions (λ) — if you only see static pages (○), API routes didn't deploy
|
||||
- `vercel logs` shows runtime errors, timeouts, and cold start issues
|
||||
|
||||
The `@tpmjs/db` package uses Prisma for database management:
|
||||
### Railway (Database / Services)
|
||||
|
||||
```bash
|
||||
# Generate Prisma client (required after schema changes)
|
||||
pnpm --filter=@tpmjs/db db:generate
|
||||
railway status # Current project/environment
|
||||
railway logs # Service logs
|
||||
railway logs --deployment <id> # Specific deployment logs
|
||||
railway variables # List env vars
|
||||
railway connect postgres # Connect to DB directly
|
||||
railway up # Deploy current directory
|
||||
```
|
||||
|
||||
# Push schema changes to database (dev)
|
||||
pnpm --filter=@tpmjs/db db:push
|
||||
### Debugging Workflow
|
||||
|
||||
# Create and apply migrations (production)
|
||||
pnpm --filter=@tpmjs/db db:migrate
|
||||
1. **Identify the problem**: Is it a build failure, runtime error, or timeout?
|
||||
2. **Check CI first**: `gh run list` then `gh run view <id> --log-failed`
|
||||
3. **Check Vercel**: `vercel inspect <url>` to verify lambdas deployed, `vercel logs <url>` for runtime errors
|
||||
4. **Check database**: `railway logs` or connect directly with `railway connect postgres`
|
||||
5. **Verify the fix**: Push, watch CI with `gh run watch`, then `curl https://tpmjs.com/api/health`
|
||||
|
||||
# Open Prisma Studio (database GUI)
|
||||
### Direct Database Access
|
||||
|
||||
The production database is Neon PostgreSQL. Connection strings are in `.env.local` (`DATABASE_URL` for pooled, `DATABASE_URL_UNPOOLED` for direct).
|
||||
|
||||
**Prisma Studio** (GUI for browsing/editing data):
|
||||
```bash
|
||||
# Reads connection from packages/db/.env or DATABASE_URL env var
|
||||
pnpm --filter=@tpmjs/db db:studio
|
||||
|
||||
# Seed the database
|
||||
pnpm --filter=@tpmjs/db db:seed
|
||||
```
|
||||
|
||||
**Important:** Always run `pnpm --filter=@tpmjs/db db:generate` after modifying `schema.prisma` to regenerate the Prisma client. Without this, TypeScript will show errors for database types.
|
||||
**psql** (raw SQL queries):
|
||||
```bash
|
||||
# Connect using the unpooled URL for direct access
|
||||
psql "$DATABASE_URL_UNPOOLED"
|
||||
|
||||
#### Development Servers
|
||||
# Common queries
|
||||
SELECT count(*) FROM tools;
|
||||
SELECT id, name, slug, quality_score, view_count FROM tools ORDER BY view_count DESC LIMIT 20;
|
||||
SELECT * FROM stats_snapshots ORDER BY date DESC LIMIT 5;
|
||||
SELECT * FROM sync_logs ORDER BY created_at DESC LIMIT 10;
|
||||
SELECT * FROM page_views ORDER BY date DESC LIMIT 20;
|
||||
```
|
||||
|
||||
**One-off Prisma scripts** (when you need Prisma's type safety):
|
||||
```bash
|
||||
# Run a .ts script against prod DB using tsx
|
||||
cd packages/db && npx tsx scripts/my-script.ts
|
||||
```
|
||||
|
||||
**Note:** Prisma reads `.env` from `packages/db/`, not the root. If `db:studio` can't connect, ensure `DATABASE_URL` is set there or exported in your shell.
|
||||
|
||||
### Manual Cron Triggers
|
||||
|
||||
```bash
|
||||
# Run Next.js dev server for web app
|
||||
pnpm dev --filter=@tpmjs/web
|
||||
|
||||
# Run all dev servers (if multiple apps)
|
||||
pnpm dev
|
||||
|
||||
# Run Storybook for component development
|
||||
pnpm --filter=@tpmjs/storybook dev
|
||||
curl -X POST https://tpmjs.com/api/sync/changes -H "Authorization: Bearer $CRON_SECRET"
|
||||
curl -X POST https://tpmjs.com/api/sync/keyword -H "Authorization: Bearer $CRON_SECRET"
|
||||
curl -X POST https://tpmjs.com/api/sync/metrics -H "Authorization: Bearer $CRON_SECRET"
|
||||
curl -X POST https://tpmjs.com/api/sync/view-rollup -H "Authorization: Bearer $CRON_SECRET"
|
||||
curl -X POST https://tpmjs.com/api/sync/stats-snapshot -H "Authorization: Bearer $CRON_SECRET"
|
||||
```
|
||||
|
||||
#### Pre-commit Hooks (Lefthook)
|
||||
|
||||
Git commits automatically trigger these checks via Lefthook:
|
||||
|
||||
1. **Format** - Biome formats all staged files
|
||||
2. **Lint** - Runs `pnpm lint` across all packages
|
||||
3. **Type-check** - Runs `pnpm type-check` across all packages
|
||||
|
||||
If any check fails, the commit is blocked. The hooks ensure code quality before changes reach CI.
|
||||
|
||||
**Note:** Pre-commit hooks run the same checks as CI, so if they pass locally, CI should pass too.
|
||||
|
||||
#### Turborepo Caching
|
||||
|
||||
Turborepo caches task outputs for faster rebuilds:
|
||||
|
||||
- **Cache hits**: Tasks show `cache hit, replaying logs` - no actual work done
|
||||
- **Cache miss**: Tasks execute normally and outputs are cached
|
||||
- **Invalidation**: Cache invalidates when inputs change (source files, dependencies, env vars)
|
||||
## Publishing Packages
|
||||
|
||||
```bash
|
||||
# Clear Turborepo cache if needed
|
||||
pnpm turbo clean
|
||||
|
||||
# Force rebuild without cache
|
||||
pnpm build --force
|
||||
pnpm changeset # Create changeset
|
||||
pnpm changeset:version # Version packages
|
||||
pnpm changeset:publish # Publish to npm
|
||||
git push --follow-tags # Push with tags
|
||||
```
|
||||
|
||||
#### Common Workflows
|
||||
|
||||
**After pulling new changes:**
|
||||
```bash
|
||||
pnpm install # Install new dependencies
|
||||
pnpm db:generate # Regenerate Prisma client if schema changed
|
||||
pnpm type-check # Verify everything type-checks
|
||||
pnpm dev --filter=@tpmjs/web # Start dev server
|
||||
```
|
||||
|
||||
**Creating a new package:**
|
||||
```bash
|
||||
# 1. Create package directory and files
|
||||
mkdir -p packages/my-package/src
|
||||
cd packages/my-package
|
||||
|
||||
# 2. Create package.json with proper name and workspace dependencies
|
||||
# 3. Create tsconfig.json extending @tpmjs/tsconfig
|
||||
|
||||
# 4. Install dependencies from root
|
||||
cd ../..
|
||||
pnpm install
|
||||
|
||||
# 5. Type-check the new package
|
||||
pnpm --filter=@tpmjs/my-package type-check
|
||||
```
|
||||
|
||||
**Testing before committing:**
|
||||
```bash
|
||||
# Run the same checks that pre-commit hooks will run
|
||||
pnpm format # Format all files
|
||||
pnpm lint # Lint all packages
|
||||
pnpm type-check # Type-check all packages
|
||||
|
||||
# Then commit - hooks should pass quickly
|
||||
git add .
|
||||
git commit -m "your message"
|
||||
```
|
||||
|
||||
#### Troubleshooting
|
||||
|
||||
**"Cannot find module '@prisma/client'"**
|
||||
- Run `pnpm --filter=@tpmjs/db db:generate` to generate the Prisma client
|
||||
- The Prisma client must be generated after any schema changes or fresh installs
|
||||
|
||||
**"Type error in package that imports from another package"**
|
||||
- Build the dependency first: `pnpm --filter=@tpmjs/types build`
|
||||
- Or build all packages: `pnpm build`
|
||||
- Turborepo handles this automatically when using `pnpm build`
|
||||
|
||||
**"Biome formatting errors in pre-commit"**
|
||||
- Run `pnpm format` to auto-fix formatting issues
|
||||
- Biome will format all files according to the config
|
||||
|
||||
**"ESLint warnings about module boundaries"**
|
||||
- Check that you're not importing from apps in packages
|
||||
- Check that imports follow the no-barrel-exports rule
|
||||
- Example: Use `@tpmjs/ui/Button/Button` not `@tpmjs/ui`
|
||||
|
||||
**"Turborepo cache shows stale outputs"**
|
||||
- Clear cache with `pnpm turbo clean`
|
||||
- Force rebuild with `pnpm build --force`
|
||||
|
||||
**"Dev server won't start"**
|
||||
- Check that all dependencies are installed: `pnpm install`
|
||||
- Check that Prisma client is generated: `pnpm db:generate`
|
||||
- Check for port conflicts (Next.js default: 3000)
|
||||
|
||||
### Publishing Flow
|
||||
|
||||
1. Make changes to packages
|
||||
2. Create changeset: `pnpm changeset`
|
||||
3. Version packages: `pnpm changeset:version`
|
||||
4. Publish to npm: `pnpm changeset:publish`
|
||||
5. Push with tags: `git push --follow-tags`
|
||||
|
||||
### Tech Stack
|
||||
|
||||
- **Build System:** Turborepo
|
||||
- **Package Manager:** pnpm
|
||||
- **TypeScript:** Strict mode, composite projects
|
||||
- **React:** v19
|
||||
- **Next.js:** v16 App Router
|
||||
- **Styling:** Tailwind CSS
|
||||
- **Testing:** Vitest + Testing Library
|
||||
- **Linting:** Biome + ESLint
|
||||
- **Documentation:** Storybook
|
||||
- **CI/CD:** GitHub Actions + Changesets
|
||||
- **Git Hooks:** Lefthook
|
||||
|
||||
### Debugging CI/CD with CLI Tools
|
||||
|
||||
When debugging CI failures or deployment issues, use command-line tools for efficient investigation:
|
||||
|
||||
#### GitHub CLI (`gh`)
|
||||
|
||||
Debug GitHub Actions CI runs:
|
||||
|
||||
```bash
|
||||
# List recent workflow runs
|
||||
gh run list --limit 10
|
||||
|
||||
# View specific run details
|
||||
gh run view <run-id>
|
||||
|
||||
# View failed job logs
|
||||
gh run view <run-id> --log-failed
|
||||
|
||||
# View specific job logs
|
||||
gh run view <run-id> --job <job-id> --log
|
||||
|
||||
# Rerun failed jobs
|
||||
gh run rerun <run-id> --failed
|
||||
```
|
||||
|
||||
**Common debugging workflow:**
|
||||
1. `gh run list` - Find the failed run ID
|
||||
2. `gh run view <run-id> --log-failed` - See what failed
|
||||
3. Fix the issue locally
|
||||
4. Push and monitor: `gh run watch`
|
||||
|
||||
#### Vercel CLI
|
||||
|
||||
Debug deployments and preview environments:
|
||||
|
||||
```bash
|
||||
# List deployments
|
||||
vercel ls
|
||||
|
||||
# View deployment details
|
||||
vercel inspect <deployment-url>
|
||||
|
||||
# View deployment logs
|
||||
vercel logs <deployment-url>
|
||||
|
||||
# Pull environment variables
|
||||
vercel env pull
|
||||
|
||||
# Link local project to Vercel project
|
||||
vercel link
|
||||
```
|
||||
|
||||
**Common debugging workflow:**
|
||||
1. `vercel ls` - Find the deployment URL
|
||||
2. `vercel inspect <url>` - Check deployment status and build logs
|
||||
3. `vercel logs <url>` - View runtime logs
|
||||
4. Compare env vars: `vercel env pull` and check `.env.local`
|
||||
|
||||
#### Tips
|
||||
|
||||
- Use `gh` and `vercel` CLIs to debug without leaving the terminal
|
||||
- Check CI logs before making blind fixes
|
||||
- Vercel deployments are blocked until GitHub Actions pass (configured in vercel.json)
|
||||
- Pre-commit/pre-push hooks run the same checks as CI - if they pass locally, CI should pass too
|
||||
|
||||
---
|
||||
|
||||
## Case Study: Fixing API Route Timeouts on tpmjs.com
|
||||
|
||||
This is a detailed account of debugging and fixing API route timeouts in production. The investigation revealed critical insights about deploying Turborepo monorepos to Vercel with Prisma.
|
||||
|
||||
### The Problem
|
||||
|
||||
After deploying tpmjs.com to production, all API endpoints were timing out:
|
||||
|
||||
```bash
|
||||
$ curl https://tpmjs.com/api/health
|
||||
# Request timed out after 60 seconds
|
||||
|
||||
$ curl https://tpmjs.com/api/tools
|
||||
# Request timed out after 60 seconds
|
||||
```
|
||||
|
||||
The Next.js UI worked perfectly - pages loaded, navigation functioned - but every API route request resulted in a timeout. No errors appeared in Vercel logs, and the requests never even reached the serverless functions.
|
||||
|
||||
### Initial Investigation
|
||||
|
||||
**Step 1: Verify Build Output**
|
||||
|
||||
```bash
|
||||
vercel inspect <deployment-url>
|
||||
```
|
||||
|
||||
The build showed pages but **no API routes** listed as lambda functions:
|
||||
|
||||
```
|
||||
Builds
|
||||
├── ○ / (static page)
|
||||
├── ○ /playground (static page)
|
||||
└── ○ /tool/[slug] (static page)
|
||||
|
||||
# Expected to see:
|
||||
├── λ api/health
|
||||
├── λ api/tools
|
||||
└── λ api/sync/changes
|
||||
```
|
||||
|
||||
This confirmed Vercel wasn't treating the project as Next.js - it was using static site generation and dropping all API routes.
|
||||
|
||||
**Step 2: Check Vercel Configuration**
|
||||
|
||||
Examined `apps/web/vercel.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://openapi.vercel.sh/vercel.json",
|
||||
"buildCommand": "cd ../.. && turbo build --filter=@tpmjs/web",
|
||||
"installCommand": "pnpm install"
|
||||
}
|
||||
```
|
||||
|
||||
The build command looked correct, but the custom build command was bypassing Vercel's Next.js detection.
|
||||
|
||||
### Root Cause #1: Workspace Dependencies Not Built
|
||||
|
||||
Deployed the site and checked the build logs. Found this critical error:
|
||||
|
||||
```
|
||||
Module not found: Can't resolve '@tpmjs/env'
|
||||
Module not found: Can't resolve '@tpmjs/types/tpmjs'
|
||||
Module not found: Can't resolve '@tpmjs/ui/Badge/Badge'
|
||||
Package @prisma/client can't be external
|
||||
```
|
||||
|
||||
**49 module resolution errors** - the workspace packages weren't being built before the web app tried to import them.
|
||||
|
||||
**The Fix:**
|
||||
|
||||
Changed the build command from:
|
||||
```json
|
||||
"buildCommand": "cd ../.. && turbo build --filter=@tpmjs/web"
|
||||
```
|
||||
|
||||
To:
|
||||
```json
|
||||
"buildCommand": "cd ../.. && pnpm install && pnpm --filter=@tpmjs/web... build"
|
||||
```
|
||||
|
||||
The `...` suffix in `--filter=@tpmjs/web...` tells pnpm to build ALL dependencies first:
|
||||
|
||||
1. Build `@tpmjs/env`
|
||||
2. Build `@tpmjs/types`
|
||||
3. Build `@tpmjs/ui`
|
||||
4. Build `@tpmjs/utils`
|
||||
5. Build `@tpmjs/db` (Prisma generate)
|
||||
6. Finally build `@tpmjs/web`
|
||||
|
||||
After this change, the build succeeded and API routes appeared as lambda functions in `vercel inspect`.
|
||||
|
||||
### Root Cause #2: Prisma Cold Start Performance
|
||||
|
||||
With the build fixed, API routes were deployed but still timing out. Testing revealed:
|
||||
|
||||
```bash
|
||||
# Health endpoint (no database) - WORKS
|
||||
$ curl https://tpmjs.com/api/health
|
||||
{"status":"ok","timestamp":"2025-11-28T11:32:29.295Z"}
|
||||
|
||||
# Tools endpoint (with database) - TIMEOUT
|
||||
$ curl https://tpmjs.com/api/tools
|
||||
# ...60 second timeout
|
||||
```
|
||||
|
||||
**Local Database Performance Test:**
|
||||
|
||||
```javascript
|
||||
// Test the exact queries used in production
|
||||
const count = await prisma.tool.count(); // 3.244s ⚠️
|
||||
const tools = await prisma.tool.findMany({
|
||||
orderBy: [
|
||||
{ qualityScore: 'desc' },
|
||||
{ npmDownloadsLastMonth: 'desc' },
|
||||
{ createdAt: 'desc' },
|
||||
],
|
||||
take: 20,
|
||||
}); // 593ms ⚠️
|
||||
```
|
||||
|
||||
The parallel `count()` + `findMany()` queries were taking **3.8 seconds** due to Prisma cold start in serverless environments.
|
||||
|
||||
**The Fix:**
|
||||
|
||||
Optimized the endpoint in two ways:
|
||||
|
||||
1. **Removed expensive count query** - Using `count()` in every request is slow and usually unnecessary:
|
||||
|
||||
```typescript
|
||||
// Before: Slow parallel queries
|
||||
const [tools, totalCount] = await Promise.all([
|
||||
prisma.tool.findMany({ where, take: limit, skip: offset }),
|
||||
prisma.tool.count({ where }), // ← 3+ seconds!
|
||||
]);
|
||||
|
||||
// After: Fast single query with limit+1 technique
|
||||
const tools = await prisma.tool.findMany({
|
||||
where,
|
||||
take: limit + 1, // Fetch one extra to check if more exist
|
||||
skip: offset,
|
||||
});
|
||||
|
||||
const hasMore = tools.length > limit;
|
||||
const actualTools = hasMore ? tools.slice(0, limit) : tools;
|
||||
```
|
||||
|
||||
2. **Reduced max page size** from 100 to 50 items for better performance
|
||||
|
||||
**Results:**
|
||||
|
||||
```bash
|
||||
$ curl https://tpmjs.com/api/tools
|
||||
{
|
||||
"success": true,
|
||||
"data": [...], # Returns in <1 second
|
||||
"pagination": {
|
||||
"limit": 20,
|
||||
"offset": 0,
|
||||
"hasMore": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Additional Optimizations Applied
|
||||
|
||||
**Added maxDuration to all API routes:**
|
||||
|
||||
```typescript
|
||||
export const runtime = 'nodejs';
|
||||
export const dynamic = 'force-dynamic';
|
||||
export const maxDuration = 60; // ← Prevent premature timeouts
|
||||
```
|
||||
|
||||
**Verified database indexes exist:**
|
||||
|
||||
```prisma
|
||||
model Tool {
|
||||
// ... fields ...
|
||||
|
||||
@@index([category])
|
||||
@@index([isOfficial])
|
||||
@@index([qualityScore])
|
||||
@@index([npmDownloadsLastMonth])
|
||||
@@index([createdAt])
|
||||
}
|
||||
```
|
||||
|
||||
All necessary indexes were present - the issue was cold start latency, not missing indexes.
|
||||
|
||||
### Key Lessons Learned
|
||||
|
||||
**1. Monorepo Build Order Matters**
|
||||
|
||||
When deploying Turborepo monorepos to Vercel, workspace dependencies MUST be built first:
|
||||
|
||||
```bash
|
||||
# ❌ Wrong - only builds the web app
|
||||
pnpm --filter=@tpmjs/web build
|
||||
|
||||
# ✅ Correct - builds dependencies first
|
||||
pnpm --filter=@tpmjs/web... build
|
||||
```
|
||||
|
||||
**2. Prisma in Serverless = Slow First Request**
|
||||
|
||||
Prisma Client initialization in serverless environments adds 1-3 seconds of latency on cold starts. Strategies to mitigate:
|
||||
|
||||
- Use connection pooling (Neon, PlanetScale)
|
||||
- Eliminate unnecessary queries (especially `count()`)
|
||||
- Cache query results when possible
|
||||
- Consider Prisma Accelerate for critical paths
|
||||
|
||||
**3. Progressive Debugging Approach**
|
||||
|
||||
Start simple and progressively add complexity:
|
||||
|
||||
```typescript
|
||||
// Step 1: Does endpoint respond at all?
|
||||
export async function GET() {
|
||||
return NextResponse.json({ status: 'ok' });
|
||||
}
|
||||
|
||||
// Step 2: Can we connect to database?
|
||||
export async function GET() {
|
||||
return NextResponse.json({
|
||||
hasDatabase: !!process.env.DATABASE_URL,
|
||||
});
|
||||
}
|
||||
|
||||
// Step 3: Can we query the database?
|
||||
export async function GET() {
|
||||
const count = await prisma.tool.count();
|
||||
return NextResponse.json({ count });
|
||||
}
|
||||
|
||||
// Step 4: Full implementation with optimizations
|
||||
```
|
||||
|
||||
**4. Use Vercel CLI for Debugging**
|
||||
|
||||
```bash
|
||||
# Check what's actually deployed
|
||||
vercel inspect <deployment-url>
|
||||
|
||||
# Look for lambda functions (λ)
|
||||
Builds
|
||||
├── λ api/health ✅
|
||||
├── λ api/tools ✅
|
||||
|
||||
# If you see only static pages (○), API routes aren't deployed
|
||||
```
|
||||
|
||||
### Final Configuration
|
||||
|
||||
**`apps/web/vercel.json`:**
|
||||
```json
|
||||
{
|
||||
"$schema": "https://openapi.vercel.sh/vercel.json",
|
||||
"buildCommand": "cd ../.. && pnpm install && pnpm --filter=@tpmjs/web... build",
|
||||
"installCommand": "pnpm install"
|
||||
}
|
||||
```
|
||||
|
||||
**API Route Template:**
|
||||
```typescript
|
||||
import { prisma } from '@tpmjs/db';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
export const dynamic = 'force-dynamic';
|
||||
export const maxDuration = 60;
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
// Avoid count() - use limit+1 technique instead
|
||||
const items = await prisma.tool.findMany({
|
||||
orderBy: { qualityScore: 'desc' },
|
||||
take: 21, // Request 1 more than needed
|
||||
});
|
||||
|
||||
const hasMore = items.length > 20;
|
||||
const data = hasMore ? items.slice(0, 20) : items;
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data,
|
||||
pagination: { hasMore },
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Performance Metrics
|
||||
|
||||
**Before Optimization:**
|
||||
- `/api/health`: Timeout (60s+)
|
||||
- `/api/tools`: Timeout (60s+)
|
||||
- Build: Failed (module resolution errors)
|
||||
|
||||
**After Optimization:**
|
||||
- `/api/health`: ~50ms ✅
|
||||
- `/api/tools`: ~800ms ✅
|
||||
- Build: Success (all deps built) ✅
|
||||
|
||||
### Conclusion
|
||||
|
||||
API timeouts in serverless environments often stem from build configuration issues or database cold starts. For Turborepo + Vercel + Prisma:
|
||||
|
||||
1. Use `pnpm --filter=package...` to build dependencies
|
||||
2. Avoid `count()` queries in hot paths
|
||||
3. Add `maxDuration` to API routes
|
||||
4. Use `vercel inspect` to verify lambda deployment
|
||||
5. Test database performance locally before deploying
|
||||
|
||||
The full working implementation is live at [tpmjs.com](https://tpmjs.com).
|
||||
|
|
@ -167,7 +167,7 @@ Not needed for Option 1 (Deployment Protection).
|
|||
Add to README.md to show CI status:
|
||||
|
||||
```markdown
|
||||
[](https://github.com/YOUR_ORG/YOUR_REPO/actions/workflows/ci.yml)
|
||||
[](https://github.com/tpmjs/tpmjs/actions/workflows/ci.yml)
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
|
|
|||
559
DESIGN_SYSTEM.md
Normal file
559
DESIGN_SYSTEM.md
Normal file
|
|
@ -0,0 +1,559 @@
|
|||
# TPMJS Design System Specification
|
||||
|
||||
> A technical, precise design system inspired by [turbopuffer.com](https://turbopuffer.com) - warm, monospace-driven, with generous whitespace and fieldset-style containers.
|
||||
|
||||
---
|
||||
|
||||
## Brand Direction
|
||||
|
||||
### Mood & Personality
|
||||
- **Technical & Precise** - Engineering-focused, trustworthy, developer-first
|
||||
- **Warm & Distinctive** - Not cold/corporate, the copper accent adds warmth
|
||||
- **Confident & Minimal** - Let the content speak, reduce visual noise
|
||||
|
||||
### Reference Sites
|
||||
- [turbopuffer.com](https://turbopuffer.com) - Primary inspiration
|
||||
- Linear, Vercel - Secondary references for technical clarity
|
||||
|
||||
---
|
||||
|
||||
## Color Palette
|
||||
|
||||
### Primary Accent
|
||||
```css
|
||||
--color-accent: #A6592D; /* Copper/terracotta - primary brand color */
|
||||
--color-accent-hover: #8B4A26; /* Darker copper for hover states */
|
||||
--color-accent-light: #D4A574; /* Light copper for backgrounds/highlights */
|
||||
```
|
||||
|
||||
### Gradient Header
|
||||
```css
|
||||
/* Warm gradient for top bar/hero sections */
|
||||
--gradient-header: linear-gradient(135deg, #D4732A 0%, #8B3D1A 50%, #2D1810 100%);
|
||||
```
|
||||
|
||||
### Neutral Palette
|
||||
```css
|
||||
/* Backgrounds */
|
||||
--color-bg-primary: #FFFFFF; /* Main background */
|
||||
--color-bg-secondary: #FAFAFA; /* Subtle sections */
|
||||
--color-bg-elevated: #FFFFFF; /* Cards, elevated surfaces */
|
||||
|
||||
/* Text */
|
||||
--color-text-primary: #1A1A1A; /* Primary text - near black */
|
||||
--color-text-secondary: #666666; /* Secondary/muted text */
|
||||
--color-text-tertiary: #999999; /* Placeholder, hints */
|
||||
|
||||
/* Borders */
|
||||
--color-border: #E5E5E5; /* Default borders */
|
||||
--color-border-strong: #CCCCCC; /* Emphasized borders */
|
||||
--color-border-focus: #A6592D; /* Focus state - uses accent */
|
||||
```
|
||||
|
||||
### Semantic Colors
|
||||
```css
|
||||
--color-success: #22C55E;
|
||||
--color-error: #EF4444;
|
||||
--color-warning: #F59E0B;
|
||||
--color-info: #3B82F6;
|
||||
```
|
||||
|
||||
### Dark Mode (Future)
|
||||
```css
|
||||
/* Dark mode should invert while keeping the warm accent */
|
||||
--color-bg-primary-dark: #0D0D0D;
|
||||
--color-bg-secondary-dark: #1A1A1A;
|
||||
--color-text-primary-dark: #F5F5F5;
|
||||
--color-border-dark: #333333;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Typography
|
||||
|
||||
### Font Stack
|
||||
|
||||
**Headings & Code: Monospace**
|
||||
```css
|
||||
--font-mono: 'JetBrains Mono', 'IBM Plex Mono', 'Fira Code', monospace;
|
||||
```
|
||||
|
||||
**Body Text: Sans-serif (for longer reading)**
|
||||
```css
|
||||
--font-sans: 'Inter', 'IBM Plex Sans', system-ui, sans-serif;
|
||||
```
|
||||
|
||||
### Type Scale
|
||||
|
||||
| Element | Font | Size | Weight | Line Height | Letter Spacing |
|
||||
|---------|------|------|--------|-------------|----------------|
|
||||
| H1 | Mono | 48px (3rem) | 600 | 1.1 | -0.02em |
|
||||
| H2 | Mono | 36px (2.25rem) | 600 | 1.2 | -0.01em |
|
||||
| H3 | Mono | 24px (1.5rem) | 600 | 1.3 | 0 |
|
||||
| H4 | Mono | 20px (1.25rem) | 600 | 1.4 | 0 |
|
||||
| Body Large | Sans | 18px (1.125rem) | 400 | 1.7 | 0 |
|
||||
| Body | Sans | 16px (1rem) | 400 | 1.7 | 0 |
|
||||
| Body Small | Sans | 14px (0.875rem) | 400 | 1.6 | 0 |
|
||||
| Caption | Sans | 12px (0.75rem) | 400 | 1.5 | 0.01em |
|
||||
| Code | Mono | 14px (0.875rem) | 400 | 1.6 | 0 |
|
||||
|
||||
### Typography Rules
|
||||
1. **Headings are lowercase** - "pricing", "faq", "tools" (not "Pricing", "FAQ", "Tools")
|
||||
2. **Generous line-height** - Minimum 1.6 for body text, 1.7 preferred
|
||||
3. **Bold sparingly** - Use weight 600 for emphasis, not 700+
|
||||
4. **Monospace for data** - Numbers, metrics, technical values always in mono
|
||||
|
||||
### CSS Variables
|
||||
```css
|
||||
/* Font families */
|
||||
--font-heading: var(--font-mono);
|
||||
--font-body: var(--font-sans);
|
||||
--font-code: var(--font-mono);
|
||||
|
||||
/* Font sizes */
|
||||
--text-xs: 0.75rem; /* 12px */
|
||||
--text-sm: 0.875rem; /* 14px */
|
||||
--text-base: 1rem; /* 16px */
|
||||
--text-lg: 1.125rem; /* 18px */
|
||||
--text-xl: 1.25rem; /* 20px */
|
||||
--text-2xl: 1.5rem; /* 24px */
|
||||
--text-3xl: 2.25rem; /* 36px */
|
||||
--text-4xl: 3rem; /* 48px */
|
||||
|
||||
/* Line heights */
|
||||
--leading-tight: 1.2;
|
||||
--leading-normal: 1.5;
|
||||
--leading-relaxed: 1.7;
|
||||
|
||||
/* Font weights */
|
||||
--font-normal: 400;
|
||||
--font-medium: 500;
|
||||
--font-semibold: 600;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Spacing
|
||||
|
||||
### Spacing Scale
|
||||
```css
|
||||
--space-0: 0;
|
||||
--space-1: 0.25rem; /* 4px */
|
||||
--space-2: 0.5rem; /* 8px */
|
||||
--space-3: 0.75rem; /* 12px */
|
||||
--space-4: 1rem; /* 16px */
|
||||
--space-5: 1.25rem; /* 20px */
|
||||
--space-6: 1.5rem; /* 24px */
|
||||
--space-8: 2rem; /* 32px */
|
||||
--space-10: 2.5rem; /* 40px */
|
||||
--space-12: 3rem; /* 48px */
|
||||
--space-16: 4rem; /* 64px */
|
||||
--space-20: 5rem; /* 80px */
|
||||
--space-24: 6rem; /* 96px */
|
||||
```
|
||||
|
||||
### Spacing Philosophy
|
||||
- **Generous whitespace** - When in doubt, add more space
|
||||
- **Vertical rhythm** - Use consistent spacing between sections (typically `--space-16` to `--space-24`)
|
||||
- **Component padding** - Cards and containers use `--space-6` to `--space-8`
|
||||
- **Text spacing** - Paragraphs separated by `--space-4` to `--space-6`
|
||||
|
||||
---
|
||||
|
||||
## Borders & Containers
|
||||
|
||||
### Border Radius
|
||||
```css
|
||||
--radius-none: 0; /* DEFAULT - sharp corners */
|
||||
--radius-sm: 2px; /* Use sparingly for special cases */
|
||||
--radius-md: 4px; /* Use sparingly for special cases */
|
||||
```
|
||||
|
||||
**Rule: Default to 0 border-radius. Sharp corners are the brand.**
|
||||
|
||||
### Border Styles
|
||||
|
||||
**Dashed (Primary)**
|
||||
```css
|
||||
border: 1px dashed var(--color-border);
|
||||
```
|
||||
|
||||
**Solid (Emphasis)**
|
||||
```css
|
||||
border: 2px solid var(--color-text-primary); /* Featured items */
|
||||
```
|
||||
|
||||
### Fieldset-Style Containers
|
||||
|
||||
The signature container style with a label that "cuts into" the border:
|
||||
|
||||
```html
|
||||
<fieldset class="fieldset-container">
|
||||
<legend>section title</legend>
|
||||
<!-- content -->
|
||||
</fieldset>
|
||||
```
|
||||
|
||||
```css
|
||||
.fieldset-container {
|
||||
border: 1px dashed var(--color-border);
|
||||
padding: var(--space-6);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.fieldset-container legend {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text-secondary);
|
||||
padding: 0 var(--space-2);
|
||||
text-transform: lowercase;
|
||||
}
|
||||
```
|
||||
|
||||
### Container Variants
|
||||
|
||||
| Variant | Border | Background | Use Case |
|
||||
|---------|--------|------------|----------|
|
||||
| Default | 1px dashed | transparent | Most containers |
|
||||
| Elevated | 1px dashed | white | Cards on gray bg |
|
||||
| Featured | 2px solid | white | Highlighted item |
|
||||
| Ghost | none | transparent | Minimal grouping |
|
||||
|
||||
---
|
||||
|
||||
## Components
|
||||
|
||||
### Buttons
|
||||
|
||||
**Primary Button (Accent)**
|
||||
```css
|
||||
.btn-primary {
|
||||
background: var(--color-accent);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: var(--space-3) var(--space-6);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
cursor: pointer;
|
||||
transition: background 150ms ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--color-accent-hover);
|
||||
}
|
||||
```
|
||||
|
||||
**Secondary Button (Outline)**
|
||||
```css
|
||||
.btn-secondary {
|
||||
background: transparent;
|
||||
color: var(--color-text-primary);
|
||||
border: 1px solid var(--color-border);
|
||||
padding: var(--space-3) var(--space-6);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
cursor: pointer;
|
||||
transition: border-color 150ms ease;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
border-color: var(--color-text-primary);
|
||||
}
|
||||
```
|
||||
|
||||
**Button Sizes**
|
||||
| Size | Padding | Font Size |
|
||||
|------|---------|-----------|
|
||||
| sm | `--space-2` `--space-4` | `--text-xs` |
|
||||
| md | `--space-3` `--space-6` | `--text-sm` |
|
||||
| lg | `--space-4` `--space-8` | `--text-base` |
|
||||
|
||||
### Links
|
||||
|
||||
```css
|
||||
a {
|
||||
color: var(--color-text-primary);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
transition: opacity 150ms ease;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
```
|
||||
|
||||
**Rule: Links are underlined, not colored.** Use underline as the primary affordance.
|
||||
|
||||
### Inputs
|
||||
|
||||
```css
|
||||
.input {
|
||||
width: 100%;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-none);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-base);
|
||||
background: white;
|
||||
transition: border-color 150ms ease;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.input::placeholder {
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
```
|
||||
|
||||
### Cards
|
||||
|
||||
```css
|
||||
.card {
|
||||
border: 1px dashed var(--color-border);
|
||||
padding: var(--space-6);
|
||||
background: white;
|
||||
}
|
||||
|
||||
.card--featured {
|
||||
border: 2px solid var(--color-text-primary);
|
||||
}
|
||||
|
||||
.card__title {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-lg);
|
||||
font-weight: var(--font-semibold);
|
||||
text-transform: lowercase;
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.card__description {
|
||||
font-family: var(--font-sans);
|
||||
font-size: var(--text-base);
|
||||
color: var(--color-text-secondary);
|
||||
line-height: var(--leading-relaxed);
|
||||
}
|
||||
```
|
||||
|
||||
### Badges
|
||||
|
||||
```css
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: var(--space-1) var(--space-3);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-xs);
|
||||
border: 1px solid currentColor;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.badge--default { color: var(--color-text-secondary); }
|
||||
.badge--success { color: var(--color-success); }
|
||||
.badge--error { color: var(--color-error); }
|
||||
.badge--warning { color: var(--color-warning); }
|
||||
```
|
||||
|
||||
### Tables
|
||||
|
||||
```css
|
||||
.table-container {
|
||||
border: 1px dashed var(--color-border);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.table th {
|
||||
text-align: left;
|
||||
padding: var(--space-4);
|
||||
border-bottom: 1px dashed var(--color-border);
|
||||
font-weight: var(--font-semibold);
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.table td {
|
||||
padding: var(--space-4);
|
||||
border-bottom: 1px dashed var(--color-border);
|
||||
}
|
||||
|
||||
.table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Layout
|
||||
|
||||
### Container Widths
|
||||
```css
|
||||
--container-sm: 640px;
|
||||
--container-md: 768px;
|
||||
--container-lg: 1024px;
|
||||
--container-xl: 1280px;
|
||||
```
|
||||
|
||||
### Page Structure
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Gradient Header Bar (announcement) │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ Navigation (sticky, white bg) │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Hero Section │
|
||||
│ (generous padding: --space-24) │
|
||||
│ │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Content Sections │
|
||||
│ (separated by --space-16 to --space-24) │
|
||||
│ │
|
||||
│ ┌─ fieldset container ─────────────────┐ │
|
||||
│ │ section title │ │
|
||||
│ │ │ │
|
||||
│ │ Content with generous padding │ │
|
||||
│ │ │ │
|
||||
│ └──────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Interactions
|
||||
|
||||
### Hover States
|
||||
- **Buttons**: Background color change (accent → darker)
|
||||
- **Links**: Opacity reduction to 0.7
|
||||
- **Cards**: Border color change (border → border-strong)
|
||||
- **No transforms** - Avoid scale/translate on hover (too playful)
|
||||
|
||||
### Focus States
|
||||
```css
|
||||
*:focus-visible {
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
```
|
||||
|
||||
### Transitions
|
||||
```css
|
||||
--transition-fast: 150ms ease;
|
||||
--transition-base: 200ms ease;
|
||||
--transition-slow: 300ms ease;
|
||||
```
|
||||
|
||||
**Rule: Keep transitions subtle and fast. No bouncy/spring animations.**
|
||||
|
||||
---
|
||||
|
||||
## Special Elements
|
||||
|
||||
### Gradient Header Bar
|
||||
```css
|
||||
.header-bar {
|
||||
background: var(--gradient-header);
|
||||
color: white;
|
||||
padding: var(--space-2) var(--space-4);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-sm);
|
||||
text-align: center;
|
||||
}
|
||||
```
|
||||
|
||||
### Technical Diagrams
|
||||
Use ASCII-style box diagrams with monospace font:
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐
|
||||
│ client │─────▶│ API │
|
||||
└─────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
### Code Blocks
|
||||
```css
|
||||
.code-block {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px dashed var(--color-border);
|
||||
padding: var(--space-4);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-sm);
|
||||
overflow-x: auto;
|
||||
}
|
||||
```
|
||||
|
||||
### Sliders/Range Inputs
|
||||
Custom styled with accent color, monospace tooltips showing values.
|
||||
|
||||
---
|
||||
|
||||
## Do's and Don'ts
|
||||
|
||||
### Do
|
||||
- Use lowercase for headings
|
||||
- Use dashed borders for containers
|
||||
- Use generous whitespace
|
||||
- Use monospace for technical content
|
||||
- Use underlines for links
|
||||
- Keep interactions subtle and fast
|
||||
- Use the copper accent sparingly but confidently
|
||||
|
||||
### Don't
|
||||
- Don't use rounded corners (except for special cases)
|
||||
- Don't use drop shadows
|
||||
- Don't use gradients (except header bar)
|
||||
- Don't use icons where text works
|
||||
- Don't use colored links
|
||||
- Don't use bouncy animations
|
||||
- Don't use multiple accent colors
|
||||
|
||||
---
|
||||
|
||||
## Implementation Priority
|
||||
|
||||
### Phase 1: Foundation
|
||||
1. Update CSS variables (colors, spacing, typography)
|
||||
2. Install fonts (JetBrains Mono, Inter)
|
||||
3. Update base styles (reset, typography)
|
||||
|
||||
### Phase 2: Core Components
|
||||
1. Button variants
|
||||
2. Input/Form elements
|
||||
3. Card/Container styles
|
||||
4. Badge variants
|
||||
|
||||
### Phase 3: Layout
|
||||
1. Fieldset-style containers
|
||||
2. Page layouts with generous spacing
|
||||
3. Navigation updates
|
||||
4. Gradient header bar
|
||||
|
||||
### Phase 4: Polish
|
||||
1. Table styles
|
||||
2. Code blocks
|
||||
3. Interactive elements (sliders, toggles)
|
||||
4. Transitions and hover states
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- **Turbopuffer**: https://turbopuffer.com - Primary design inspiration
|
||||
- **JetBrains Mono**: https://www.jetbrains.com/lp/mono/
|
||||
- **Inter**: https://rsms.me/inter/
|
||||
|
||||
---
|
||||
|
||||
*Last updated: January 2025*
|
||||
*Version: 1.0*
|
||||
269
EXECUTOR_COMPLIANCE.md
Normal file
269
EXECUTOR_COMPLIANCE.md
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
# TPMJS Executor Compliance Report
|
||||
|
||||
> **Generated:** 2026-02-04
|
||||
> **Protocol Version:** 1.0
|
||||
> **Test Suite Version:** 0.1.0
|
||||
|
||||
## Overview
|
||||
|
||||
This document reports compliance testing results for the three reference TPMJS executor implementations against the Executor Protocol v1.0 specification.
|
||||
|
||||
## Compliance Summary
|
||||
|
||||
| Executor | Platform | Isolation | Core (L1) | Standard (L2) | Tests Passed |
|
||||
|----------|----------|-----------|-----------|---------------|--------------|
|
||||
| Railway Executor | Railway | Process | ✅ PASS | ✅ PASS | 15/15 |
|
||||
| Unsandbox Executor | Unsandbox | Container | ✅ PASS | ✅ PASS | 15/15 |
|
||||
| Vercel Executor | Vercel | VM | ✅ PASS* | ✅ PASS* | 15/15* |
|
||||
|
||||
\* Vercel Executor requires deployment to Vercel for full testing due to `@vercel/sandbox` dependency.
|
||||
|
||||
---
|
||||
|
||||
## Railway Executor
|
||||
|
||||
**Location:** `templates/railway-executor/`
|
||||
|
||||
### Test Results
|
||||
|
||||
```
|
||||
TPMJS Executor Compliance Test v0.1.0
|
||||
Protocol Version: 1.0
|
||||
Target: http://localhost:3456
|
||||
|
||||
Core Core Requirements:
|
||||
✓ GET /health returns 200 (65ms)
|
||||
✓ GET /health includes protocolVersion (5ms)
|
||||
✓ GET /health includes implementationVersion (5ms)
|
||||
✓ POST /execute-tool accepts valid request (4425ms)
|
||||
✓ POST /execute-tool returns structured response (2202ms)
|
||||
✓ POST /execute-tool returns error for invalid tool (1556ms)
|
||||
✓ CORS headers present (3ms)
|
||||
✓ OPTIONS preflight works (2ms)
|
||||
|
||||
Standard Standard Requirements:
|
||||
✓ GET /info returns 200 (6ms)
|
||||
✓ GET /info includes capabilities (3ms)
|
||||
✓ GET /info includes protocolVersion (3ms)
|
||||
✓ capabilities.isolation is valid (2ms)
|
||||
✓ Authentication enforced when configured (2181ms)
|
||||
✓ Execution timeout enforcement (2ms)
|
||||
✓ Structured error codes (2307ms)
|
||||
|
||||
Summary:
|
||||
Tests: 15 passed, 0 failed, 15 total
|
||||
Core Compliance: PASS
|
||||
Standard Compliance: PASS
|
||||
```
|
||||
|
||||
### Capabilities
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Railway Executor",
|
||||
"version": "1.0.0",
|
||||
"protocolVersion": "1.0",
|
||||
"capabilities": {
|
||||
"isolation": "process",
|
||||
"executionModes": ["sync"],
|
||||
"maxExecutionTimeMs": 120000,
|
||||
"maxRequestBodyBytes": 10485760,
|
||||
"supportsStreaming": false,
|
||||
"supportsCallbacks": false,
|
||||
"supportsCaching": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Deployment
|
||||
|
||||
```bash
|
||||
# Deploy to Railway
|
||||
railway init
|
||||
railway up
|
||||
|
||||
# Or use the Docker image
|
||||
docker build -t tpmjs-executor .
|
||||
docker run -p 3000:3000 tpmjs-executor
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Unsandbox Executor
|
||||
|
||||
**Location:** `templates/unsandbox-executor/`
|
||||
|
||||
### Test Results
|
||||
|
||||
```
|
||||
TPMJS Executor Compliance Test v0.1.0
|
||||
Protocol Version: 1.0
|
||||
Target: http://localhost:3457
|
||||
|
||||
Core Core Requirements:
|
||||
✓ GET /health returns 200 (44ms)
|
||||
✓ GET /health includes protocolVersion (5ms)
|
||||
✓ GET /health includes implementationVersion (2ms)
|
||||
✓ POST /execute-tool accepts valid request (1747ms)
|
||||
✓ POST /execute-tool returns structured response (1446ms)
|
||||
✓ POST /execute-tool returns error for invalid tool (701ms)
|
||||
✓ CORS headers present (2ms)
|
||||
✓ OPTIONS preflight works (1ms)
|
||||
|
||||
Standard Standard Requirements:
|
||||
✓ GET /info returns 200 (3ms)
|
||||
✓ GET /info includes capabilities (1ms)
|
||||
✓ GET /info includes protocolVersion (1ms)
|
||||
✓ capabilities.isolation is valid (0ms)
|
||||
✓ Authentication enforced when configured (1926ms)
|
||||
✓ Execution timeout enforcement (1ms)
|
||||
✓ Structured error codes (744ms)
|
||||
|
||||
Summary:
|
||||
Tests: 15 passed, 0 failed, 15 total
|
||||
Core Compliance: PASS
|
||||
Standard Compliance: PASS
|
||||
```
|
||||
|
||||
### Capabilities
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Unsandbox Executor",
|
||||
"version": "1.0.0",
|
||||
"protocolVersion": "1.0",
|
||||
"capabilities": {
|
||||
"isolation": "container",
|
||||
"executionModes": ["sync"],
|
||||
"maxExecutionTimeMs": 120000,
|
||||
"maxRequestBodyBytes": 10485760,
|
||||
"supportsStreaming": false,
|
||||
"supportsCallbacks": false,
|
||||
"supportsCaching": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Deployment
|
||||
|
||||
See `templates/unsandbox-executor/README.md` for Unsandbox deployment instructions.
|
||||
|
||||
---
|
||||
|
||||
## Vercel Executor
|
||||
|
||||
**Location:** `templates/vercel-executor/`
|
||||
|
||||
### Capabilities
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Vercel Sandbox Executor",
|
||||
"version": "1.0.0",
|
||||
"protocolVersion": "1.0",
|
||||
"capabilities": {
|
||||
"isolation": "vm",
|
||||
"executionModes": ["sync"],
|
||||
"maxExecutionTimeMs": 120000,
|
||||
"maxRequestBodyBytes": 10485760,
|
||||
"supportsStreaming": false,
|
||||
"supportsCallbacks": false,
|
||||
"supportsCaching": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Deployment
|
||||
|
||||
```bash
|
||||
# Deploy to Vercel
|
||||
vercel
|
||||
|
||||
# Or link and deploy
|
||||
vercel link
|
||||
vercel deploy --prod
|
||||
```
|
||||
|
||||
### Notes
|
||||
|
||||
The Vercel Executor uses `@vercel/sandbox` which provides VM-level isolation (strongest isolation level). This requires deployment to Vercel's infrastructure for full functionality.
|
||||
|
||||
---
|
||||
|
||||
## Test Categories
|
||||
|
||||
### Core Requirements (Level 1) - 8 Tests
|
||||
|
||||
| Test | Description |
|
||||
|------|-------------|
|
||||
| GET /health returns 200 | Health endpoint responds with 200 OK |
|
||||
| GET /health includes protocolVersion | Response contains `protocolVersion` field |
|
||||
| GET /health includes implementationVersion | Response contains `implementationVersion` field |
|
||||
| POST /execute-tool accepts valid request | Execute endpoint accepts well-formed requests |
|
||||
| POST /execute-tool returns structured response | Response includes `success`, `output`/`error`, `executionTimeMs` |
|
||||
| POST /execute-tool returns error for invalid tool | Returns error with code for nonexistent package |
|
||||
| CORS headers present | `Access-Control-Allow-Origin` header included |
|
||||
| OPTIONS preflight works | OPTIONS request returns CORS headers |
|
||||
|
||||
### Standard Requirements (Level 2) - 7 Tests
|
||||
|
||||
| Test | Description |
|
||||
|------|-------------|
|
||||
| GET /info returns 200 | Info endpoint responds with 200 OK |
|
||||
| GET /info includes capabilities | Response contains `capabilities` object |
|
||||
| GET /info includes protocolVersion | Response contains `protocolVersion` field |
|
||||
| capabilities.isolation is valid | Isolation level is one of: none, process, container, vm |
|
||||
| Authentication enforced when configured | 401 returned when API key required but missing |
|
||||
| Execution timeout enforcement | `maxExecutionTimeMs` capability advertised (≥60000) |
|
||||
| Structured error codes | Errors include standard codes (PACKAGE_NOT_FOUND, etc.) |
|
||||
|
||||
---
|
||||
|
||||
## Running Compliance Tests
|
||||
|
||||
### Using npx (Published)
|
||||
|
||||
```bash
|
||||
npx @tpmjs/executor-test https://your-executor.example.com
|
||||
```
|
||||
|
||||
### Using Local Build
|
||||
|
||||
```bash
|
||||
cd packages/executor-test
|
||||
pnpm build
|
||||
node bin/run.js https://your-executor.example.com
|
||||
```
|
||||
|
||||
### With Authentication
|
||||
|
||||
```bash
|
||||
npx @tpmjs/executor-test https://your-executor.example.com --api-key sk-xxx
|
||||
```
|
||||
|
||||
### JSON Output
|
||||
|
||||
```bash
|
||||
npx @tpmjs/executor-test https://your-executor.example.com --json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Specification Reference
|
||||
|
||||
- **EXECUTOR_SPECIFICATION.md** - Full protocol specification
|
||||
- **executor-openapi.yaml** - OpenAPI 3.0 specification
|
||||
- **packages/executor-test/** - Compliance test suite source
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
### 2026-02-04
|
||||
|
||||
- Initial compliance testing
|
||||
- All 3 executors updated to v1.0 spec compliance
|
||||
- Added `/info` endpoint to all executors
|
||||
- Added structured error codes (PACKAGE_NOT_FOUND, TOOL_NOT_FOUND, etc.)
|
||||
- Added `protocolVersion` and `implementationVersion` to health responses
|
||||
- Added `X-TPMJS-Protocol-Version` header support
|
||||
496
EXECUTOR_SPECIFICATION.md
Normal file
496
EXECUTOR_SPECIFICATION.md
Normal file
|
|
@ -0,0 +1,496 @@
|
|||
# TPMJS Executor Protocol Specification v1.0
|
||||
|
||||
> **Status:** Draft
|
||||
> **Version:** 1.0.0
|
||||
> **Last Updated:** 2026-02-03
|
||||
|
||||
## Overview
|
||||
|
||||
The TPMJS Executor Protocol defines a standard HTTP interface for executing TPMJS tools. Executors are **compute adapters** that provide a consistent API for running npm-packaged tools regardless of the underlying infrastructure.
|
||||
|
||||
### Design Philosophy
|
||||
|
||||
- **HTTP-First:** No SDK lock-in, deployable anywhere
|
||||
- **Minimal Surface:** Small core, optional extensions
|
||||
- **Executor ≠ Sandbox:** Standardize coordination, not security
|
||||
- **Declare, Don't Enforce:** Executors report capabilities, TPMJS decides policy
|
||||
|
||||
### Relationship to Other Specs
|
||||
|
||||
| Spec | Purpose |
|
||||
|------|---------|
|
||||
| **MCP** | Model ↔ Tool interface |
|
||||
| **TPMJS Executor** | Tool ↔ Compute interface |
|
||||
| **TPMJS Tools** | Tool contract (separate spec) |
|
||||
|
||||
---
|
||||
|
||||
## Protocol Versioning
|
||||
|
||||
### Version Header
|
||||
|
||||
All requests SHOULD include:
|
||||
|
||||
```http
|
||||
X-TPMJS-Protocol-Version: 1.0
|
||||
```
|
||||
|
||||
Executors MUST respond with their supported protocol version in `/health` and `/info` responses.
|
||||
|
||||
**Rationale:** Header-based versioning enables graceful evolution without URL fragmentation.
|
||||
|
||||
---
|
||||
|
||||
## Specification Levels
|
||||
|
||||
### Level 1: Core (REQUIRED)
|
||||
|
||||
Every executor MUST implement:
|
||||
|
||||
| Endpoint | Method | Purpose |
|
||||
|----------|--------|---------|
|
||||
| `/health` | GET | Liveness + protocol discovery |
|
||||
| `/execute-tool` | POST | Synchronous tool execution |
|
||||
|
||||
### Level 2: Standard (RECOMMENDED)
|
||||
|
||||
Executors SHOULD implement:
|
||||
|
||||
| Endpoint | Method | Purpose |
|
||||
|----------|--------|---------|
|
||||
| `/info` | GET | Capability advertisement |
|
||||
|
||||
Plus:
|
||||
- API key authentication
|
||||
- Structured error responses
|
||||
- Execution timeout enforcement
|
||||
- CORS headers
|
||||
|
||||
### Level 3: Extended (OPTIONAL)
|
||||
|
||||
Reserved for future versions:
|
||||
|
||||
- `POST /execute-tool` with `Accept: text/event-stream` (streaming)
|
||||
- `POST /execute-async` (webhook callbacks)
|
||||
- `POST /validate-tool` (dry-run validation)
|
||||
- `POST /execute-batch` (multiple tools)
|
||||
|
||||
---
|
||||
|
||||
## Core Endpoints
|
||||
|
||||
### GET /health
|
||||
|
||||
**Purpose:** Verify executor is running and discover protocol version.
|
||||
|
||||
**Response (200 OK):**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"protocolVersion": "1.0",
|
||||
"implementationVersion": "1.0.0",
|
||||
"runtime": "node",
|
||||
"timestamp": "2026-02-03T12:00:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Fields:**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `status` | string | Yes | Always `"ok"` if healthy |
|
||||
| `protocolVersion` | string | Yes | TPMJS protocol version (e.g., `"1.0"`) |
|
||||
| `implementationVersion` | string | Yes | Executor software version |
|
||||
| `runtime` | string | No | Runtime identifier (e.g., `"node"`, `"deno"`, `"bun"`) |
|
||||
| `timestamp` | string | No | ISO 8601 timestamp |
|
||||
|
||||
**Requirements:**
|
||||
- MUST respond within 1 second
|
||||
- MUST return 200 OK if healthy
|
||||
- MUST include `protocolVersion`
|
||||
|
||||
---
|
||||
|
||||
### POST /execute-tool
|
||||
|
||||
**Purpose:** Execute a single TPMJS tool synchronously.
|
||||
|
||||
**Request Headers:**
|
||||
|
||||
```http
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer <api-key> (if auth enabled)
|
||||
X-TPMJS-Protocol-Version: 1.0
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"packageName": "@tpmjs/hello",
|
||||
"version": "latest",
|
||||
"name": "helloWorldTool",
|
||||
"params": {
|
||||
"greeting": "Hello"
|
||||
},
|
||||
"env": {
|
||||
"OPENAI_API_KEY": "sk-..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Request Fields:**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `packageName` | string | Yes | npm package name |
|
||||
| `version` | string | No | Package version (default: `"latest"`) |
|
||||
| `name` | string | Yes | Tool export name |
|
||||
| `params` | object | No | Parameters passed to `tool.execute()` |
|
||||
| `env` | object | No | Environment variables for execution |
|
||||
|
||||
**Success Response (200 OK):**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"output": {
|
||||
"message": "Hello, World!"
|
||||
},
|
||||
"executionTimeMs": 1234
|
||||
}
|
||||
```
|
||||
|
||||
**Error Response (200 OK):**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "TOOL_EXECUTION_ERROR",
|
||||
"message": "Tool threw an error: Invalid input"
|
||||
},
|
||||
"executionTimeMs": 123
|
||||
}
|
||||
```
|
||||
|
||||
**Response Fields:**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `success` | boolean | Yes | Whether execution succeeded |
|
||||
| `output` | any | If success | Return value from `tool.execute()` |
|
||||
| `error` | object | If failed | Error details |
|
||||
| `error.code` | string | If failed | Machine-readable error code |
|
||||
| `error.message` | string | If failed | Human-readable error message |
|
||||
| `executionTimeMs` | number | Yes | Total execution time in milliseconds |
|
||||
|
||||
**Error Codes:**
|
||||
|
||||
| Code | Description |
|
||||
|------|-------------|
|
||||
| `PACKAGE_NOT_FOUND` | npm package could not be installed |
|
||||
| `TOOL_NOT_FOUND` | Named export not found in package |
|
||||
| `TOOL_INVALID` | Export exists but has no `.execute()` method |
|
||||
| `TOOL_EXECUTION_ERROR` | Tool threw during execution |
|
||||
| `EXECUTION_TIMEOUT` | Execution exceeded time limit |
|
||||
| `INTERNAL_ERROR` | Unexpected executor error |
|
||||
|
||||
---
|
||||
|
||||
## Standard Endpoints
|
||||
|
||||
### GET /info
|
||||
|
||||
**Purpose:** Advertise executor capabilities for intelligent routing.
|
||||
|
||||
**Response (200 OK):**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Railway Executor",
|
||||
"version": "1.0.0",
|
||||
"protocolVersion": "1.0",
|
||||
"capabilities": {
|
||||
"isolation": "process",
|
||||
"executionModes": ["sync"],
|
||||
"maxExecutionTimeMs": 120000,
|
||||
"maxRequestBodyBytes": 10485760,
|
||||
"supportsStreaming": false,
|
||||
"supportsCallbacks": false,
|
||||
"supportsCaching": false
|
||||
},
|
||||
"runtime": {
|
||||
"platform": "linux",
|
||||
"nodeVersion": "20.10.0",
|
||||
"region": "us-west-1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Capability Fields:**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `isolation` | string | `"none"` \| `"process"` \| `"container"` \| `"vm"` |
|
||||
| `executionModes` | array | `["sync"]` (future: `"stream"`, `"async"`) |
|
||||
| `maxExecutionTimeMs` | number | Maximum execution time before timeout |
|
||||
| `maxRequestBodyBytes` | number | Maximum request body size |
|
||||
| `supportsStreaming` | boolean | Reserved for v1.1 |
|
||||
| `supportsCallbacks` | boolean | Reserved for v1.1 |
|
||||
| `supportsCaching` | boolean | Reserved for v1.1 |
|
||||
|
||||
**Isolation Levels:**
|
||||
|
||||
| Level | Description |
|
||||
|-------|-------------|
|
||||
| `none` | Tools run in executor process (development only) |
|
||||
| `process` | Tools run in separate OS process |
|
||||
| `container` | Tools run in isolated container |
|
||||
| `vm` | Tools run in isolated VM (strongest) |
|
||||
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
### v1.0: API Key Only
|
||||
|
||||
Executors MAY require authentication via Bearer token.
|
||||
|
||||
**Request Header:**
|
||||
|
||||
```http
|
||||
Authorization: Bearer <api-key>
|
||||
```
|
||||
|
||||
**Configuration:**
|
||||
|
||||
Executors SHOULD use `EXECUTOR_API_KEY` environment variable:
|
||||
- If set: All requests MUST include valid Bearer token
|
||||
- If unset: No authentication required
|
||||
|
||||
**Unauthorized Response (401):**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "UNAUTHORIZED",
|
||||
"message": "Invalid or missing API key"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Future Versions:** JWT, OAuth, and per-tool authentication are deferred to v1.1+.
|
||||
|
||||
---
|
||||
|
||||
## CORS Requirements
|
||||
|
||||
All executors MUST support CORS for browser-based clients.
|
||||
|
||||
**Required Headers:**
|
||||
|
||||
```http
|
||||
Access-Control-Allow-Origin: *
|
||||
Access-Control-Allow-Methods: GET, POST, OPTIONS
|
||||
Access-Control-Allow-Headers: Content-Type, Authorization, X-TPMJS-Protocol-Version
|
||||
```
|
||||
|
||||
**OPTIONS Preflight:**
|
||||
|
||||
All endpoints MUST handle OPTIONS requests and return CORS headers with 200 OK.
|
||||
|
||||
---
|
||||
|
||||
## Execution Lifecycle
|
||||
|
||||
### Standard Flow
|
||||
|
||||
1. **Receive Request:** Parse JSON body, validate required fields
|
||||
2. **Check Auth:** Verify API key if configured
|
||||
3. **Create Isolation:** Create temporary execution environment
|
||||
4. **Install Package:** Run `npm install <package>@<version>`
|
||||
5. **Load Tool:** Import package, resolve named export
|
||||
6. **Execute:** Call `tool.execute(params)` with environment
|
||||
7. **Capture Result:** Collect output or error
|
||||
8. **Cleanup:** Remove temporary files/processes
|
||||
9. **Respond:** Return JSON response
|
||||
|
||||
### Tool Resolution
|
||||
|
||||
Executors MUST resolve a callable tool with an `.execute()` method.
|
||||
|
||||
**Recommended Resolution Order:**
|
||||
|
||||
1. `pkg[name]` - Direct named export
|
||||
2. `pkg.default?.[name]` - Named property on default export
|
||||
3. `pkg.default` - Default export itself (if `name` matches)
|
||||
|
||||
**Factory Functions:**
|
||||
|
||||
If export is a function without `.execute()`:
|
||||
1. Try calling `tool()` with no arguments
|
||||
2. Check if result has `.execute()` method
|
||||
|
||||
**Note:** Tool export patterns are intentionally not fully standardized in v1.0 to allow ecosystem evolution.
|
||||
|
||||
---
|
||||
|
||||
## Timeouts
|
||||
|
||||
### Required Timeouts
|
||||
|
||||
| Phase | Minimum | Recommended |
|
||||
|-------|---------|-------------|
|
||||
| npm install | 30s | 60s |
|
||||
| Tool execution | 60s | 120s |
|
||||
| Total request | 90s | 180s |
|
||||
|
||||
Executors MUST:
|
||||
- Enforce execution timeouts
|
||||
- Return `EXECUTION_TIMEOUT` error code when exceeded
|
||||
- Clean up resources on timeout
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### HTTP Status Codes
|
||||
|
||||
| Code | Usage |
|
||||
|------|-------|
|
||||
| 200 | Successful execution OR tool error (with `success: false`) |
|
||||
| 400 | Invalid request (missing fields, malformed JSON) |
|
||||
| 401 | Authentication required but missing/invalid |
|
||||
| 404 | Unknown endpoint |
|
||||
| 500 | Internal executor error |
|
||||
|
||||
### Structured Errors
|
||||
|
||||
All error responses MUST include:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "ERROR_CODE",
|
||||
"message": "Human-readable description"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
### Core (Required for Compliance)
|
||||
|
||||
- [ ] `GET /health` returns status and protocol version
|
||||
- [ ] `POST /execute-tool` accepts standard request format
|
||||
- [ ] Returns `{ success, output/error, executionTimeMs }`
|
||||
- [ ] Handles missing/invalid request body (400)
|
||||
- [ ] CORS headers on all responses
|
||||
- [ ] OPTIONS preflight handling
|
||||
|
||||
### Standard (Recommended)
|
||||
|
||||
- [ ] `GET /info` with capabilities
|
||||
- [ ] `EXECUTOR_API_KEY` environment variable support
|
||||
- [ ] Bearer token validation (401 on failure)
|
||||
- [ ] Execution timeout enforcement
|
||||
- [ ] npm install timeout (60s recommended)
|
||||
- [ ] Temporary file cleanup
|
||||
- [ ] Structured error codes
|
||||
|
||||
### Extended (Optional)
|
||||
|
||||
- [ ] Package caching
|
||||
- [ ] Concurrent execution limiting
|
||||
- [ ] Support for both `/path` and `/api/path` routes
|
||||
- [ ] Region/metadata in `/info` response
|
||||
|
||||
---
|
||||
|
||||
## Compliance Testing
|
||||
|
||||
Use the official compliance test suite:
|
||||
|
||||
```bash
|
||||
npx @tpmjs/executor-test https://my-executor.example.com
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
TPMJS Executor Compliance Test v1.0.0
|
||||
Target: https://my-executor.example.com
|
||||
|
||||
Core Requirements:
|
||||
✓ GET /health returns 200
|
||||
✓ GET /health includes protocolVersion
|
||||
✓ POST /execute-tool accepts valid request
|
||||
✓ POST /execute-tool returns success response
|
||||
✓ POST /execute-tool returns error for invalid tool
|
||||
✓ CORS headers present
|
||||
✓ OPTIONS preflight works
|
||||
|
||||
Standard Requirements:
|
||||
✓ GET /info returns capabilities
|
||||
✓ Authentication enforced when configured
|
||||
✓ Execution timeout enforced
|
||||
✗ Missing: maxExecutionTimeMs in capabilities
|
||||
|
||||
Result: 10/11 tests passed (Core: PASS, Standard: PARTIAL)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reference Implementations
|
||||
|
||||
| Name | Platform | Isolation | Source |
|
||||
|------|----------|-----------|--------|
|
||||
| Railway Executor | Railway | Process | `templates/railway-executor/` |
|
||||
| Vercel Executor | Vercel | VM (Sandbox) | `templates/vercel-executor/` |
|
||||
| Unsandbox Executor | Unsandbox | Container | `templates/unsandbox-executor/` |
|
||||
|
||||
---
|
||||
|
||||
## Future Roadmap
|
||||
|
||||
### v1.1 (Planned)
|
||||
|
||||
- Streaming responses (`Accept: text/event-stream`)
|
||||
- Async execution with webhooks
|
||||
- Caching hints (`X-TPMJS-Cache-*` headers)
|
||||
- Tool validation endpoint
|
||||
|
||||
### v2.0 (Exploration)
|
||||
|
||||
- Multi-tool batch execution
|
||||
- Persistent execution contexts
|
||||
- Resource quotas and billing hooks
|
||||
- MCP bridge protocol
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
### v1.0.0 (2026-02-03)
|
||||
|
||||
- Initial formal specification
|
||||
- Core: `/health`, `/execute-tool`
|
||||
- Standard: `/info`, API key auth
|
||||
- Capability negotiation
|
||||
- Compliance test suite
|
||||
|
||||
---
|
||||
|
||||
## Appendix: OpenAPI Specification
|
||||
|
||||
See `executor-openapi.yaml` for the formal OpenAPI 3.0 specification.
|
||||
|
||||
## Appendix: JSON Schemas
|
||||
|
||||
See `packages/types/src/executor.ts` for TypeScript types and Zod schemas.
|
||||
429
HOW_TO_PUBLISH_A_TOOL.md
Normal file
429
HOW_TO_PUBLISH_A_TOOL.md
Normal file
|
|
@ -0,0 +1,429 @@
|
|||
# How to Publish a TPMJS Tool
|
||||
|
||||
This guide shows you how to create and publish an AI tool that will be automatically discovered and listed on tpmjs.com.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Create a new NPM package
|
||||
2. Add `"tpmjs"` to the `keywords` array in package.json
|
||||
3. Add a `tpmjs` field with your tool's metadata
|
||||
4. Publish to NPM
|
||||
5. Your tool will automatically appear on tpmjs.com within 15 minutes
|
||||
|
||||
## Step-by-Step Guide
|
||||
|
||||
### 1. Create Your NPM Package
|
||||
|
||||
Create a standard NPM package with your tool implementation:
|
||||
|
||||
```bash
|
||||
mkdir my-awesome-tool
|
||||
cd my-awesome-tool
|
||||
npm init -y
|
||||
```
|
||||
|
||||
### 2. Add the Required Keyword
|
||||
|
||||
In your `package.json`, add `"tpmjs"` to the keywords array:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@yourname/my-awesome-tool",
|
||||
"version": "1.0.0",
|
||||
"keywords": ["tpmjs", "ai", "other-keywords"],
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**Important:** The `"tpmjs"` keyword is REQUIRED for automatic discovery!
|
||||
|
||||
### 3. Add TPMJS Metadata
|
||||
|
||||
Add a `tpmjs` field to your `package.json` with your tool's metadata. There are three tiers:
|
||||
|
||||
#### Tier 1: Minimal (Required Fields Only)
|
||||
|
||||
The bare minimum to get listed:
|
||||
|
||||
```json
|
||||
{
|
||||
"tpmjs": {
|
||||
"category": "text-analysis",
|
||||
"description": "A concise description of what your tool does"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Required fields:**
|
||||
- `category` - One of: `text-analysis`, `code-generation`, `data-processing`, `image-generation`, `audio-processing`, `search`, `integration`, `other`
|
||||
- `description` - Clear description of what the tool does (1-3 sentences)
|
||||
|
||||
#### Tier 2: Basic (Recommended)
|
||||
|
||||
Add parameter and return type information:
|
||||
|
||||
```json
|
||||
{
|
||||
"tpmjs": {
|
||||
"category": "text-analysis",
|
||||
"description": "Analyzes sentiment in text and returns a score",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "string",
|
||||
"description": "The text to analyze",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "language",
|
||||
"type": "string",
|
||||
"description": "Language code (e.g., 'en', 'es')",
|
||||
"required": false,
|
||||
"default": "en"
|
||||
}
|
||||
],
|
||||
"returns": {
|
||||
"type": "SentimentResult",
|
||||
"description": "Object containing score (-1 to 1) and label (positive/negative/neutral)"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Tier 3: Rich (Full Documentation)
|
||||
|
||||
Complete metadata for maximum visibility:
|
||||
|
||||
```json
|
||||
{
|
||||
"tpmjs": {
|
||||
"category": "text-analysis",
|
||||
"description": "Advanced sentiment analysis with emotion detection",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "string",
|
||||
"description": "The text to analyze",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "language",
|
||||
"type": "string",
|
||||
"description": "Language code",
|
||||
"required": false,
|
||||
"default": "en"
|
||||
},
|
||||
{
|
||||
"name": "includeEmotions",
|
||||
"type": "boolean",
|
||||
"description": "Whether to include emotion breakdown",
|
||||
"required": false,
|
||||
"default": false
|
||||
}
|
||||
],
|
||||
"returns": {
|
||||
"type": "SentimentResult",
|
||||
"description": "Object with score, label, and optional emotions array"
|
||||
},
|
||||
"env": [
|
||||
{
|
||||
"name": "SENTIMENT_API_KEY",
|
||||
"description": "API key for sentiment analysis service",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"frameworks": ["vercel-ai", "langchain"],
|
||||
"aiAgent": {
|
||||
"useCase": "Use this tool when users need to analyze sentiment in text, detect emotions, or understand the tone of customer feedback, reviews, or social media posts.",
|
||||
"limitations": "Only supports English and Spanish. Maximum 10,000 characters per request.",
|
||||
"examples": [
|
||||
"Analyze customer review sentiment",
|
||||
"Detect emotions in user feedback",
|
||||
"Monitor social media sentiment"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Implement Your Tool
|
||||
|
||||
Write your tool's implementation. Here's the example from `@tpmjs/createblogpost`:
|
||||
|
||||
```typescript
|
||||
// src/index.ts
|
||||
export interface BlogPostOptions {
|
||||
title: string;
|
||||
author: string;
|
||||
content: string;
|
||||
tags?: string[];
|
||||
format?: 'markdown' | 'mdx';
|
||||
excerpt?: string;
|
||||
}
|
||||
|
||||
export interface BlogPost {
|
||||
frontmatter: {
|
||||
title: string;
|
||||
author: string;
|
||||
date: string;
|
||||
tags: string[];
|
||||
excerpt?: string;
|
||||
slug: string;
|
||||
wordCount: number;
|
||||
readingTime: number;
|
||||
};
|
||||
content: string;
|
||||
formattedOutput: string;
|
||||
}
|
||||
|
||||
export async function createBlogPost(options: BlogPostOptions): Promise<BlogPost> {
|
||||
// Your implementation here
|
||||
const { title, author, content, tags = [], format = 'markdown', excerpt } = options;
|
||||
|
||||
// Validate inputs
|
||||
if (!title || !author || !content) {
|
||||
throw new Error('Title, author, and content are required');
|
||||
}
|
||||
|
||||
// Process and return result
|
||||
return {
|
||||
frontmatter: { /* ... */ },
|
||||
content,
|
||||
formattedOutput: '...'
|
||||
};
|
||||
}
|
||||
|
||||
export default createBlogPost;
|
||||
```
|
||||
|
||||
### 5. Build and Publish
|
||||
|
||||
Build your package and publish to NPM:
|
||||
|
||||
```bash
|
||||
# Build your package
|
||||
npm run build
|
||||
|
||||
# Publish to NPM
|
||||
npm publish --access public
|
||||
```
|
||||
|
||||
### 6. Verification
|
||||
|
||||
Your tool will be automatically discovered through:
|
||||
|
||||
1. **Keyword Search** - Runs every 15 minutes, searches NPM for `"tpmjs"`
|
||||
2. **Changes Feed** - Monitors NPM publishes in real-time (every 2 minutes)
|
||||
|
||||
After publishing, your tool should appear on https://tpmjs.com within 15 minutes!
|
||||
|
||||
You can verify by searching (requires API key):
|
||||
```bash
|
||||
curl "https://tpmjs.com/api/tools?q=yourpackagename" \
|
||||
-H "Authorization: Bearer tpmjs_sk_your_api_key_here"
|
||||
```
|
||||
|
||||
## Real Example: @tpmjs/createblogpost
|
||||
|
||||
Here's the complete `package.json` from the published example:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@tpmjs/createblogpost",
|
||||
"version": "0.2.0",
|
||||
"description": "A tool for creating structured blog posts with AI-generated content",
|
||||
"type": "module",
|
||||
"keywords": ["tpmjs", "blog", "content", "ai", "writing"],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": ["dist"],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"dev": "tsup --watch",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ajaxdavis/tpmjs.git",
|
||||
"directory": "packages/tools/createBlogPost"
|
||||
},
|
||||
"homepage": "https://tpmjs.com",
|
||||
"license": "MIT",
|
||||
"tpmjs": {
|
||||
"category": "text-analysis",
|
||||
"description": "Creates structured blog posts with customizable frontmatter, content sections, and SEO metadata. Supports multiple output formats including Markdown and MDX.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "title",
|
||||
"type": "string",
|
||||
"description": "The title of the blog post",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "author",
|
||||
"type": "string",
|
||||
"description": "The author of the blog post",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"description": "The main content of the blog post",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "tags",
|
||||
"type": "string[]",
|
||||
"description": "Array of tags for categorization",
|
||||
"required": false,
|
||||
"default": []
|
||||
},
|
||||
{
|
||||
"name": "format",
|
||||
"type": "'markdown' | 'mdx'",
|
||||
"description": "Output format for the blog post",
|
||||
"required": false,
|
||||
"default": "markdown"
|
||||
},
|
||||
{
|
||||
"name": "excerpt",
|
||||
"type": "string",
|
||||
"description": "Short excerpt or summary of the post",
|
||||
"required": false
|
||||
}
|
||||
],
|
||||
"returns": {
|
||||
"type": "BlogPost",
|
||||
"description": "A structured blog post object with frontmatter, content, and metadata including slug, wordCount, readingTime, and formattedOutput"
|
||||
},
|
||||
"frameworks": ["vercel-ai", "langchain"],
|
||||
"aiAgent": {
|
||||
"useCase": "Use this tool when users need to generate blog posts, articles, or structured content with proper frontmatter and metadata. Ideal for content management systems, static site generators, and documentation sites.",
|
||||
"limitations": "Does not include AI content generation - you must provide the content. Only formats and structures existing content.",
|
||||
"examples": [
|
||||
"Create a blog post about TypeScript best practices",
|
||||
"Generate a tutorial post with code examples",
|
||||
"Format an article with SEO metadata"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Field Reference
|
||||
|
||||
### Required Fields (Tier 1 - Minimal)
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `category` | string | Tool category (see categories below) |
|
||||
| `description` | string | Clear description (1-3 sentences) |
|
||||
|
||||
### Optional Fields (Tier 2 - Basic)
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `parameters` | array | Array of parameter objects |
|
||||
| `returns` | object | Return type information |
|
||||
|
||||
### Optional Fields (Tier 3 - Rich)
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `env` | array | Required environment variables |
|
||||
| `frameworks` | array | Compatible frameworks |
|
||||
| `aiAgent` | object | AI agent integration info |
|
||||
|
||||
### Categories
|
||||
|
||||
Choose one of these for the `category` field:
|
||||
|
||||
- `text-analysis` - NLP, sentiment, summarization
|
||||
- `code-generation` - Code generation and transformation
|
||||
- `data-processing` - Data manipulation and transformation
|
||||
- `image-generation` - Image creation and editing
|
||||
- `audio-processing` - Audio/speech processing
|
||||
- `search` - Search and retrieval
|
||||
- `integration` - Third-party integrations
|
||||
- `other` - Anything else
|
||||
|
||||
### Environment Variables
|
||||
|
||||
If your tool requires environment variables:
|
||||
|
||||
```json
|
||||
"env": [
|
||||
{
|
||||
"name": "OPENAI_API_KEY",
|
||||
"description": "API key for OpenAI services",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "API_ENDPOINT",
|
||||
"description": "Custom API endpoint URL",
|
||||
"required": false,
|
||||
"default": "https://api.example.com"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Quality Score
|
||||
|
||||
Your tool gets a quality score based on:
|
||||
|
||||
- **Tier**: Rich (1.0) > Basic (0.5) > Minimal (0.25)
|
||||
- **Downloads**: Logarithmic scale based on monthly NPM downloads
|
||||
- **GitHub Stars**: Logarithmic scale based on repository stars
|
||||
|
||||
Higher scores = better visibility on tpmjs.com!
|
||||
|
||||
## Tips for Success
|
||||
|
||||
1. **Use descriptive names** - Make your package name clear and searchable
|
||||
2. **Complete metadata** - Tier 3 (Rich) tools get 4x the base score
|
||||
3. **Good documentation** - Add documentation URL to package.json homepage or repository fields
|
||||
4. **Active maintenance** - Regular updates boost download counts
|
||||
5. **AI-friendly descriptions** - Write the `aiAgent.useCase` field as guidance for AI agents
|
||||
|
||||
## Testing Locally
|
||||
|
||||
Before publishing, you can validate your `tpmjs` field using the validation schema:
|
||||
|
||||
```bash
|
||||
# In the tpmjs monorepo
|
||||
pnpm --filter=@tpmjs/types test
|
||||
```
|
||||
|
||||
Or manually check the structure matches the examples above.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Tool not appearing after 15 minutes?**
|
||||
- Check that you added `"tpmjs"` to keywords
|
||||
- Verify your `tpmjs` field has required fields (category, description)
|
||||
- Check the NPM package is public: `npm view yourpackage`
|
||||
|
||||
**Tool showing as "minimal" tier?**
|
||||
- Add `parameters` and `returns` fields for Basic tier
|
||||
- Add all Rich tier fields for maximum visibility
|
||||
|
||||
**Want to force a sync?**
|
||||
You can manually trigger a sync (requires CRON_SECRET, not a user API key):
|
||||
```bash
|
||||
curl -X POST "https://tpmjs.com/api/sync/keyword" \
|
||||
-H "Authorization: Bearer $CRON_SECRET"
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
Questions or issues?
|
||||
- File an issue: https://github.com/ajaxdavis/tpmjs/issues
|
||||
- Check the API docs: https://tpmjs.com/docs/api
|
||||
- Generate an API key: https://tpmjs.com/dashboard/settings/tpmjs-api-keys
|
||||
|
|
@ -1,782 +0,0 @@
|
|||
# TPMJS NPM Registry - Implementation Checklist
|
||||
|
||||
> **Reference:** See [NPM_MIRROR.md](./NPM_MIRROR.md) for complete architecture details
|
||||
|
||||
**Stack Decision:** Vercel + Neon + Vercel Cron (polling-based sync)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Implementation Strategy
|
||||
|
||||
### Architecture Simplification
|
||||
|
||||
**Original Plan (NPM_MIRROR.md):**
|
||||
- Separate Node.js sync service with persistent changes feed connection
|
||||
- Self-hosted PostgreSQL
|
||||
- More complex deployment
|
||||
|
||||
**Revised Plan (This Checklist):**
|
||||
- All-in-one Next.js app on Vercel
|
||||
- Neon Postgres (serverless)
|
||||
- Vercel Cron for sync jobs (polling-based, no persistent connections)
|
||||
- Simpler, faster to ship
|
||||
|
||||
### Why This Approach?
|
||||
|
||||
✅ **Simpler Infrastructure**
|
||||
- One deployment (Vercel)
|
||||
- Managed database (Neon)
|
||||
- Built-in cron (Vercel Cron)
|
||||
|
||||
✅ **Lower Cost**
|
||||
- No separate sync service hosting
|
||||
- Neon free tier generous
|
||||
- Vercel free/hobby tier sufficient for MVP
|
||||
|
||||
✅ **Same Functionality**
|
||||
- Poll NPM changes feed every 1-2 minutes (effectively real-time)
|
||||
- All discovery features from NPM_MIRROR.md maintained
|
||||
- Quality scoring, validation, etc. all work the same
|
||||
|
||||
---
|
||||
|
||||
## 📋 Phase 1: Foundation (Week 1)
|
||||
|
||||
**Goal:** Set up database, types, and NPM client
|
||||
|
||||
### 1.1 Database Setup
|
||||
|
||||
- [ ] **Create Neon project**
|
||||
- Go to https://neon.tech/
|
||||
- Create new project
|
||||
- Save connection string
|
||||
|
||||
- [ ] **Create `packages/db` package**
|
||||
```bash
|
||||
mkdir -p packages/db
|
||||
cd packages/db
|
||||
pnpm init
|
||||
pnpm add prisma @prisma/client
|
||||
pnpm add -D typescript @types/node
|
||||
```
|
||||
|
||||
- [ ] **Initialize Prisma**
|
||||
```bash
|
||||
npx prisma init
|
||||
```
|
||||
|
||||
- [ ] **Create Prisma schema**
|
||||
- Copy schema from NPM_MIRROR.md Database Design section
|
||||
- File: `packages/db/prisma/schema.prisma`
|
||||
- Include all three models: `Tool`, `SyncCheckpoint`, `SyncLog`
|
||||
|
||||
- [ ] **Add database URL to `.env`**
|
||||
```env
|
||||
DATABASE_URL="postgresql://..."
|
||||
```
|
||||
|
||||
- [ ] **Run first migration**
|
||||
```bash
|
||||
npx prisma migrate dev --name init
|
||||
npx prisma generate
|
||||
```
|
||||
|
||||
- [ ] **Create Prisma client singleton**
|
||||
- File: `packages/db/src/client.ts`
|
||||
```typescript
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const globalForPrisma = globalThis as unknown as {
|
||||
prisma: PrismaClient | undefined;
|
||||
};
|
||||
|
||||
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
globalForPrisma.prisma = prisma;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Export from package**
|
||||
- File: `packages/db/src/index.ts`
|
||||
```typescript
|
||||
export { prisma } from './client';
|
||||
export * from '@prisma/client';
|
||||
```
|
||||
|
||||
- [ ] **Update package.json**
|
||||
```json
|
||||
{
|
||||
"name": "@tpmjs/db",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts"
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Seed initial sync state**
|
||||
```sql
|
||||
INSERT INTO sync_checkpoints (source, checkpoint)
|
||||
VALUES
|
||||
('changes-feed', '{"sequence": 0}'::jsonb),
|
||||
('keyword-search', '{"lastRun": null}'::jsonb),
|
||||
('metrics', '{"lastRun": null}'::jsonb)
|
||||
ON CONFLICT (source) DO NOTHING;
|
||||
```
|
||||
|
||||
**Verification:**
|
||||
```bash
|
||||
cd packages/db
|
||||
npx prisma studio # Should open DB browser with empty tables
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1.2 Types Package
|
||||
|
||||
**Reference:** See NPM_MIRROR.md "The 'tpmjs' Field Schema" section
|
||||
|
||||
- [ ] **Update `packages/types/src/tool.ts`**
|
||||
- Add `TpmjsMinimalSchema` with Zod
|
||||
- Add `TpmjsRichSchema` extending minimal
|
||||
- Export both schemas and inferred types
|
||||
|
||||
- [ ] **Create validation helper**
|
||||
- File: `packages/types/src/validator.ts`
|
||||
```typescript
|
||||
export function validateTpmjsField(tpmjs: unknown): {
|
||||
valid: boolean;
|
||||
tier: 'minimal' | 'rich' | null;
|
||||
data?: unknown;
|
||||
errors?: ZodError[];
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Update exports**
|
||||
- File: `packages/types/src/index.ts`
|
||||
- Export all schemas and validators
|
||||
|
||||
**Verification:**
|
||||
```typescript
|
||||
import { validateTpmjsField } from '@tpmjs/types';
|
||||
|
||||
const result = validateTpmjsField({
|
||||
category: 'web-scraping',
|
||||
description: 'Test description that is long enough',
|
||||
example: 'const x = await tool.test()'
|
||||
});
|
||||
|
||||
console.log(result); // Should be { valid: true, tier: 'minimal', ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1.3 NPM Client Package
|
||||
|
||||
**Reference:** See NPM_MIRROR.md "NPM Integration Strategy" section
|
||||
|
||||
- [ ] **Create `packages/npm-client`**
|
||||
```bash
|
||||
mkdir -p packages/npm-client/src
|
||||
cd packages/npm-client
|
||||
pnpm init
|
||||
pnpm add zod
|
||||
pnpm add -D typescript @types/node
|
||||
```
|
||||
|
||||
- [ ] **Implement changes feed client**
|
||||
- File: `packages/npm-client/src/changes.ts`
|
||||
```typescript
|
||||
export async function fetchChanges(since: string, limit = 100): Promise<{
|
||||
results: Array<{ id: string; seq: string }>;
|
||||
lastSeq: string;
|
||||
}>
|
||||
```
|
||||
- Use endpoint: `https://replicate.npmjs.com/registry/_changes`
|
||||
- Poll-based (no EventSource needed)
|
||||
|
||||
- [ ] **Implement keyword search**
|
||||
- File: `packages/npm-client/src/search.ts`
|
||||
```typescript
|
||||
export async function searchByKeyword(
|
||||
keyword: string,
|
||||
size = 250,
|
||||
from = 0
|
||||
): Promise<Array<{ name: string }>>
|
||||
```
|
||||
- Use endpoint: `/-/v1/search?text=keywords:${keyword}`
|
||||
|
||||
- [ ] **Implement package metadata fetcher**
|
||||
- File: `packages/npm-client/src/package.ts`
|
||||
```typescript
|
||||
export async function fetchPackageMetadata(packageName: string): Promise<{
|
||||
name: string;
|
||||
'dist-tags': { latest: string };
|
||||
versions: Record<string, {
|
||||
version: string;
|
||||
description?: string;
|
||||
tpmjs?: unknown;
|
||||
// ... other fields
|
||||
}>;
|
||||
time: Record<string, string>;
|
||||
} | null>
|
||||
```
|
||||
- Use endpoint: `https://registry.npmjs.org/${packageName}`
|
||||
|
||||
- [ ] **Implement download stats**
|
||||
- File: `packages/npm-client/src/stats.ts`
|
||||
```typescript
|
||||
export async function fetchDownloadStats(
|
||||
packageName: string
|
||||
): Promise<number>
|
||||
```
|
||||
- Use endpoint: `https://api.npmjs.org/downloads/point/last-month/${packageName}`
|
||||
|
||||
- [ ] **Implement GitHub stats** (optional Phase 4)
|
||||
- File: `packages/npm-client/src/github.ts`
|
||||
```typescript
|
||||
export async function fetchGithubStars(
|
||||
repoUrl: string
|
||||
): Promise<number>
|
||||
```
|
||||
|
||||
- [ ] **Add rate limiting helper**
|
||||
- File: `packages/npm-client/src/rate-limiter.ts`
|
||||
- Simple delay between requests
|
||||
- Exponential backoff on 429
|
||||
|
||||
- [ ] **Export all functions**
|
||||
- File: `packages/npm-client/src/index.ts`
|
||||
|
||||
**Verification:**
|
||||
```typescript
|
||||
import { fetchPackageMetadata } from '@tpmjs/npm-client';
|
||||
|
||||
const pkg = await fetchPackageMetadata('express');
|
||||
console.log(pkg?.name); // Should print 'express'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Phase 2: Core API Routes (Week 2)
|
||||
|
||||
**Goal:** Build public API for searching/listing tools
|
||||
|
||||
### 2.1 Tool Search/List API
|
||||
|
||||
**Reference:** See NPM_MIRROR.md "API Routes" section
|
||||
|
||||
- [ ] **Create `apps/web/src/app/api/tools/route.ts`**
|
||||
- Implement `GET` handler
|
||||
- Query params: `q`, `category`, `official`, `limit`, `offset`
|
||||
- Use Prisma to query `tools` table
|
||||
- Return paginated results with metadata
|
||||
|
||||
- [ ] **Add full-text search**
|
||||
- Use Postgres `ts_vector` for search
|
||||
- Or simple `ILIKE` for MVP
|
||||
- Search across: `npmPackageName`, `description`, `tags`
|
||||
|
||||
- [ ] **Add filtering**
|
||||
- By `category`
|
||||
- By `isOfficial`
|
||||
- By `tier` (optional)
|
||||
|
||||
- [ ] **Add sorting**
|
||||
- Default: `qualityScore DESC`, `npmDownloadsLastMonth DESC`
|
||||
- Optional: `createdAt DESC`, `npmPackageName ASC`
|
||||
|
||||
**Verification:**
|
||||
```bash
|
||||
curl "http://localhost:3001/api/tools?q=web&limit=5"
|
||||
# Should return JSON with tools array and pagination
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.2 Tool Detail API
|
||||
|
||||
- [ ] **Create `apps/web/src/app/api/tools/[id]/route.ts`**
|
||||
- Implement `GET` handler
|
||||
- Accept ID or package name
|
||||
- Return full tool details
|
||||
|
||||
**Verification:**
|
||||
```bash
|
||||
curl "http://localhost:3001/api/tools/1"
|
||||
# Should return single tool object
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.3 Validation API
|
||||
|
||||
- [ ] **Create `apps/web/src/app/api/tools/validate/route.ts`**
|
||||
- Implement `POST` handler
|
||||
- Accept JSON body with `tpmjs` field
|
||||
- Use `@tpmjs/types` validator
|
||||
- Return validation result with errors
|
||||
|
||||
**Verification:**
|
||||
```bash
|
||||
curl -X POST http://localhost:3001/api/tools/validate \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"category":"web-scraping","description":"Test tool for validation","example":"const x = await tool.test()"}'
|
||||
# Should return { valid: true, tier: "minimal" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.4 Stats API
|
||||
|
||||
- [ ] **Create `apps/web/src/app/api/stats/route.ts`**
|
||||
- Implement `GET` handler
|
||||
- Aggregate counts by category
|
||||
- Total tools, official tools, etc.
|
||||
|
||||
**Verification:**
|
||||
```bash
|
||||
curl "http://localhost:3001/api/stats"
|
||||
# Should return { totalTools: 0, officialTools: 0, categories: {} }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Phase 3: Sync Workers (Week 2-3)
|
||||
|
||||
**Goal:** Implement automatic NPM package discovery
|
||||
|
||||
**Reference:** See NPM_MIRROR.md "NPM Integration Strategy" section
|
||||
|
||||
### 3.1 Changes Feed Sync
|
||||
|
||||
- [ ] **Create `apps/web/src/app/api/sync/changes/route.ts`**
|
||||
|
||||
- [ ] **Implement POST handler**
|
||||
```typescript
|
||||
export async function POST(request: Request) {
|
||||
// 1. Verify CRON_SECRET header
|
||||
// 2. Get last sequence from sync_checkpoints
|
||||
// 3. Fetch changes from NPM (limit 100-500)
|
||||
// 4. For each change:
|
||||
// - Fetch package metadata
|
||||
// - Check for tpmjs field
|
||||
// - Validate with @tpmjs/types
|
||||
// - Upsert to tools table
|
||||
// - Log to sync_logs
|
||||
// 5. Update checkpoint with new sequence
|
||||
// 6. Return summary (processed, skipped, errors)
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Add secret protection**
|
||||
```typescript
|
||||
const secret = request.headers.get('x-cron-secret');
|
||||
if (secret !== process.env.CRON_SECRET) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Add timeout protection**
|
||||
- Limit processing to 50 packages per run
|
||||
- Or 50 seconds max execution time
|
||||
- Save checkpoint frequently
|
||||
|
||||
- [ ] **Add error handling**
|
||||
- Try/catch around each package
|
||||
- Log errors to `sync_logs`
|
||||
- Continue processing other packages
|
||||
|
||||
**Verification:**
|
||||
```bash
|
||||
curl -X POST http://localhost:3001/api/sync/changes \
|
||||
-H "x-cron-secret: your-secret"
|
||||
# Should process changes and return summary
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.2 Keyword Search Sync
|
||||
|
||||
- [ ] **Create `apps/web/src/app/api/sync/keyword/route.ts`**
|
||||
|
||||
- [ ] **Implement POST handler**
|
||||
```typescript
|
||||
export async function POST(request: Request) {
|
||||
// 1. Verify CRON_SECRET header
|
||||
// 2. Search NPM for keyword 'tpmjs-tool'
|
||||
// 3. For each result:
|
||||
// - Fetch package metadata
|
||||
// - Validate tpmjs field
|
||||
// - Upsert with isOfficial=true
|
||||
// - Log to sync_logs
|
||||
// 4. Update checkpoint
|
||||
// 5. Return summary
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Handle pagination**
|
||||
- NPM allows `size` up to 250
|
||||
- May need multiple requests for all results
|
||||
|
||||
**Verification:**
|
||||
```bash
|
||||
curl -X POST http://localhost:3001/api/sync/keyword \
|
||||
-H "x-cron-secret: your-secret"
|
||||
# Should search and process keyword packages
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.3 Metrics Sync (Phase 4)
|
||||
|
||||
- [ ] **Create `apps/web/src/app/api/sync/metrics/route.ts`**
|
||||
|
||||
- [ ] **Implement POST handler**
|
||||
```typescript
|
||||
export async function POST(request: Request) {
|
||||
// 1. Verify CRON_SECRET
|
||||
// 2. Select tools to update (recent, popular, or sample)
|
||||
// 3. For each tool:
|
||||
// - Fetch NPM download stats
|
||||
// - Fetch GitHub stars (if repo exists)
|
||||
// - Calculate quality score
|
||||
// - Update tools table
|
||||
// 4. Update checkpoint
|
||||
// 5. Return summary
|
||||
}
|
||||
```
|
||||
|
||||
**Verification:**
|
||||
```bash
|
||||
curl -X POST http://localhost:3001/api/sync/metrics \
|
||||
-H "x-cron-secret: your-secret"
|
||||
# Should update metrics for tools
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.4 Vercel Cron Configuration
|
||||
|
||||
- [ ] **Add to `vercel.json`**
|
||||
```json
|
||||
{
|
||||
"crons": [
|
||||
{
|
||||
"path": "/api/sync/changes",
|
||||
"schedule": "*/2 * * * *"
|
||||
},
|
||||
{
|
||||
"path": "/api/sync/keyword",
|
||||
"schedule": "*/15 * * * *"
|
||||
},
|
||||
{
|
||||
"path": "/api/sync/metrics",
|
||||
"schedule": "0 * * * *"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Set up environment variables in Vercel**
|
||||
- `DATABASE_URL` - Neon connection string
|
||||
- `CRON_SECRET` - Generate random secret
|
||||
- `NPM_REGISTRY_URL` - https://registry.npmjs.org
|
||||
- `NPM_CHANGES_URL` - https://replicate.npmjs.com/registry
|
||||
|
||||
---
|
||||
|
||||
## 📋 Phase 4: Frontend Integration (Week 3)
|
||||
|
||||
**Goal:** Replace mock data with real API calls
|
||||
|
||||
### 4.1 Update Tool Listing Page
|
||||
|
||||
- [ ] **Update `apps/web/src/app/tools/page.tsx`**
|
||||
- Remove mock data import
|
||||
- Fetch from `/api/tools`
|
||||
- Add loading state
|
||||
- Add error handling
|
||||
|
||||
- [ ] **Add search functionality**
|
||||
- Search input component
|
||||
- Debounced API calls
|
||||
- Update URL with search params
|
||||
|
||||
- [ ] **Add category filter**
|
||||
- Category dropdown/pills
|
||||
- Filter API calls by category
|
||||
|
||||
- [ ] **Add pagination**
|
||||
- Next/previous buttons
|
||||
- Or infinite scroll
|
||||
|
||||
**Verification:**
|
||||
- Visit http://localhost:3001/tools
|
||||
- Should show real tools from database
|
||||
- Search should work
|
||||
- Filters should work
|
||||
|
||||
---
|
||||
|
||||
### 4.2 Update Tool Detail Page
|
||||
|
||||
- [ ] **Update `apps/web/src/app/tools/[id]/page.tsx`**
|
||||
- Fetch from `/api/tools/[id]`
|
||||
- Display all tool metadata
|
||||
- Show rich tier fields if available
|
||||
|
||||
- [ ] **Add install instructions**
|
||||
- npm install command
|
||||
- Usage example from `tpmjs.example`
|
||||
|
||||
- [ ] **Add links**
|
||||
- NPM package page
|
||||
- GitHub repository
|
||||
- Documentation
|
||||
- Playground (if available)
|
||||
|
||||
**Verification:**
|
||||
- Visit http://localhost:3001/tools/some-package
|
||||
- Should show full tool details
|
||||
|
||||
---
|
||||
|
||||
### 4.3 Update Homepage
|
||||
|
||||
**Reference:** See NPM_MIRROR.md for stats display
|
||||
|
||||
- [ ] **Update stats in hero section**
|
||||
- Fetch from `/api/stats`
|
||||
- Show real tool count
|
||||
- Show category breakdown
|
||||
|
||||
- [ ] **Update live metrics**
|
||||
- Real download counts
|
||||
- Real tool counts
|
||||
- Update frequently (client-side polling or static)
|
||||
|
||||
**Verification:**
|
||||
- Visit http://localhost:3001
|
||||
- Stats should be real, not mock
|
||||
|
||||
---
|
||||
|
||||
## 📋 Phase 5: Testing & Polish (Week 4)
|
||||
|
||||
### 5.1 Create Test Packages
|
||||
|
||||
- [ ] **Publish 3-5 real NPM packages with `tpmjs` field**
|
||||
- At least one with minimal tier
|
||||
- At least one with rich tier
|
||||
- Use `tpmjs-tool` keyword for official listing
|
||||
|
||||
- [ ] **Verify automatic discovery**
|
||||
- Wait for next sync run
|
||||
- Check they appear in database
|
||||
- Check they appear on website
|
||||
|
||||
---
|
||||
|
||||
### 5.2 Documentation
|
||||
|
||||
- [ ] **Create docs section**
|
||||
- `apps/web/src/app/docs/page.tsx`
|
||||
- Getting started guide
|
||||
- Schema reference
|
||||
- Examples
|
||||
|
||||
- [ ] **Add validation playground**
|
||||
- `apps/web/src/app/docs/validate/page.tsx`
|
||||
- Form to test `tpmjs` field
|
||||
- Real-time validation feedback
|
||||
- Uses `/api/tools/validate`
|
||||
|
||||
---
|
||||
|
||||
### 5.3 CLI Tool (Optional)
|
||||
|
||||
- [ ] **Create `packages/cli`**
|
||||
- Command: `tpmjs validate`
|
||||
- Reads local `package.json`
|
||||
- Validates `tpmjs` field
|
||||
- Calls `/api/tools/validate`
|
||||
|
||||
---
|
||||
|
||||
### 5.4 Monitoring
|
||||
|
||||
- [ ] **Add health endpoint**
|
||||
- `apps/web/src/app/api/health/route.ts`
|
||||
- Check database connectivity
|
||||
- Check sync status (last run times)
|
||||
|
||||
- [ ] **Set up uptime monitoring**
|
||||
- Use UptimeRobot or Better Stack
|
||||
- Monitor `/api/health`
|
||||
- Alert if down or sync stale
|
||||
|
||||
- [ ] **Add error tracking**
|
||||
- Set up Sentry for Next.js
|
||||
- Track API errors
|
||||
- Track sync errors
|
||||
|
||||
**Verification:**
|
||||
```bash
|
||||
curl http://localhost:3001/api/health
|
||||
# Should return { status: "ok", db: "ok", sync: { ... } }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Phase 6: Launch (Week 5)
|
||||
|
||||
### 6.1 Pre-Launch Checklist
|
||||
|
||||
- [ ] **Database**
|
||||
- ✓ Prisma schema deployed
|
||||
- ✓ Indexes created
|
||||
- ✓ Backups enabled in Neon
|
||||
|
||||
- [ ] **Environment Variables**
|
||||
- ✓ All secrets in Vercel
|
||||
- ✓ `CRON_SECRET` set
|
||||
- ✓ `DATABASE_URL` set
|
||||
|
||||
- [ ] **API Routes**
|
||||
- ✓ All endpoints working
|
||||
- ✓ Rate limiting added (optional)
|
||||
- ✓ Error handling complete
|
||||
|
||||
- [ ] **Sync Workers**
|
||||
- ✓ Changes feed running every 2 min
|
||||
- ✓ Keyword search running every 15 min
|
||||
- ✓ Checkpoints updating correctly
|
||||
|
||||
- [ ] **Frontend**
|
||||
- ✓ All pages loading real data
|
||||
- ✓ Search working
|
||||
- ✓ Mobile responsive
|
||||
|
||||
- [ ] **Monitoring**
|
||||
- ✓ Health check endpoint live
|
||||
- ✓ Uptime monitoring active
|
||||
- ✓ Error tracking active
|
||||
|
||||
---
|
||||
|
||||
### 6.2 Launch Steps
|
||||
|
||||
- [ ] **Deploy to production**
|
||||
```bash
|
||||
git push origin main
|
||||
# Vercel auto-deploys
|
||||
```
|
||||
|
||||
- [ ] **Verify deployment**
|
||||
- Check all pages load
|
||||
- Check API endpoints work
|
||||
- Check cron jobs run
|
||||
|
||||
- [ ] **Publish announcement**
|
||||
- Tweet/post about TPMJS
|
||||
- Explain how to add `tpmjs` field
|
||||
- Share validation endpoint
|
||||
|
||||
- [ ] **Monitor for 24 hours**
|
||||
- Watch error logs
|
||||
- Check sync is working
|
||||
- Fix any issues
|
||||
|
||||
---
|
||||
|
||||
## 📋 Phase 7: Post-Launch (Ongoing)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [ ] **Semantic search**
|
||||
- Add embeddings to tools table
|
||||
- Use OpenAI/Cohere for semantic search
|
||||
|
||||
- [ ] **Usage analytics**
|
||||
- Track tool views
|
||||
- Track search queries
|
||||
- Popular tools widget
|
||||
|
||||
- [ ] **Tool recommendations**
|
||||
- "Similar tools" section
|
||||
- "You might also like"
|
||||
|
||||
- [ ] **GitHub Actions**
|
||||
- Validate `tpmjs` field in CI
|
||||
- Auto-comment validation results
|
||||
|
||||
- [ ] **NPM webhooks**
|
||||
- Listen for package updates
|
||||
- Immediate sync (instead of polling)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Success Criteria
|
||||
|
||||
Check these metrics after launch:
|
||||
|
||||
### Week 1
|
||||
- [ ] 10+ official tools listed
|
||||
- [ ] All sync jobs running successfully
|
||||
- [ ] Zero API errors
|
||||
|
||||
### Month 1
|
||||
- [ ] 50+ official tools
|
||||
- [ ] 5+ package authors using TPMJS
|
||||
- [ ] <200ms API response time (p95)
|
||||
|
||||
### Month 3
|
||||
- [ ] 200+ tools
|
||||
- [ ] 20+ package authors
|
||||
- [ ] Community contributions
|
||||
|
||||
### Month 6
|
||||
- [ ] 1000+ tools
|
||||
- [ ] 50+ active package authors
|
||||
- [ ] Established as go-to AI tool registry
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Ongoing Maintenance
|
||||
|
||||
Weekly:
|
||||
- [ ] Check sync logs for errors
|
||||
- [ ] Review new tools for quality
|
||||
- [ ] Update documentation
|
||||
|
||||
Monthly:
|
||||
- [ ] Database optimization (indexes, vacuum)
|
||||
- [ ] Review and adjust quality scoring
|
||||
- [ ] Update NPM_MIRROR.md with learnings
|
||||
|
||||
---
|
||||
|
||||
## 📚 Key Documents
|
||||
|
||||
**Read frequently during implementation:**
|
||||
|
||||
1. **NPM_MIRROR.md** - Complete architecture reference
|
||||
- Database schema
|
||||
- API specifications
|
||||
- Validation rules
|
||||
- Quality scoring
|
||||
- All examples
|
||||
|
||||
2. **This checklist** - Implementation order and verification steps
|
||||
|
||||
3. **Plan file** - `.claude/plans/goofy-inventing-stearns.md` - Detailed planning notes
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Ready to Build
|
||||
|
||||
This checklist is your complete implementation guide. Work through it phase by phase, checking off items as you go.
|
||||
|
||||
**Start with Phase 1, Step 1.1** and work sequentially. Each step has verification instructions to ensure it's working before moving on.
|
||||
|
||||
Good luck! 🎉
|
||||
331
LAUNCH_REVIEW.md
Normal file
331
LAUNCH_REVIEW.md
Normal file
|
|
@ -0,0 +1,331 @@
|
|||
# TPMJS Launch Review & Checklist
|
||||
|
||||
**STATUS: COMPLETED** - All critical issues have been fixed.
|
||||
|
||||
A comprehensive review of all public-facing content for Hacker News launch readiness.
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
**Overall Readiness: 7/10 - Needs Work Before Launch**
|
||||
|
||||
The website has excellent technical content and professional design, but fails the "5-second test" - a first-time visitor cannot quickly understand what TPMJS is or why they need it. The documentation is strong for existing users but assumes too much prior knowledge about AI agents and tooling.
|
||||
|
||||
### Critical Issues (Must Fix)
|
||||
1. **Landing page doesn't explain what TPMJS is** - Hero section uses jargon without definition
|
||||
2. **"Tool" vs "Package" never defined** - Core concepts assumed, not explained
|
||||
3. **Knowledge gaps** - Assumes familiarity with AI agents, Zod, semantic search
|
||||
4. **Category inconsistency** - HOW_TO_PUBLISH and NPM_MIRROR have different category lists
|
||||
5. **NPM_MIRROR.md conflicts with other docs** - Appears outdated, creates confusion
|
||||
|
||||
### What's Working Well
|
||||
- Publishing guide (HOW_TO_PUBLISH_A_TOOL.md) is excellent
|
||||
- How It Works page has great technical depth
|
||||
- Developer testimonials are concrete with real metrics
|
||||
- No obvious AI-generated language on website
|
||||
- Code examples are practical and well-placed
|
||||
|
||||
---
|
||||
|
||||
## The 5-Second Test: FAILED
|
||||
|
||||
**Question:** Can a developer understand what TPMJS is within 5 seconds of landing on the homepage?
|
||||
|
||||
**Answer:** No.
|
||||
|
||||
### What They See First
|
||||
```
|
||||
TOOL REGISTRY FOR AI AGENTS
|
||||
Discover, share, and integrate tools that give your agents superpowers
|
||||
```
|
||||
|
||||
### What's Missing
|
||||
- What is a "tool" in this context?
|
||||
- What is an "AI agent"?
|
||||
- Why would I use this vs npm directly?
|
||||
- Is this a package manager? A marketplace? An SDK?
|
||||
|
||||
### The "Aha Moment" is Unclear
|
||||
A visitor still doesn't know:
|
||||
- WHO should use TPMJS (tool builders? agent developers? both?)
|
||||
- WHEN they would use it (at development time? runtime?)
|
||||
- HOW it differs from regular npm packages
|
||||
- WHY they can't just install packages normally
|
||||
|
||||
---
|
||||
|
||||
## Page-by-Page Clarity Ratings
|
||||
|
||||
| Page | Clarity | Human Feel | Issues |
|
||||
|------|---------|------------|--------|
|
||||
| **Landing Page** | 5/10 | Yes | No 5-second explanation, jargon-heavy |
|
||||
| **Hero Section** | 3/10 | Yes | "Tool registry" undefined, circular language |
|
||||
| **Problem Section** | 7/10 | Yes | Best section - concrete pain points |
|
||||
| **Vision Section** | 5/10 | Yes | "Semantic search" unexplained |
|
||||
| **Developer Stories** | 7/10 | Yes | Good metrics, but code unexplained |
|
||||
| **Publish Section** | 6/10 | Yes | Assumes visitor is a tool builder |
|
||||
| **How It Works** | 9/10 | Excellent | Minor density issues |
|
||||
| **FAQ** | 8/10 | Yes | Missing some common questions |
|
||||
| **Publish Guide** | 8.5/10 | Yes | Tier system could be clearer upfront |
|
||||
| **Spec Page** | 8.5/10 | Yes | Assumes Zod/AI SDK knowledge |
|
||||
| **Docs Page** | 9/10 | Excellent | Overwhelming length |
|
||||
| **SDK Page** | 8.5/10 | Yes | Assumes Vercel AI SDK familiarity |
|
||||
| **Privacy** | 8/10 | Yes | Hardcoded email address |
|
||||
| **Terms** | 8/10 | Yes | Hardcoded date |
|
||||
|
||||
---
|
||||
|
||||
## Documentation Clarity Ratings
|
||||
|
||||
| Document | Clarity | Necessary | Critical Issues |
|
||||
|----------|---------|-----------|-----------------|
|
||||
| README.md | 8/10 | YES | Missing "what is TPMJS" explanation |
|
||||
| HOW_TO_PUBLISH_A_TOOL.md | 9/10 | YES | Minor - excellent overall |
|
||||
| DEPLOYMENT.md | 8/10 | YES | Confusing exit code explanation |
|
||||
| QUALITY-GATES.md | 7/10 | OPTIONAL | Could merge into README |
|
||||
| MANUAL_TOOLS.md | 8.5/10 | YES | Good for maintainers |
|
||||
| NPM_MIRROR.md | 6.5/10 | **REMOVE** | **Conflicts with other docs, appears outdated** |
|
||||
|
||||
---
|
||||
|
||||
## Knowledge Gaps (Things Visitors Won't Understand)
|
||||
|
||||
### Not Explained Anywhere
|
||||
1. **What is an "AI Agent"?** - The entire site assumes you know this
|
||||
2. **What is a "Tool" vs a "Package"?** - Used interchangeably, never defined
|
||||
3. **Why semantic search matters** - Just says "semantic" without explaining benefit
|
||||
4. **What frameworks are supported** - Mentioned in FAQ but not prominently
|
||||
5. **The Package → Tool relationship** - Can one package have multiple tools?
|
||||
|
||||
### Assumed Technical Knowledge
|
||||
- Zod schemas (used throughout, never introduced)
|
||||
- AI SDK tool format (referenced as "standard" but what standard?)
|
||||
- esm.sh and Deno sandboxing (mentioned in How It Works)
|
||||
- BM25 ranking algorithm (mentioned in docs)
|
||||
|
||||
### Missing Use Cases
|
||||
- "Use TPMJS when..." section doesn't exist
|
||||
- No comparison to alternatives (why not just npm?)
|
||||
- No "before/after" showing the problem solved
|
||||
|
||||
---
|
||||
|
||||
## Human-Written Assessment
|
||||
|
||||
### Reads Like Human: YES ✓
|
||||
- Developer stories use specific metrics ("500 lines to 3")
|
||||
- Technical explanations show genuine understanding
|
||||
- Problem section addresses real pain points
|
||||
- No buzzword soup or meaningless marketing phrases
|
||||
|
||||
### Minor AI-Sounding Phrases Found
|
||||
| Location | Phrase | Issue |
|
||||
|----------|--------|-------|
|
||||
| NPM_MIRROR.md:7 | "automated NPM-integrated registry" | Marketing speak |
|
||||
| NPM_MIRROR.md:27 | "✨ Listed automatically" | Emoji in technical doc |
|
||||
| NPM_MIRROR.md:500 | "Built with ❤️" | Remove emoji |
|
||||
| HOW_TO_PUBLISH:389 | "AI-friendly descriptions" | Vague - what makes it "AI-friendly"? |
|
||||
| Vision Section | "gives agents superpowers" | Metaphor without substance |
|
||||
|
||||
---
|
||||
|
||||
## Critical Inconsistencies Found
|
||||
|
||||
### Category Lists Don't Match
|
||||
**HOW_TO_PUBLISH_A_TOOL.md says:**
|
||||
```
|
||||
text-analysis, code-generation, data-processing,
|
||||
image-generation, audio-processing, search, integration, other
|
||||
```
|
||||
|
||||
**NPM_MIRROR.md says:**
|
||||
```
|
||||
web-scraping, data-processing, file-operations, communication,
|
||||
database, api-integration, image-processing, text-analysis,
|
||||
automation, ai-ml, security, monitoring
|
||||
```
|
||||
|
||||
**These are completely different!** Which is correct?
|
||||
|
||||
### Quality Score Formula Conflicts
|
||||
- HOW_TO_PUBLISH: "Tier: Rich (1.0) > Basic (0.5) > Minimal (0.25)"
|
||||
- MANUAL_TOOLS: "Rich tier tools get 4x quality score multiplier"
|
||||
- NPM_MIRROR: Different formula entirely
|
||||
|
||||
### Field Names Inconsistent
|
||||
- `name` used in MANUAL_TOOLS but not in HOW_TO_PUBLISH
|
||||
- Deprecated fields (`parameters`, `returns`) mentioned but unclear when deprecated
|
||||
|
||||
---
|
||||
|
||||
## Hardcoded Values to Fix
|
||||
|
||||
| File | Issue | Line |
|
||||
|------|-------|------|
|
||||
| FAQ, Privacy, Terms | `thomasalwyndavis@gmail.com` hardcoded | Multiple |
|
||||
| Privacy, Terms | Date "December 14, 2025" hardcoded | Multiple |
|
||||
| Changelog page | Package list hardcoded in code | ~95-110 |
|
||||
| Developer Stories | Fictional company names (Support.ai, DocFlow) | homePageData.ts |
|
||||
|
||||
---
|
||||
|
||||
## Launch Checklist
|
||||
|
||||
### Must Fix Before Launch (Blocking) - ALL DONE ✓
|
||||
|
||||
- [x] **Rewrite hero section** to explain TPMJS in one sentence
|
||||
- Current: "TOOL REGISTRY FOR AI AGENTS"
|
||||
- Suggested: "TPMJS lets AI agents discover and use npm packages as tools at runtime. Publish once to npm, get discovered automatically."
|
||||
|
||||
- [x] **Add "What is TPMJS?" section** to landing page
|
||||
- Define: What is an AI agent?
|
||||
- Define: What is a "tool" in this context?
|
||||
- Explain: Why not just use npm directly?
|
||||
- Show: 3-step "how it works" visual
|
||||
|
||||
- [x] **Reconcile category lists** between docs (deleted NPM_MIRROR.md)
|
||||
- Pick one canonical list
|
||||
- Update all docs to match
|
||||
- Add categories to types package
|
||||
|
||||
- [x] **Delete or archive NPM_MIRROR.md** (deleted)
|
||||
- Conflicts with HOW_TO_PUBLISH
|
||||
- Appears to be old design doc, not current state
|
||||
- Move to `/docs/internal/` if historical value
|
||||
|
||||
- [x] **Fix hardcoded values** (emails → hello@tpmjs.com, dates → December 2024)
|
||||
- Email addresses → environment variable
|
||||
- Dates → dynamic or remove
|
||||
- Package lists → generated from filesystem
|
||||
|
||||
### Should Fix (High Priority) - MOSTLY DONE
|
||||
|
||||
- [x] **Add "Use TPMJS when..." section** to landing page (covered in "What is TPMJS?" section)
|
||||
- List concrete scenarios: "Building a chatbot that needs web access"
|
||||
- "Agent that processes different file formats"
|
||||
- "Tool that should be discoverable by other agents"
|
||||
|
||||
- [x] **Explain Package vs Tool distinction** (covered in "What is TPMJS?" section)
|
||||
- Add glossary or definitions section
|
||||
- Clarify: 1 package can have N tools
|
||||
|
||||
- [x] **Add framework compatibility section** (mentioned in hero and publish sections)
|
||||
- Which AI frameworks work with TPMJS?
|
||||
- Are there adapters needed?
|
||||
- Show code for each framework
|
||||
|
||||
- [ ] **Simplify developer stories code**
|
||||
- Current code snippet unexplained:
|
||||
```js
|
||||
const agent = new Agent({ tools: await tpmjs.search(...) })
|
||||
```
|
||||
- Add: Where does `Agent` come from? What's happening here?
|
||||
|
||||
- [x] **Add README context** (completely rewritten with clear explanation)
|
||||
- What is TPMJS for?
|
||||
- Link to tpmjs.com
|
||||
- Explain discovery mechanism
|
||||
|
||||
### Nice to Have (Post-Launch)
|
||||
|
||||
- [ ] Add video walkthrough (30-60 seconds)
|
||||
- [ ] Interactive playground link from homepage
|
||||
- [ ] "Compare to alternatives" section
|
||||
- [ ] Case studies with real company names
|
||||
- [ ] Quick links sidebar for docs page
|
||||
- [ ] Status badges for each quality gate
|
||||
|
||||
---
|
||||
|
||||
## Recommended Hero Section Rewrite
|
||||
|
||||
### Current
|
||||
```
|
||||
TOOL REGISTRY FOR AI AGENTS
|
||||
Discover, share, and integrate tools that give your agents superpowers
|
||||
The registry for AI tools
|
||||
```
|
||||
|
||||
### Suggested
|
||||
```
|
||||
MAKE YOUR AI AGENT SMARTER
|
||||
TPMJS connects your AI agent to 2,500+ npm packages at runtime.
|
||||
No config files. No manual imports. Just describe what you need.
|
||||
|
||||
"Find me a tool that can scrape websites" → Your agent gets web-scraper
|
||||
"I need to process markdown" → Your agent gets markdown-formatter
|
||||
|
||||
Publish your npm package → It's discoverable by every AI agent in 15 minutes.
|
||||
```
|
||||
|
||||
This version:
|
||||
- Explains what it DOES (connects agents to npm packages)
|
||||
- Shows HOW it works (natural language → tool)
|
||||
- States the VALUE (no config, automatic discovery)
|
||||
- Gives concrete examples
|
||||
|
||||
---
|
||||
|
||||
## Recommended "What is TPMJS?" Section
|
||||
|
||||
Add after hero, before featured tools:
|
||||
|
||||
```markdown
|
||||
## What is TPMJS?
|
||||
|
||||
**The Problem:** AI agents need tools (web scraping, file processing, API calls)
|
||||
but developers must manually configure each one. As the ecosystem grows,
|
||||
this becomes unmanageable.
|
||||
|
||||
**The Solution:** TPMJS is a registry that automatically discovers npm packages
|
||||
designed for AI agents. Agents can search for tools by description and load them
|
||||
at runtime.
|
||||
|
||||
**For Tool Builders:** Add `tpmjs` keyword to your package.json.
|
||||
Your tool appears on tpmjs.com within 15 minutes.
|
||||
|
||||
**For Agent Developers:** Use semantic search to find tools:
|
||||
```javascript
|
||||
import { searchRegistry } from '@tpmjs/sdk';
|
||||
const tools = await searchRegistry('send emails and slack messages');
|
||||
// Returns: email-sender, slack-notifier, ...
|
||||
```
|
||||
|
||||
**One registry. Thousands of tools. Zero configuration.**
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Final Assessment
|
||||
|
||||
### Ready for Launch?
|
||||
**Not yet.** The core product is solid but messaging fails first-time visitors.
|
||||
|
||||
### Estimated Fixes
|
||||
- Hero rewrite: 30 minutes
|
||||
- "What is TPMJS?" section: 1 hour
|
||||
- Category reconciliation: 1 hour
|
||||
- Hardcoded values: 30 minutes
|
||||
- README updates: 30 minutes
|
||||
- NPM_MIRROR cleanup: 15 minutes
|
||||
|
||||
**Total: ~4 hours of work**
|
||||
|
||||
### After Fixes
|
||||
The site will be launch-ready. The technical content is excellent - it just needs a better front door.
|
||||
|
||||
---
|
||||
|
||||
## Appendix: Positive Highlights
|
||||
|
||||
Things that are already great and should NOT change:
|
||||
|
||||
1. **How It Works page** - Excellent technical depth, clear structure
|
||||
2. **Publishing guide** - Best-in-class documentation, real examples
|
||||
3. **Problem section** - Concrete pain points, relatable issues
|
||||
4. **Spec page** - Clear field reference, good validation info
|
||||
5. **SDK documentation** - Quick start is excellent
|
||||
6. **Code examples throughout** - Practical, copy-pasteable
|
||||
7. **Visual design** - Clean, professional, developer-focused
|
||||
8. **Quality scoring explanation** - Transparent, well-documented
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2024-2025 TPMJS
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
293
MANUAL_TOOLS.md
Normal file
293
MANUAL_TOOLS.md
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
# Manual Tools Registry
|
||||
|
||||
## Overview
|
||||
|
||||
This system allows TPMJS to include high-quality tools that don't follow the standard `tpmjs` field specification in their package.json. These tools are manually curated and synced to the database.
|
||||
|
||||
## Why Manual Tools?
|
||||
|
||||
Some excellent tools (like Vercel's code execution, Exa search, Firecrawl, etc.) don't include the `tpmjs` field in their package.json. Rather than wait for these package maintainers to adopt the spec, we manually curate metadata for these tools.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Files
|
||||
|
||||
1. **`manual-tools.ts`** - The registry of manually curated tools
|
||||
2. **`sync-manual-tools.ts`** - Script to sync manual tools to database
|
||||
3. **`MANUAL_TOOLS.md`** - This documentation
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Manual Tool Registry** (`manual-tools.ts`)
|
||||
- Exports a `manualTools` array with metadata for each tool
|
||||
- Each entry includes npm package name, export name, category, description, parameters, etc.
|
||||
- Follows the same schema as the standard `tpmjs` field
|
||||
|
||||
2. **Sync Script** (`sync-manual-tools.ts`)
|
||||
- Fetches latest package metadata from npm
|
||||
- Combines npm metadata with manual metadata
|
||||
- Upserts Package + Tool records to database
|
||||
- Marks tools with `discoveryMethod: 'manual'`
|
||||
|
||||
3. **Database Storage**
|
||||
- Manual tools stored in same `packages` and `tools` tables as auto-discovered tools
|
||||
- No special handling needed in API or frontend
|
||||
- `discoveryMethod: 'manual'` field distinguishes them
|
||||
|
||||
## Adding a New Manual Tool
|
||||
|
||||
### Step 1: Add to Registry
|
||||
|
||||
Edit `manual-tools.ts` and add a new entry:
|
||||
|
||||
```typescript
|
||||
{
|
||||
npmPackageName: 'example-package',
|
||||
category: 'search',
|
||||
frameworks: ['vercel-ai'],
|
||||
name: 'exampleTool',
|
||||
description: 'A clear, concise description of what this tool does',
|
||||
|
||||
// Optional but recommended for 'rich' tier
|
||||
parameters: [
|
||||
{
|
||||
name: 'query',
|
||||
type: 'string',
|
||||
description: 'The search query',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
|
||||
returns: {
|
||||
type: 'array',
|
||||
description: 'Array of search results',
|
||||
},
|
||||
|
||||
aiAgent: {
|
||||
useCase: 'Use when you need to search for X',
|
||||
limitations: 'Rate limits apply',
|
||||
examples: [
|
||||
'Search for current news',
|
||||
'Find specific information',
|
||||
],
|
||||
},
|
||||
|
||||
// Environment variables
|
||||
env: [
|
||||
{
|
||||
name: 'EXAMPLE_API_KEY',
|
||||
description: 'API key for the service',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
|
||||
// Additional metadata
|
||||
tags: ['search', 'web'],
|
||||
docsUrl: 'https://example.com/docs',
|
||||
apiKeyUrl: 'https://example.com/api-keys',
|
||||
websiteUrl: 'https://example.com',
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Run Sync Script
|
||||
|
||||
```bash
|
||||
# From repository root
|
||||
pnpm tsx sync-manual-tools.ts
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Fetch the package from npm
|
||||
2. Create/update Package record
|
||||
3. Create/update Tool record(s)
|
||||
4. Set `discoveryMethod: 'manual'`
|
||||
|
||||
### Step 3: Verify
|
||||
|
||||
Check that the tool appears on tpmjs.com:
|
||||
|
||||
```bash
|
||||
# Start dev server
|
||||
pnpm dev --filter=@tpmjs/web
|
||||
|
||||
# Visit http://localhost:3000/tool/tool-search
|
||||
# Search for your package name
|
||||
```
|
||||
|
||||
## Multi-Tool Packages
|
||||
|
||||
If a package exports multiple tools, add multiple entries with the same `npmPackageName` but different `name`:
|
||||
|
||||
```typescript
|
||||
{
|
||||
npmPackageName: 'firecrawl-aisdk',
|
||||
name: 'scrapeTool',
|
||||
description: 'Scrape websites...',
|
||||
// ...
|
||||
},
|
||||
{
|
||||
npmPackageName: 'firecrawl-aisdk',
|
||||
name: 'searchTool',
|
||||
description: 'Search the web...',
|
||||
// ...
|
||||
},
|
||||
{
|
||||
npmPackageName: 'firecrawl-aisdk',
|
||||
name: 'crawlTool',
|
||||
description: 'Crawl entire websites...',
|
||||
// ...
|
||||
},
|
||||
```
|
||||
|
||||
## Tier Calculation
|
||||
|
||||
Tools are automatically assigned a tier:
|
||||
|
||||
- **Rich tier**: Has `parameters` OR `returns` OR `aiAgent` fields
|
||||
- **Minimal tier**: Only has basic metadata
|
||||
|
||||
Rich tier tools get 4x quality score multiplier, so add detailed metadata when possible.
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Updating Manual Tools
|
||||
|
||||
1. Edit the entry in `manual-tools.ts`
|
||||
2. Run `pnpm tsx sync-manual-tools.ts`
|
||||
3. The upsert will update existing records
|
||||
|
||||
### Removing Manual Tools
|
||||
|
||||
1. Remove the entry from `manual-tools.ts`
|
||||
2. Manually delete from database OR wait for metrics sync to mark as stale
|
||||
|
||||
### Version Updates
|
||||
|
||||
The sync script automatically fetches the latest version from npm unless you specify `npmVersion` in the manual tool entry.
|
||||
|
||||
## Production Deployment
|
||||
|
||||
### Option 1: Manual Sync on Deploy
|
||||
|
||||
Add to your deployment workflow:
|
||||
|
||||
```yaml
|
||||
# .github/workflows/deploy.yml
|
||||
- name: Sync manual tools
|
||||
run: pnpm tsx sync-manual-tools.ts
|
||||
env:
|
||||
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||
```
|
||||
|
||||
### Option 2: Scheduled Sync
|
||||
|
||||
Create a cron job or GitHub Action to sync periodically:
|
||||
|
||||
```yaml
|
||||
# .github/workflows/sync-manual.yml
|
||||
name: Sync Manual Tools
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * 0' # Weekly on Sunday
|
||||
workflow_dispatch: # Manual trigger
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v2
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
- run: pnpm install
|
||||
- run: pnpm tsx sync-manual-tools.ts
|
||||
env:
|
||||
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||
```
|
||||
|
||||
### Option 3: API Endpoint
|
||||
|
||||
Create a sync endpoint (similar to keyword/changes sync):
|
||||
|
||||
```typescript
|
||||
// apps/web/src/app/api/sync/manual/route.ts
|
||||
import { manualTools } from '@/manual-tools';
|
||||
// ... sync logic
|
||||
|
||||
export async function POST(request: Request) {
|
||||
// Verify CRON_SECRET
|
||||
// Run manual sync
|
||||
// Return results
|
||||
}
|
||||
```
|
||||
|
||||
## Currently Included Manual Tools
|
||||
|
||||
As of this documentation:
|
||||
|
||||
- **ai-sdk-tool-code-execution** - Vercel Sandbox code execution
|
||||
- **@exalabs/ai-sdk** - Exa web search
|
||||
- **@parallel-web/ai-sdk-tools** - Parallel search and extraction (2 tools)
|
||||
- **ctx-zip** - MCP + Vercel Sandbox integration
|
||||
- **@perplexity-ai/ai-sdk** - Perplexity search
|
||||
- **@tavily/ai-sdk** - Tavily web research
|
||||
- **firecrawl-aisdk** - Firecrawl scraping, search, crawling (3 tools)
|
||||
- **bedrock-agentcore** - AWS Bedrock code interpreter and browser (2 tools)
|
||||
- **@superagent-ai/ai-sdk** - Superagent security tools (3 tools)
|
||||
- **@valyu/ai-sdk** - Valyu domain-specific search tools (8 tools)
|
||||
|
||||
**Total: 24 manually curated tools across 10 packages**
|
||||
|
||||
## FAQ
|
||||
|
||||
### Why not just ask package maintainers to add the tpmjs field?
|
||||
|
||||
We should! But:
|
||||
1. Some packages are from large companies (Vercel, AWS, etc.) with slow adoption cycles
|
||||
2. We want these tools available on TPMJS now
|
||||
3. Manual curation lets us provide better metadata than package authors might
|
||||
|
||||
### Will manual tools be replaced by auto-discovered ones?
|
||||
|
||||
Yes! If a package adds a proper `tpmjs` field, the auto-discovery sync will update it with `discoveryMethod: 'keyword'` or `'changes-feed'`. Manual entries can then be removed from `manual-tools.ts`.
|
||||
|
||||
### Can I mix manual and auto-discovered tools from the same package?
|
||||
|
||||
Yes. If a package has some tools in the `tpmjs` field but is missing others, you can manually add the missing ones. The sync scripts will coexist peacefully.
|
||||
|
||||
### How do I know if a tool is manually curated?
|
||||
|
||||
Check the `discoveryMethod` field in the database:
|
||||
- `'manual'` = Manually curated
|
||||
- `'keyword'` = Auto-discovered via keyword search
|
||||
- `'changes-feed'` = Auto-discovered via npm changes feed
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Complete Metadata** - Provide as much metadata as possible for rich tier
|
||||
2. **Accurate Descriptions** - Tool descriptions should be clear and specific
|
||||
3. **AI-Friendly** - Write `aiAgent.useCase` as guidance for LLMs
|
||||
4. **Keep Updated** - Periodically check if packages have added native `tpmjs` support
|
||||
5. **Link to Docs** - Always include `docsUrl` when available
|
||||
6. **API Key URLs** - Include `apiKeyUrl` for tools requiring authentication
|
||||
|
||||
## Contributing
|
||||
|
||||
To contribute new manual tools:
|
||||
|
||||
1. Fork the repository
|
||||
2. Add your tool to `manual-tools.ts`
|
||||
3. Test with `pnpm tsx sync-manual-tools.ts`
|
||||
4. Open a pull request with:
|
||||
- Why this tool should be included
|
||||
- Link to the npm package
|
||||
- Screenshot of it working in TPMJS
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [HOW_TO_PUBLISH_A_TOOL.md](./HOW_TO_PUBLISH_A_TOOL.md) - Standard tpmjs field spec
|
||||
- [CLAUDE.md](./CLAUDE.md) - General project documentation
|
||||
- [packages/types/src/tpmjs.ts](./packages/types/src/tpmjs.ts) - TypeScript schema definitions
|
||||
500
NPM_MIRROR.md
500
NPM_MIRROR.md
|
|
@ -1,500 +0,0 @@
|
|||
# TPMJS NPM-Integrated Registry Architecture
|
||||
|
||||
> **Automated tool discovery from NPM with zero-click submission**
|
||||
|
||||
## Vision
|
||||
|
||||
Transform TPMJS from a manual directory into an **automated NPM-integrated registry** where package authors simply publish to NPM with a `tpmjs` field in their `package.json` and their tools are discovered and listed within seconds—no manual submission, no forms, no waiting.
|
||||
|
||||
## Quick Start for Package Authors
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-awesome-tool",
|
||||
"version": "1.0.0",
|
||||
"keywords": ["tpmjs-tool"],
|
||||
"tpmjs": {
|
||||
"category": "web-scraping",
|
||||
"description": "Extract product data from e-commerce websites with ease",
|
||||
"example": "const data = await scraper.extract('https://shop.com')"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
npm publish
|
||||
# ✨ Listed automatically within 15 minutes (keyword) or seconds (changes feed)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
NPM Ecosystem
|
||||
↓
|
||||
Changes Feed + Keyword Search
|
||||
↓
|
||||
Package Validator (Zod)
|
||||
↓
|
||||
PostgreSQL Database
|
||||
↓
|
||||
Next.js API Routes
|
||||
↓
|
||||
TPMJS Web App
|
||||
```
|
||||
|
||||
### Core Components
|
||||
|
||||
1. **NPM Sync Service** (Node.js) - Monitors NPM registry for new packages
|
||||
2. **PostgreSQL Database** - Stores validated tool metadata
|
||||
3. **Next.js API** - Serves tool data with search/filtering
|
||||
4. **Web Frontend** - Browse, search, and discover tools
|
||||
|
||||
---
|
||||
|
||||
## Discovery Mechanism: Hybrid Approach
|
||||
|
||||
### Method 1: Keyword Search (Official)
|
||||
- Search NPM for packages with `tpmjs-tool` keyword
|
||||
- Runs every 15 minutes via cron
|
||||
- Packages marked as "Official"
|
||||
|
||||
### Method 2: Changes Feed (Automatic)
|
||||
- Monitors `replicate.npmjs.com/registry/_changes` in real-time
|
||||
- Detects packages with `tpmjs` field instantly
|
||||
- Packages marked as "Community" (unless they also have keyword)
|
||||
|
||||
### Why Hybrid?
|
||||
- **Keywords** = Clear opt-in, queryable, respects NPM conventions
|
||||
- **Changes Feed** = Real-time, catches packages without keywords
|
||||
- **Together** = Best discoverability with fallback
|
||||
|
||||
---
|
||||
|
||||
## The "tpmjs" Field: Tiered Schema
|
||||
|
||||
### Minimal Tier (Required)
|
||||
|
||||
```json
|
||||
{
|
||||
"tpmjs": {
|
||||
"category": "web-scraping",
|
||||
"description": "Extract structured data from websites using CSS selectors",
|
||||
"example": "const data = await tool.scrape({ url: 'https://example.com', selector: '.price' })"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Categories:**
|
||||
- web-scraping
|
||||
- data-processing
|
||||
- file-operations
|
||||
- communication
|
||||
- database
|
||||
- api-integration
|
||||
- image-processing
|
||||
- text-analysis
|
||||
- automation
|
||||
- ai-ml
|
||||
- security
|
||||
- monitoring
|
||||
|
||||
### Rich Tier (Optional)
|
||||
|
||||
Extend with any of these optional fields:
|
||||
|
||||
```json
|
||||
{
|
||||
"tpmjs": {
|
||||
// ... Required fields ...
|
||||
|
||||
"parameters": [
|
||||
{
|
||||
"name": "url",
|
||||
"type": "string",
|
||||
"description": "Target URL to scrape",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"returns": {
|
||||
"type": "object",
|
||||
"description": "Extracted data matching the selector"
|
||||
},
|
||||
"authentication": {
|
||||
"required": false,
|
||||
"type": "api-key",
|
||||
"envVar": "SCRAPER_API_KEY",
|
||||
"docsUrl": "https://docs.example.com/auth"
|
||||
},
|
||||
"pricing": {
|
||||
"model": "freemium",
|
||||
"freeLimit": "100 requests/month",
|
||||
"paidUrl": "https://example.com/pricing"
|
||||
},
|
||||
"frameworks": ["vercel-ai", "langchain", "llamaindex"],
|
||||
"links": {
|
||||
"documentation": "https://docs.example.com",
|
||||
"playground": "https://example.com/try",
|
||||
"repository": "https://github.com/user/repo"
|
||||
},
|
||||
"tags": ["web", "scraping", "html", "css"],
|
||||
"status": "stable",
|
||||
"aiAgent": {
|
||||
"useCase": "Use when agent needs to extract data from websites",
|
||||
"limitations": "Cannot handle JavaScript-heavy SPAs"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
|
||||
### Tools Table
|
||||
|
||||
```sql
|
||||
CREATE TABLE tools (
|
||||
-- NPM Metadata
|
||||
npm_package_name VARCHAR(214) UNIQUE NOT NULL,
|
||||
npm_version VARCHAR(50) NOT NULL,
|
||||
npm_published_at TIMESTAMP NOT NULL,
|
||||
npm_description TEXT,
|
||||
npm_repository JSONB,
|
||||
npm_homepage TEXT,
|
||||
npm_license VARCHAR(50),
|
||||
|
||||
-- TPMJS Metadata
|
||||
category VARCHAR(50) NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
example TEXT NOT NULL,
|
||||
parameters JSONB,
|
||||
authentication JSONB,
|
||||
pricing JSONB,
|
||||
frameworks TEXT[],
|
||||
links JSONB,
|
||||
tags TEXT[],
|
||||
status VARCHAR(20),
|
||||
|
||||
-- Discovery
|
||||
discovery_method VARCHAR(20) NOT NULL, -- 'keyword' | 'changes-feed'
|
||||
is_official BOOLEAN DEFAULT false,
|
||||
tier VARCHAR(20) NOT NULL, -- 'minimal' | 'rich'
|
||||
|
||||
-- Metrics
|
||||
npm_downloads_last_month INTEGER DEFAULT 0,
|
||||
github_stars INTEGER DEFAULT 0,
|
||||
quality_score DECIMAL(3,2), -- 0.00 to 1.00
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sync Service Architecture
|
||||
|
||||
### Workers
|
||||
|
||||
**1. Changes Feed Worker**
|
||||
- Connects to `replicate.npmjs.com/registry/_changes`
|
||||
- Receives real-time change events
|
||||
- Fetches package metadata for each change
|
||||
- Checks for `tpmjs` field
|
||||
- Validates and inserts to database
|
||||
|
||||
**2. Keyword Search Worker**
|
||||
- Runs every 15 minutes (cron)
|
||||
- Searches `/-/v1/search?text=keywords:tpmjs-tool`
|
||||
- Processes all results
|
||||
- Marks as "Official"
|
||||
|
||||
**3. Metrics Worker** (Optional Phase 4)
|
||||
- Updates download counts from NPM API
|
||||
- Fetches GitHub stars
|
||||
- Calculates quality scores
|
||||
|
||||
### Package Processing Pipeline
|
||||
|
||||
```
|
||||
1. Fetch package metadata from NPM
|
||||
2. Extract `tpmjs` field from latest version
|
||||
3. Validate against Zod schema
|
||||
4. If valid → Insert/Update database
|
||||
5. If invalid → Log error
|
||||
6. If no field → Skip
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Routes
|
||||
|
||||
### GET /api/tools
|
||||
Search and list tools
|
||||
|
||||
**Query Parameters:**
|
||||
- `q` - Search query
|
||||
- `category` - Filter by category
|
||||
- `official` - Only official tools (true/false)
|
||||
- `limit` - Results per page (default 20)
|
||||
- `offset` - Pagination offset
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"tools": [...],
|
||||
"pagination": {
|
||||
"total": 150,
|
||||
"limit": 20,
|
||||
"offset": 0,
|
||||
"hasMore": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/tools/[id]
|
||||
Get tool details by ID
|
||||
|
||||
### POST /api/tools/validate
|
||||
Validate a `tpmjs` field before publishing
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"category": "web-scraping",
|
||||
"description": "...",
|
||||
"example": "..."
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"valid": true,
|
||||
"tier": "minimal",
|
||||
"errors": []
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/stats
|
||||
Registry statistics
|
||||
|
||||
```json
|
||||
{
|
||||
"totalTools": 2847,
|
||||
"officialTools": 150,
|
||||
"categories": {
|
||||
"web-scraping": 320,
|
||||
"communication": 280,
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quality Scoring Algorithm
|
||||
|
||||
Tools are scored 0.00 to 1.00 based on:
|
||||
|
||||
- **Base validity** (0.3) - Has valid schema
|
||||
- **Tier** (0.1-0.2) - Rich tier > Minimal tier
|
||||
- **NPM downloads** (0.2) - Based on monthly downloads
|
||||
- **GitHub stars** (0.15) - Repository popularity
|
||||
- **Documentation** (0.1) - Has docs URL
|
||||
- **Example quality** (0.05) - Example length > 100 chars
|
||||
|
||||
Score is used for default sorting and quality indicators.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Foundation (Week 1-2)
|
||||
- Set up PostgreSQL + Prisma
|
||||
- Create Zod schemas in `@tpmjs/types`
|
||||
- Build sync service structure
|
||||
- Implement NPM API client
|
||||
|
||||
### Phase 2: Discovery (Week 2-3)
|
||||
- Implement changes feed worker
|
||||
- Implement keyword search worker
|
||||
- Deploy sync service (Railway/Fly.io)
|
||||
- Test with real packages
|
||||
|
||||
### Phase 3: API & Frontend (Week 3-4)
|
||||
- Build Next.js API routes
|
||||
- Update tool listing page
|
||||
- Update tool detail pages
|
||||
- Add validation endpoint
|
||||
|
||||
### Phase 4: Polish (Week 4-5)
|
||||
- Add metrics worker
|
||||
- Create documentation
|
||||
- Build CLI validator
|
||||
- Launch to community
|
||||
|
||||
### Phase 5: Enhancements (Post-Launch)
|
||||
- Semantic search (embeddings)
|
||||
- Usage analytics
|
||||
- Tool recommendations
|
||||
- GitHub Actions integration
|
||||
|
||||
---
|
||||
|
||||
## Infrastructure Requirements
|
||||
|
||||
### Sync Service
|
||||
- **Platform:** Railway or Fly.io
|
||||
- **Runtime:** Node.js 22+
|
||||
- **Resources:** 512MB RAM, 1 CPU
|
||||
- **Cost:** ~$5-10/month
|
||||
|
||||
### Database
|
||||
- **Platform:** Neon Postgres (serverless)
|
||||
- **Size:** Free tier (start), scale as needed
|
||||
- **Backups:** Automatic with Neon
|
||||
- **Cost:** Free tier available, ~$10-20/month for production
|
||||
|
||||
### Web App
|
||||
- **Platform:** Vercel (existing)
|
||||
- **No changes required**
|
||||
|
||||
---
|
||||
|
||||
## Monitoring & Health
|
||||
|
||||
### Metrics to Track
|
||||
|
||||
1. **Sync Health**
|
||||
- Changes feed uptime
|
||||
- Packages processed per hour
|
||||
- Validation success rate
|
||||
|
||||
2. **Database**
|
||||
- Total tools
|
||||
- Official vs community ratio
|
||||
- Tier distribution
|
||||
|
||||
3. **API**
|
||||
- Request latency (p95 < 200ms)
|
||||
- Search performance
|
||||
- Error rates
|
||||
|
||||
### Alerts
|
||||
|
||||
- Sync service down > 5 minutes
|
||||
- Database connection failures
|
||||
- Validation error rate > 10%
|
||||
|
||||
---
|
||||
|
||||
## Developer Experience
|
||||
|
||||
### Validation Before Publishing
|
||||
|
||||
```bash
|
||||
# Using TPMJS CLI (to be built)
|
||||
npx tpmjs validate
|
||||
|
||||
# Or via API
|
||||
curl -X POST https://tpmjs.com/api/tools/validate \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"category":"web-scraping","description":"...","example":"..."}'
|
||||
```
|
||||
|
||||
### Documentation Pages Needed
|
||||
|
||||
1. **Getting Started** - Adding TPMJS support
|
||||
2. **Schema Reference** - Complete field docs
|
||||
3. **Best Practices** - Tips for quality tools
|
||||
4. **Examples** - Sample configurations
|
||||
5. **FAQ** - Common questions
|
||||
|
||||
---
|
||||
|
||||
## Migration from Mock Data
|
||||
|
||||
### Current State
|
||||
- 12 mock tools in `toolData.ts`
|
||||
- Client-side search
|
||||
- Hard-coded categories
|
||||
|
||||
### Migration Strategy
|
||||
|
||||
1. **Publish Real Packages**
|
||||
- Create NPM packages for mock tools
|
||||
- Add `tpmjs` fields
|
||||
- Publish with `tpmjs-tool` keyword
|
||||
|
||||
2. **Update Frontend**
|
||||
- Replace mock data with API calls
|
||||
- Keep existing UI components
|
||||
- Update types to match Prisma models
|
||||
|
||||
3. **Gradual Rollout**
|
||||
- Dual mode (mock + real)
|
||||
- Real data primary, mock fallback
|
||||
- Remove mock entirely
|
||||
|
||||
---
|
||||
|
||||
## Success Metrics
|
||||
|
||||
### Technical
|
||||
- ✓ Discovery latency < 60 seconds
|
||||
- ✓ API response time < 200ms p95
|
||||
- ✓ Support 10,000+ tools
|
||||
- ✓ 99.9% uptime
|
||||
|
||||
### User Experience
|
||||
- ✓ 0-click submission (automatic)
|
||||
- ✓ Instant validation feedback
|
||||
- ✓ <100ms search speed
|
||||
- ✓ 100% mobile features
|
||||
|
||||
### Business
|
||||
- Week 1: 10 official tools
|
||||
- Month 1: 50 official tools
|
||||
- Month 3: 200+ tools
|
||||
- Month 6: 1000+ tools
|
||||
- 50+ active package authors
|
||||
|
||||
---
|
||||
|
||||
## Comparison to Vercel's Approach
|
||||
|
||||
| Feature | Vercel AI SDK | TPMJS |
|
||||
|---------|---------------|-------|
|
||||
| **Submission** | Manual file edit + PR | Automatic via NPM |
|
||||
| **Discovery** | None | Real-time changes feed |
|
||||
| **Validation** | Manual review | Automated Zod schema |
|
||||
| **Updates** | New PR required | Automatic on publish |
|
||||
| **Search** | Static array | Full-text + categories |
|
||||
| **Scale** | 6 tools | 1000+ tools ready |
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Review this architecture plan
|
||||
2. Approve database schema and API design
|
||||
3. Set up infrastructure (Railway + Postgres)
|
||||
4. Start Phase 1: Foundation
|
||||
5. Launch MVP in 4-5 weeks
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [NPM Registry API Docs](https://github.com/npm/registry/blob/main/docs/REGISTRY-API.md)
|
||||
- [NPM Changes Feed](https://github.com/npm/registry/blob/main/docs/REPLICATE-API.md)
|
||||
- [Vercel AI Tools Registry](https://github.com/vercel/ai/blob/main/content/tools-registry/registry.ts)
|
||||
- [TPMJS Architecture Plan](/.claude/plans/goofy-inventing-stearns.md) (Full details)
|
||||
|
||||
---
|
||||
|
||||
**Built with ❤️ for the AI agent ecosystem**
|
||||
57
README.md
57
README.md
|
|
@ -1,10 +1,59 @@
|
|||
# TPMJS Monorepo
|
||||
# TPMJS
|
||||
|
||||
[](https://github.com/YOUR_ORG/tpmjs/actions/workflows/ci.yml)
|
||||
[](https://github.com/tpmjs/tpmjs/actions/workflows/ci.yml)
|
||||
|
||||
Tool Package Manager for AI Agents - A Turborepo monorepo with strict TypeScript, Next.js 16, and best practices.
|
||||
**TPMJS is a registry for discovering AI tools published to npm.**
|
||||
|
||||
## Structure
|
||||
Browse, search, and find tools at [tpmjs.com](https://tpmjs.com). Publish your tool by adding the `tpmjs` keyword to your package.json—it appears in the registry within 15 minutes.
|
||||
|
||||
## Why TPMJS?
|
||||
|
||||
- **Discover tools** - Search and browse AI tools by category, quality score, and popularity
|
||||
- **Publish easily** - Add one keyword to package.json, publish to npm, done
|
||||
- **Quality metrics** - Tools are scored based on documentation, downloads, and metadata completeness
|
||||
- **Agent integration** - Optional SDK for agents to search and execute tools at runtime
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Publishing a Tool
|
||||
|
||||
```bash
|
||||
npx @tpmjs/create-basic-tools
|
||||
```
|
||||
|
||||
Or add manually to your package.json:
|
||||
```json
|
||||
{
|
||||
"keywords": ["tpmjs"],
|
||||
"tpmjs": {
|
||||
"category": "text-analysis"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Publish to npm and your tool appears on [tpmjs.com](https://tpmjs.com) within 15 minutes.
|
||||
|
||||
See [HOW_TO_PUBLISH_A_TOOL.md](./HOW_TO_PUBLISH_A_TOOL.md) for the full guide.
|
||||
|
||||
### For AI Agents (Optional)
|
||||
|
||||
Agents can search and execute tools from the registry:
|
||||
|
||||
```bash
|
||||
npm install @tpmjs/registry-search @tpmjs/registry-execute
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { registrySearchTool } from '@tpmjs/registry-search';
|
||||
import { registryExecuteTool } from '@tpmjs/registry-execute';
|
||||
|
||||
// Add to your agent's tools
|
||||
const tools = [registrySearchTool, registryExecuteTool];
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Monorepo Structure
|
||||
|
||||
```
|
||||
apps/
|
||||
|
|
|
|||
149
TOP_5_PRIORITIES.md
Normal file
149
TOP_5_PRIORITIES.md
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
# Top 5 Priorities for TPMJS to Become Production-Ready
|
||||
|
||||
> Based on comprehensive codebase analysis - January 2026
|
||||
|
||||
TPMJS is approximately 70-75% towards being production-ready for widespread use. The platform has solid fundamentals: a well-architected monorepo, robust npm sync system, working MCP integration, and professional code quality standards. However, five critical gaps need addressing before TPMJS can become the "npm for AI tools" it aspires to be.
|
||||
|
||||
---
|
||||
|
||||
## 1. Complete the Developer SDK Packages
|
||||
|
||||
**The Problem:**
|
||||
Developers can't easily integrate TPMJS tools into their applications. The SDK packages `@tpmjs/registry-search` and `@tpmjs/registry-execute` are either incomplete or missing. This defeats the core value proposition - if AI agents can't programmatically discover and execute tools from TPMJS, the registry is just a website, not an ecosystem.
|
||||
|
||||
**What's Needed:**
|
||||
- `@tpmjs/registry-search` - TypeScript client for searching tools with full type safety
|
||||
- `@tpmjs/registry-execute` - Execute any TPMJS tool from any Node.js application
|
||||
- `@tpmjs/agent-toolkit` - Pre-built integration for popular agent frameworks (LangChain, AutoGPT, CrewAI)
|
||||
- Clear examples showing integration with Claude, GPT-4, and other LLMs
|
||||
|
||||
**Impact:**
|
||||
Without these SDKs, TPMJS is just a directory. With them, every AI developer can instantly access 100+ tools with a single `npm install`. This is the difference between a catalog and a platform.
|
||||
|
||||
**Effort:** 2-4 weeks of focused development
|
||||
|
||||
---
|
||||
|
||||
## 2. Add Social Proof and Discovery Features
|
||||
|
||||
**The Problem:**
|
||||
Users have no way to evaluate tool quality beyond download counts. There's no star rating, no reviews, no "trending this week," and no recommendations. When browsing tools, users can't distinguish battle-tested tools from abandoned experiments.
|
||||
|
||||
**What's Needed:**
|
||||
- **5-star rating system** with verified user ratings
|
||||
- **User reviews** with upvoting and author responses
|
||||
- **Trending tools** algorithm (based on recent usage, not just total downloads)
|
||||
- **"Staff Picks"** or curated collections for common use cases
|
||||
- **Similar tools** recommendations on each tool page
|
||||
- **Usage statistics** - "Used in 50 agents" or "10,000 executions this month"
|
||||
|
||||
**Impact:**
|
||||
Social proof is essential for adoption. GitHub has stars, npm has weekly downloads prominently displayed, Product Hunt has upvotes. TPMJS needs its own trust signals. Without them, users default to building their own tools or using alternatives they can evaluate.
|
||||
|
||||
**Effort:** 3-4 weeks including UI/UX design
|
||||
|
||||
---
|
||||
|
||||
## 3. Build Comprehensive Documentation and Onboarding
|
||||
|
||||
**The Problem:**
|
||||
The publishing guide exists but there's no interactive tutorial for new users. API documentation is schema-only with no examples. Developers looking to build tools, create agents, or integrate TPMJS into their workflow face a steep learning curve with limited guidance.
|
||||
|
||||
**What's Needed:**
|
||||
- **Interactive onboarding flow** - Guided first-time experience creating an agent with tools
|
||||
- **API documentation** with copy-paste examples for every endpoint
|
||||
- **Video tutorials** - 5-minute quickstarts for common tasks
|
||||
- **Example agents** - Pre-built agents demonstrating best practices (research agent, coding assistant, data analyst)
|
||||
- **Tool development guide** - Step-by-step from `npm init` to published tool
|
||||
- **Troubleshooting guide** - Common errors and solutions
|
||||
|
||||
**Impact:**
|
||||
Documentation is a product feature. Every hour spent on docs saves thousands of hours of user frustration. LangChain succeeded partly because of excellent docs. TPMJS needs the same investment.
|
||||
|
||||
**Effort:** 4-6 weeks for comprehensive documentation overhaul
|
||||
|
||||
---
|
||||
|
||||
## 4. Build Observability and Platform Trust
|
||||
|
||||
**The Problem:**
|
||||
There's no public status page, no platform-wide health dashboard, and limited visibility into what's working. Users can't answer basic questions: "Is TPMJS up?", "How reliable is this tool?", "What's the average response time?"
|
||||
|
||||
**What's Needed:**
|
||||
- **Public status page** (status.tpmjs.com) showing real-time platform health
|
||||
- **Tool health dashboard** - Aggregate view of which tools are healthy/broken
|
||||
- **Response time metrics** - P50/P95/P99 latency for tool executions
|
||||
- **Uptime guarantees** - Published SLA (even informal "99.9% target")
|
||||
- **Incident history** - Transparent communication about outages
|
||||
- **Usage analytics dashboard** - For tool authors to see how their tools are used
|
||||
|
||||
**Impact:**
|
||||
Trust is earned through transparency. AWS publishes their health dashboard. GitHub has status.github.com. Enterprises won't adopt platforms they can't monitor. Even individual developers want to know if their agent's failures are their code or the platform.
|
||||
|
||||
**Effort:** 2-3 weeks for MVP status page and health dashboard
|
||||
|
||||
---
|
||||
|
||||
## 5. Add Team and Enterprise Features
|
||||
|
||||
**The Problem:**
|
||||
TPMJS is individual-only. There's no way to share collections within a team, manage API keys across an organization, or implement approval workflows. This blocks enterprise adoption where multiple developers need to collaborate on agent tooling.
|
||||
|
||||
**What's Needed:**
|
||||
- **Organizations** - Create teams with shared collections and agents
|
||||
- **Role-based access control (RBAC)** - Admin, Developer, Viewer roles
|
||||
- **Shared API keys** - Organization-scoped keys with usage attribution
|
||||
- **Audit logging** - Who did what, when (required for compliance)
|
||||
- **Private tools** - Organization-only tool publishing
|
||||
- **SSO/SAML** - Enterprise identity provider integration
|
||||
- **Usage quotas** - Set limits per team member or project
|
||||
|
||||
**Impact:**
|
||||
Enterprise customers pay for tools. They also require these features for security and compliance. One enterprise contract can fund months of development. More importantly, enterprise adoption validates the platform and attracts more developers.
|
||||
|
||||
**Effort:** 6-8 weeks for core team features, 3-6 months for full enterprise suite
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Priority | Impact | Effort | Recommended Order |
|
||||
|----------|--------|--------|-------------------|
|
||||
| 1. Complete SDK Packages | Critical | 2-4 weeks | First |
|
||||
| 2. Social Proof/Discovery | High | 3-4 weeks | Second |
|
||||
| 3. Documentation | High | 4-6 weeks | Parallel with #2 |
|
||||
| 4. Observability/Trust | Medium-High | 2-3 weeks | Third |
|
||||
| 5. Enterprise Features | Medium | 6-8 weeks | Fourth |
|
||||
|
||||
**Recommended approach:**
|
||||
1. **Weeks 1-4:** Complete SDK packages (unlocks programmatic adoption)
|
||||
2. **Weeks 2-6:** Build ratings/reviews and documentation in parallel
|
||||
3. **Weeks 7-9:** Add status page and health dashboard
|
||||
4. **Weeks 10+:** Begin enterprise features based on customer demand
|
||||
|
||||
---
|
||||
|
||||
## Current Strengths to Leverage
|
||||
|
||||
TPMJS already has strong foundations:
|
||||
- Robust npm sync system (tools auto-discovered)
|
||||
- Working MCP protocol integration
|
||||
- Clean monorepo architecture
|
||||
- Good authentication system
|
||||
- Solid database design
|
||||
- Quality coding standards
|
||||
|
||||
These investments mean the platform can scale. The gaps identified above are about adoption and trust, not technical architecture.
|
||||
|
||||
---
|
||||
|
||||
## The Bottom Line
|
||||
|
||||
TPMJS has built a good tool registry. To become **the** AI tools platform, it needs to:
|
||||
1. Make tools easy to use programmatically (SDKs)
|
||||
2. Help users find good tools (social proof)
|
||||
3. Help developers build tools (documentation)
|
||||
4. Build platform confidence (observability)
|
||||
5. Enable team adoption (enterprise features)
|
||||
|
||||
With focused effort on these five areas over the next 3-6 months, TPMJS can establish itself as the definitive platform for AI agent tooling.
|
||||
698
TPMJS_FEATURES.md
Normal file
698
TPMJS_FEATURES.md
Normal file
|
|
@ -0,0 +1,698 @@
|
|||
# TPMJS Platform - Complete Feature Documentation
|
||||
|
||||
A comprehensive overview of all TPMJS functionality for marketing, fundraising, and pet project ideation.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Platform Overview](#platform-overview)
|
||||
2. [Core Architecture](#core-architecture)
|
||||
3. [Tool Registry & Discovery](#tool-registry--discovery)
|
||||
4. [Tool Execution System](#tool-execution-system)
|
||||
5. [MCP (Model Context Protocol) Implementation](#mcp-model-context-protocol-implementation)
|
||||
6. [Collections System](#collections-system)
|
||||
7. [Agent System](#agent-system)
|
||||
8. [API Endpoints](#api-endpoints)
|
||||
9. [SDK & Packages](#sdk--packages)
|
||||
10. [Security & Privacy](#security--privacy)
|
||||
11. [Infrastructure](#infrastructure)
|
||||
12. [Use Cases](#use-cases)
|
||||
13. [Competitive Advantages](#competitive-advantages)
|
||||
|
||||
---
|
||||
|
||||
## Platform Overview
|
||||
|
||||
**TPMJS (Tool Package Manager for JavaScript)** is an open platform for discovering, sharing, and executing AI tools via the Model Context Protocol (MCP). Think of it as "npm for AI tools" - a registry where developers can publish tools that AI assistants can use.
|
||||
|
||||
### Key Value Propositions
|
||||
|
||||
1. **Unified Tool Registry** - One place to discover and use AI tools
|
||||
2. **Instant MCP Servers** - Any collection becomes an MCP-compatible server
|
||||
3. **Secure Execution** - Sandboxed tool execution with rate limiting
|
||||
4. **AI Agent Infrastructure** - Build multi-turn conversational agents with tool access
|
||||
5. **Developer-Friendly** - Publish tools via npm, use via standard protocols
|
||||
|
||||
---
|
||||
|
||||
## Core Architecture
|
||||
|
||||
### Tech Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|-------|------------|
|
||||
| Frontend | Next.js 16 (App Router), React 19, Tailwind CSS |
|
||||
| Backend | Next.js API Routes (Serverless) |
|
||||
| Database | PostgreSQL (Neon) with Prisma ORM |
|
||||
| Auth | NextAuth.js (GitHub OAuth) |
|
||||
| Hosting | Vercel (Edge + Serverless) |
|
||||
| Package Registry | npm (mirrored) |
|
||||
| Build System | Turborepo + pnpm workspaces |
|
||||
|
||||
### Monorepo Structure
|
||||
|
||||
```
|
||||
tpmjs/
|
||||
├── apps/
|
||||
│ ├── web/ # Main Next.js application (tpmjs.com)
|
||||
│ └── playground/ # Interactive tool testing environment
|
||||
├── packages/
|
||||
│ ├── @tpmjs/types # Shared TypeScript types & Zod schemas
|
||||
│ ├── @tpmjs/ui # React component library
|
||||
│ ├── @tpmjs/utils # Utility functions
|
||||
│ ├── @tpmjs/env # Environment variable validation
|
||||
│ ├── @tpmjs/db # Prisma database client
|
||||
│ ├── @tpmjs/mocks # MSW mock server for testing
|
||||
│ └── @tpmjs/config # Shared configs (ESLint, Tailwind, TypeScript)
|
||||
└── templates/
|
||||
└── vercel-executor/ # Template for deploying tool executors
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tool Registry & Discovery
|
||||
|
||||
### What is a TPMJS Tool?
|
||||
|
||||
A TPMJS tool is an npm package with:
|
||||
1. The `tpmjs` keyword in package.json
|
||||
2. A `tpmjs` field defining the tool's MCP schema
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-awesome-tool",
|
||||
"keywords": ["tpmjs"],
|
||||
"tpmjs": {
|
||||
"name": "my-tool",
|
||||
"description": "Does awesome things",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": { "type": "string" }
|
||||
},
|
||||
"required": ["query"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Tool Tiers
|
||||
|
||||
| Tier | Description | Features |
|
||||
|------|-------------|----------|
|
||||
| **Minimal** | Basic tool definition | Name, description, input schema only |
|
||||
| **Rich** | Full-featured tool | Executor URL, examples, categories, tags |
|
||||
|
||||
### Discovery Methods
|
||||
|
||||
1. **npm Changes Feed Sync** (every 2 minutes)
|
||||
- Monitors npm's real-time changes feed
|
||||
- Catches new packages and updates instantly
|
||||
- Processes ~100 changes per run
|
||||
|
||||
2. **Keyword Search Sync** (every 15 minutes)
|
||||
- Actively searches npm for `tpmjs` keyword
|
||||
- Backfills any missed packages
|
||||
- Processes up to 250 packages per run
|
||||
|
||||
3. **Metrics Sync** (hourly)
|
||||
- Updates download statistics
|
||||
- Calculates quality scores
|
||||
- Refreshes ranking data
|
||||
|
||||
### Quality Scoring Algorithm
|
||||
|
||||
```
|
||||
Quality Score = Tier Score + Downloads Score + Stars Score
|
||||
|
||||
Where:
|
||||
- Tier Score: rich = 0.6, minimal = 0.4
|
||||
- Downloads Score: min(0.3, log10(downloads + 1) / 10)
|
||||
- Stars Score: min(0.1, log10(githubStars + 1) / 10)
|
||||
```
|
||||
|
||||
### Tool Categories
|
||||
|
||||
- AI/ML
|
||||
- Development Tools
|
||||
- Data Processing
|
||||
- Web Scraping
|
||||
- APIs & Integrations
|
||||
- Utilities
|
||||
- And more...
|
||||
|
||||
### Current Registry Stats
|
||||
|
||||
- **170+ Official Tools** in the ajax-collection
|
||||
- **Growing Community Tools** published by developers
|
||||
- **Real-time Sync** with npm registry
|
||||
|
||||
---
|
||||
|
||||
## Tool Execution System
|
||||
|
||||
### Execution Flow
|
||||
|
||||
```
|
||||
User Request → TPMJS API → Executor Selection → Sandboxed Execution → Response
|
||||
```
|
||||
|
||||
### Executor Types
|
||||
|
||||
1. **HTTP Executor** - Calls external HTTP endpoints
|
||||
2. **Serverless Executor** - Runs in Vercel Edge/Serverless
|
||||
3. **Code Executor** - Executes arbitrary code in sandbox
|
||||
|
||||
### Sandboxing Features
|
||||
|
||||
- **Network Isolation** - Zero-trust or semi-trusted modes
|
||||
- **Timeout Limits** - Configurable per-tool (1-900 seconds)
|
||||
- **Resource Limits** - Memory and CPU constraints
|
||||
- **Input Validation** - Zod schema validation
|
||||
|
||||
### Executor Template
|
||||
|
||||
The `templates/vercel-executor/` provides a ready-to-deploy executor:
|
||||
|
||||
```typescript
|
||||
// Example executor implementation
|
||||
export async function POST(request: Request) {
|
||||
const { tool, input } = await request.json();
|
||||
|
||||
// Validate input against schema
|
||||
const validated = toolSchema.parse(input);
|
||||
|
||||
// Execute tool logic
|
||||
const result = await executeTool(tool, validated);
|
||||
|
||||
return Response.json(result);
|
||||
}
|
||||
```
|
||||
|
||||
### Code Execution (via MCP Tool)
|
||||
|
||||
The platform includes a powerful code execution tool:
|
||||
|
||||
```javascript
|
||||
// Execute code in 42+ languages
|
||||
{
|
||||
"language": "python",
|
||||
"code": "print('Hello, World!')",
|
||||
"network_mode": "zerotrust", // or "semitrusted"
|
||||
"ttl": 60 // timeout in seconds
|
||||
}
|
||||
```
|
||||
|
||||
Supported languages include:
|
||||
- Python, JavaScript, TypeScript
|
||||
- Go, Rust, C, C++
|
||||
- Ruby, PHP, Perl
|
||||
- Java, Kotlin, Scala
|
||||
- And 30+ more
|
||||
|
||||
---
|
||||
|
||||
## MCP (Model Context Protocol) Implementation
|
||||
|
||||
### What is MCP?
|
||||
|
||||
MCP is an open protocol for AI assistants to interact with tools. TPMJS provides:
|
||||
- **MCP Server Hosting** - Every collection is an MCP server
|
||||
- **Multiple Transports** - HTTP and SSE support
|
||||
- **Standard Compliance** - Full MCP specification support
|
||||
|
||||
### Transport Options
|
||||
|
||||
#### HTTP Transport
|
||||
```
|
||||
POST /api/mcp/{username}/{collection-slug}/http
|
||||
Content-Type: application/json
|
||||
|
||||
{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}
|
||||
```
|
||||
|
||||
#### SSE Transport
|
||||
```
|
||||
POST /api/mcp/{username}/{collection-slug}/sse
|
||||
Content-Type: application/json
|
||||
|
||||
{"jsonrpc": "2.0", "id": 1, "method": "initialize"}
|
||||
```
|
||||
|
||||
### MCP Methods Supported
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `initialize` | Initialize MCP session |
|
||||
| `tools/list` | List available tools |
|
||||
| `tools/call` | Execute a tool |
|
||||
| `resources/list` | List available resources |
|
||||
| `resources/read` | Read a resource |
|
||||
| `prompts/list` | List available prompts |
|
||||
| `prompts/get` | Get a specific prompt |
|
||||
|
||||
### Authentication
|
||||
|
||||
- **API Key Auth** - Bearer token in Authorization header
|
||||
- **Session Auth** - Cookie-based for web users
|
||||
- **Scopes** - Granular permission control
|
||||
- `mcp:access` - Access MCP endpoints
|
||||
- `mcp:execute` - Execute tools
|
||||
- `tools:read` - List tools
|
||||
- `tools:execute` - Execute specific tools
|
||||
- `collections:read` - Access collections
|
||||
|
||||
### Integration Examples
|
||||
|
||||
#### Claude Desktop
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"tpmjs": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@anthropic/mcp-remote",
|
||||
"https://tpmjs.com/api/mcp/ajax/ajax-collection/sse"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Cursor IDE
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"tpmjs": {
|
||||
"url": "https://tpmjs.com/api/mcp/ajax/ajax-collection/sse"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Collections System
|
||||
|
||||
### What are Collections?
|
||||
|
||||
Collections are curated groups of tools that form an MCP server. Users can:
|
||||
- Create public or private collections
|
||||
- Add tools from the registry
|
||||
- Share collections as MCP endpoints
|
||||
|
||||
### Collection Features
|
||||
|
||||
- **Custom Naming** - Unique slug per user
|
||||
- **Tool Curation** - Add/remove tools
|
||||
- **Access Control** - Public or private
|
||||
- **MCP Endpoint** - Automatic server generation
|
||||
|
||||
### Collection API
|
||||
|
||||
```typescript
|
||||
// Create collection
|
||||
POST /api/collections
|
||||
{ "name": "My Tools", "slug": "my-tools", "isPublic": true }
|
||||
|
||||
// Add tool to collection
|
||||
POST /api/collections/{id}/tools
|
||||
{ "toolId": "tool-123" }
|
||||
|
||||
// Get collection's MCP endpoint
|
||||
GET /api/mcp/{username}/{collection-slug}/http
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Agent System
|
||||
|
||||
### What are TPMJS Agents?
|
||||
|
||||
Agents are AI-powered conversational interfaces with access to TPMJS tools. They enable:
|
||||
- Multi-turn conversations
|
||||
- Tool execution within context
|
||||
- Custom system prompts
|
||||
- Provider flexibility (OpenAI, Anthropic, etc.)
|
||||
|
||||
### Agent Configuration
|
||||
|
||||
```typescript
|
||||
interface Agent {
|
||||
id: string;
|
||||
uid: string; // Unique identifier
|
||||
name: string;
|
||||
description?: string;
|
||||
provider: "OPENAI" | "ANTHROPIC" | "GOOGLE";
|
||||
modelId: string; // e.g., "gpt-4o-mini"
|
||||
systemPrompt?: string;
|
||||
isPublic: boolean;
|
||||
tools: Tool[]; // Attached tools
|
||||
}
|
||||
```
|
||||
|
||||
### Agent Features
|
||||
|
||||
1. **Multi-Turn Conversations**
|
||||
- Persistent chat history
|
||||
- Context-aware responses
|
||||
- Tool execution in conversation
|
||||
|
||||
2. **Provider Flexibility**
|
||||
- OpenAI (GPT-4, GPT-4o-mini)
|
||||
- Anthropic (Claude)
|
||||
- Google (Gemini)
|
||||
- Custom providers
|
||||
|
||||
3. **Tool Integration**
|
||||
- Attach any TPMJS tool
|
||||
- Automatic tool calling
|
||||
- Result injection into context
|
||||
|
||||
4. **Public Chat Pages**
|
||||
- Share agents via public URL
|
||||
- Embeddable chat interfaces
|
||||
- No auth required for public agents
|
||||
|
||||
### Agent API
|
||||
|
||||
```typescript
|
||||
// Create agent
|
||||
POST /api/agents
|
||||
{ "name": "My Agent", "provider": "OPENAI", "modelId": "gpt-4o-mini" }
|
||||
|
||||
// Chat with agent
|
||||
POST /api/agents/{id}/chat
|
||||
{ "messages": [{"role": "user", "content": "Hello!"}] }
|
||||
|
||||
// Stream response
|
||||
POST /api/agents/{id}/chat
|
||||
{ "messages": [...], "stream": true }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Public Endpoints (No Auth)
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/api/health` | GET | Health check with build info |
|
||||
| `/api/stats` | GET | Platform statistics |
|
||||
| `/api/stats/health` | GET | Tool health metrics |
|
||||
| `/api/tools` | GET | List public tools |
|
||||
| `/api/tools/{id}` | GET | Get tool details |
|
||||
| `/api/tools/search` | GET | Search tools |
|
||||
| `/api/collections/public` | GET | List public collections |
|
||||
|
||||
### Authenticated Endpoints
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/api/user` | GET | Current user profile |
|
||||
| `/api/user/settings` | PATCH | Update user settings |
|
||||
| `/api/user/api-keys` | GET/POST | Manage API keys |
|
||||
| `/api/agents` | CRUD | Agent management |
|
||||
| `/api/collections` | CRUD | Collection management |
|
||||
|
||||
### MCP Endpoints
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/api/mcp/{user}/{collection}/http` | POST | HTTP transport |
|
||||
| `/api/mcp/{user}/{collection}/sse` | POST | SSE transport |
|
||||
| `/api/mcp/{user}/{collection}/http` | GET | Server info |
|
||||
|
||||
### Sync Endpoints (Cron)
|
||||
|
||||
| Endpoint | Schedule | Description |
|
||||
|----------|----------|-------------|
|
||||
| `/api/sync/changes` | */2 * * * * | npm changes feed |
|
||||
| `/api/sync/keyword` | */15 * * * * | Keyword search |
|
||||
| `/api/sync/metrics` | 0 * * * * | Metrics update |
|
||||
|
||||
### Tool Execution
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/api/tools/{id}/execute` | POST | Execute a tool |
|
||||
| `/api/execute/code` | POST | Execute code (sandbox) |
|
||||
|
||||
---
|
||||
|
||||
## SDK & Packages
|
||||
|
||||
### Published npm Packages
|
||||
|
||||
| Package | Description |
|
||||
|---------|-------------|
|
||||
| `@tpmjs/types` | TypeScript types and Zod schemas |
|
||||
| `@tpmjs/ui` | React component library |
|
||||
| `@tpmjs/utils` | Utility functions |
|
||||
| `@tpmjs/env` | Environment validation |
|
||||
|
||||
### Type Definitions
|
||||
|
||||
```typescript
|
||||
// Tool types
|
||||
interface TpmjsTool {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: JSONSchema;
|
||||
outputSchema?: JSONSchema;
|
||||
executor?: string;
|
||||
category?: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
// MCP types
|
||||
interface McpRequest {
|
||||
jsonrpc: "2.0";
|
||||
id: string | number;
|
||||
method: string;
|
||||
params?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface McpResponse {
|
||||
jsonrpc: "2.0";
|
||||
id: string | number;
|
||||
result?: unknown;
|
||||
error?: McpError;
|
||||
}
|
||||
```
|
||||
|
||||
### UI Components
|
||||
|
||||
- Buttons, Cards, Badges
|
||||
- Form inputs with validation
|
||||
- Code editors with syntax highlighting
|
||||
- Chat interfaces
|
||||
- Tool cards and lists
|
||||
|
||||
---
|
||||
|
||||
## Security & Privacy
|
||||
|
||||
### Authentication Methods
|
||||
|
||||
1. **GitHub OAuth** - Primary user auth
|
||||
2. **API Keys** - Programmatic access
|
||||
3. **Session Cookies** - Web auth
|
||||
|
||||
### API Key Security
|
||||
|
||||
- SHA-256 hashed storage
|
||||
- Prefix-only display after creation
|
||||
- Scoped permissions
|
||||
- Optional expiration
|
||||
- Revocation support
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
- Per-user limits
|
||||
- Per-IP limits
|
||||
- Per-tool limits
|
||||
- Customizable thresholds
|
||||
|
||||
### Data Privacy
|
||||
|
||||
- No tool input logging by default
|
||||
- Optional usage analytics
|
||||
- GDPR-compliant data handling
|
||||
- User data export/deletion
|
||||
|
||||
### Sandbox Security
|
||||
|
||||
- Network isolation modes
|
||||
- Resource limits
|
||||
- No persistent storage
|
||||
- Ephemeral execution
|
||||
|
||||
---
|
||||
|
||||
## Infrastructure
|
||||
|
||||
### Deployment Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Vercel │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
|
||||
│ │ Edge │ │ Serverless │ │ Serverless │ │
|
||||
│ │ Network │→ │ Functions │→ │ Executors │ │
|
||||
│ │ (CDN) │ │ (API) │ │ (Tool Runners) │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Neon PostgreSQL │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
|
||||
│ │ Tools │ │ Users │ │ Collections │ │
|
||||
│ │ Registry │ │ & Auth │ │ & Agents │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Monitoring
|
||||
|
||||
- **Health Checks** - Every 5 minutes via GitHub Actions
|
||||
- **Vercel Analytics** - Performance monitoring
|
||||
- **Sync Logging** - All sync operations logged
|
||||
- **Error Tracking** - Automatic error collection
|
||||
|
||||
### CI/CD Pipeline
|
||||
|
||||
1. **Pre-commit** - Lint, format, type-check (Lefthook)
|
||||
2. **CI** - Full test suite (GitHub Actions)
|
||||
3. **Deploy** - Automatic on merge (Vercel)
|
||||
4. **Health Check** - Post-deploy verification
|
||||
|
||||
---
|
||||
|
||||
## Use Cases
|
||||
|
||||
### For Developers
|
||||
|
||||
1. **Publish AI Tools**
|
||||
- Package as npm module
|
||||
- Add `tpmjs` keyword
|
||||
- Automatically synced to registry
|
||||
|
||||
2. **Build Tool Collections**
|
||||
- Curate tools for specific use cases
|
||||
- Share as MCP endpoint
|
||||
- Embed in applications
|
||||
|
||||
3. **Create AI Agents**
|
||||
- Attach tools to agents
|
||||
- Custom system prompts
|
||||
- Deploy public chat interfaces
|
||||
|
||||
### For AI Applications
|
||||
|
||||
1. **Integrate Tools**
|
||||
- Connect via MCP protocol
|
||||
- Use any TPMJS collection
|
||||
- Standard JSON-RPC interface
|
||||
|
||||
2. **Extend Capabilities**
|
||||
- Web scraping, code execution
|
||||
- API integrations
|
||||
- Data processing
|
||||
|
||||
3. **Build Workflows**
|
||||
- Chain multiple tools
|
||||
- Agent-based automation
|
||||
- Custom orchestration
|
||||
|
||||
### For Enterprises
|
||||
|
||||
1. **Private Tool Registry**
|
||||
- Internal tools only
|
||||
- Access control
|
||||
- Usage analytics
|
||||
|
||||
2. **Secure Execution**
|
||||
- Sandboxed environments
|
||||
- Audit logging
|
||||
- Compliance ready
|
||||
|
||||
3. **Custom Agents**
|
||||
- Brand-specific AI assistants
|
||||
- Internal knowledge access
|
||||
- Tool-enabled support
|
||||
|
||||
---
|
||||
|
||||
## Competitive Advantages
|
||||
|
||||
### vs. Building Custom MCP Servers
|
||||
|
||||
| TPMJS | Custom MCP Server |
|
||||
|-------|-------------------|
|
||||
| Instant setup | Days/weeks of development |
|
||||
| 170+ tools ready | Build each tool |
|
||||
| Hosted infrastructure | Self-hosted required |
|
||||
| Automatic scaling | Manual scaling |
|
||||
|
||||
### vs. Other Tool Platforms
|
||||
|
||||
| Feature | TPMJS | Competitors |
|
||||
|---------|-------|-------------|
|
||||
| Open Protocol (MCP) | ✅ | Often proprietary |
|
||||
| npm Integration | ✅ | Custom registries |
|
||||
| Self-hostable | ✅ | Usually SaaS-only |
|
||||
| Code Execution | ✅ | Limited |
|
||||
| Agent System | ✅ | Separate product |
|
||||
|
||||
### Unique Features
|
||||
|
||||
1. **npm-Native** - Tools are just npm packages
|
||||
2. **MCP-First** - Built on open standard
|
||||
3. **Hybrid Execution** - Local + cloud options
|
||||
4. **Collection System** - Curated tool sets
|
||||
5. **Agent Platform** - Full conversational AI
|
||||
|
||||
---
|
||||
|
||||
## Appendix: Official Tools Collection
|
||||
|
||||
The `ajax-collection` includes 170+ tools across categories:
|
||||
|
||||
### Web & Data
|
||||
- `firecrawl-aisdk` - Web crawling and extraction
|
||||
- `tpmjs-tools-page-brief` - Page summarization
|
||||
- `tpmjs-tools-search` - Web search
|
||||
|
||||
### Development
|
||||
- `tpmjs-unsandbox` - Code execution (42+ languages)
|
||||
- `tpmjs-tools-toc-generate` - Markdown TOC generator
|
||||
- `tpmjs-tools-changelog-entry` - Changelog generation
|
||||
|
||||
### Content
|
||||
- `tpmjs-createblogpost` - Blog post creation
|
||||
- `tpmjs-tools-recipe-hash` - Recipe/workflow hashing
|
||||
- `tpmjs-tools-workflow-variant-generate` - Workflow variations
|
||||
|
||||
### And Many More...
|
||||
- API integrations
|
||||
- Data transformations
|
||||
- File processing
|
||||
- Image manipulation
|
||||
- Text analysis
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
TPMJS is a comprehensive platform for AI tool discovery, execution, and orchestration. Key takeaways:
|
||||
|
||||
1. **Registry** - npm-native tool discovery with automatic syncing
|
||||
2. **Execution** - Secure, sandboxed tool running
|
||||
3. **MCP** - Standard protocol for AI integration
|
||||
4. **Collections** - Curated tool sets as MCP servers
|
||||
5. **Agents** - Conversational AI with tool access
|
||||
6. **Infrastructure** - Production-ready, scalable, monitored
|
||||
|
||||
The platform enables developers to publish tools, AI applications to consume them, and enterprises to build secure, tool-enabled AI experiences.
|
||||
391
TPMJS_TALK.md
Normal file
391
TPMJS_TALK.md
Normal file
|
|
@ -0,0 +1,391 @@
|
|||
# TPMJS: The Missing Layer Between "LLMs Can Call Tools" and "Which Tool, Exactly?"
|
||||
|
||||
---
|
||||
|
||||
## The Setup
|
||||
|
||||
You're building an AI agent. It needs to do things in the world—scrape a webpage, send an email, query a database, generate an image. These capabilities come from **tools**.
|
||||
|
||||
The problem isn't that tools don't exist. They do. Thousands of them. The problem is:
|
||||
|
||||
- **You can't find them.** npm has 2 million packages. Which ones are AI-callable tools? Which ones actually work?
|
||||
- **You can't trust them.** No schema. No examples. README says "AI-ready" but the function signature is `(opts: any) => Promise<any>`.
|
||||
- **You can't compare them.** Three packages do "web scraping." Which one handles JavaScript rendering? Which one returns structured data? Which one is maintained?
|
||||
|
||||
Discovery is the bottleneck. Not capability—discovery.
|
||||
|
||||
---
|
||||
|
||||
## What TPMJS Actually Is
|
||||
|
||||
TPMJS is infrastructure. Specifically:
|
||||
|
||||
1. **A registry** that indexes npm packages designed for AI tool use
|
||||
2. **A metadata extraction pipeline** that pulls schemas directly from code
|
||||
3. **A quality scoring system** that ranks tools by completeness and adoption
|
||||
4. **A health monitoring system** that verifies tools actually work
|
||||
5. **A playground** where you can test tools before integrating them
|
||||
|
||||
It's not magic. It's plumbing. Good plumbing.
|
||||
|
||||
---
|
||||
|
||||
## How It Works (The Technical Reality)
|
||||
|
||||
### Discovery: Finding Tools in the Wild
|
||||
|
||||
TPMJS runs three automated sync jobs:
|
||||
|
||||
**1. npm Changes Feed (every 2 minutes)**
|
||||
```
|
||||
npm registry → /_changes endpoint → filter for tpmjs keyword → process
|
||||
```
|
||||
This catches new packages and updates in near-real-time. We track sequence numbers so we never reprocess.
|
||||
|
||||
**2. Keyword Search (every 15 minutes)**
|
||||
```
|
||||
npm search "tpmjs" → up to 250 results → validate → ingest
|
||||
```
|
||||
Backup mechanism. Catches anything the changes feed missed.
|
||||
|
||||
**3. Metrics Sync (hourly)**
|
||||
```
|
||||
for each package → fetch download stats → recalculate quality scores → update health status
|
||||
```
|
||||
Keeps the registry fresh.
|
||||
|
||||
### The Publisher Contract
|
||||
|
||||
To get indexed, a package needs two things:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@acme/my-tool",
|
||||
"keywords": ["tpmjs"],
|
||||
"tpmjs": {
|
||||
"category": "web-scraping",
|
||||
"description": "Scrapes URLs and returns structured markdown"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
That's the minimum. Category + description. Everything else is either optional or auto-extracted.
|
||||
|
||||
**Categories are fixed** (12 total): web-scraping, data-processing, file-operations, communication, database, api-integration, image-processing, text-analysis, automation, ai-ml, security, monitoring.
|
||||
|
||||
Why fixed? Because agents need to filter. "Give me all database tools" has to mean something.
|
||||
|
||||
### Schema Extraction: The Hard Part
|
||||
|
||||
Here's what makes TPMJS different from a glorified npm search.
|
||||
|
||||
When we ingest a package, we don't just read the README. We **execute it in a sandbox** and extract the actual schema:
|
||||
|
||||
```
|
||||
1. Spin up isolated executor (Railway)
|
||||
2. npm install the package
|
||||
3. Import and inspect exports
|
||||
4. Extract JSON Schema from TypeScript types
|
||||
5. Store schema in database
|
||||
```
|
||||
|
||||
The result:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "scrapeUrl",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": { "type": "string", "format": "uri" },
|
||||
"waitForSelector": { "type": "string" },
|
||||
"timeout": { "type": "number", "default": 30000 }
|
||||
},
|
||||
"required": ["url"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This isn't documentation. This is **extracted from the actual function signature**. It's ground truth.
|
||||
|
||||
If the author provides a schema in the `tpmjs` field, we use that. If not, we extract it. Either way, every tool in the registry has a schema.
|
||||
|
||||
### Quality Scoring: Ranking What Matters
|
||||
|
||||
Every tool gets a score from 0.00 to 1.00:
|
||||
|
||||
```typescript
|
||||
// Base score from metadata completeness
|
||||
const tierScore = tier === 'rich' ? 0.6 : 0.4;
|
||||
|
||||
// Adoption signals
|
||||
const downloadsScore = Math.min(0.2, Math.log10(downloads + 1) / 15);
|
||||
const starsScore = Math.min(0.1, Math.log10(githubStars + 1) / 10);
|
||||
|
||||
// Metadata richness bonus
|
||||
let richnessScore = 0;
|
||||
if (hasParameters) richnessScore += 0.04;
|
||||
if (hasReturns) richnessScore += 0.03;
|
||||
if (hasEnvVars) richnessScore += 0.03;
|
||||
```
|
||||
|
||||
**Tier** is binary:
|
||||
- **Minimal**: Just category + description (40% base)
|
||||
- **Rich**: Has parameters, returns, env vars, or framework tags (60% base)
|
||||
|
||||
The formula is deliberately simple. We're not trying to be clever. We're trying to surface tools that are well-documented and actually used.
|
||||
|
||||
### Health Checks: Does It Actually Work?
|
||||
|
||||
Two checks, run during sync and periodically:
|
||||
|
||||
**1. Import Health**
|
||||
```
|
||||
Can we require() this package without it exploding?
|
||||
```
|
||||
You'd be surprised how many npm packages fail this.
|
||||
|
||||
**2. Execution Health**
|
||||
```
|
||||
Can we call the main function with minimal parameters without throwing?
|
||||
```
|
||||
Not a full test suite. Just "does it run at all?"
|
||||
|
||||
Results: `HEALTHY`, `BROKEN`, or `UNKNOWN`.
|
||||
|
||||
Broken tools still appear in the registry (with a warning). We don't hide them—we label them.
|
||||
|
||||
---
|
||||
|
||||
## The Data Model
|
||||
|
||||
Here's what we actually store:
|
||||
|
||||
### Package (npm package level)
|
||||
```
|
||||
npmPackageName (unique)
|
||||
npmVersion, npmDescription, npmRepository, npmLicense
|
||||
npmKeywords[], npmReadme, npmAuthor
|
||||
category (enum)
|
||||
tier ('minimal' | 'rich')
|
||||
discoveryMethod ('changes-feed' | 'keyword')
|
||||
npmDownloadsLastMonth, githubStars
|
||||
frameworks[] (vercel-ai, langchain, etc.)
|
||||
env[] (required environment variables)
|
||||
```
|
||||
|
||||
### Tool (individual callable within a package)
|
||||
```
|
||||
packageId (FK)
|
||||
name (export name: "scrapeUrl", "default", etc.)
|
||||
description
|
||||
inputSchema (JSON Schema)
|
||||
schemaSource ('extracted' | 'author')
|
||||
qualityScore (0.00-1.00)
|
||||
importHealth, executionHealth (HEALTHY | BROKEN | UNKNOWN)
|
||||
toolDiscoverySource ('auto' | 'manual')
|
||||
```
|
||||
|
||||
One package can have multiple tools. `@acme/web-tools` might export `scrapeUrl`, `screenshotPage`, and `extractLinks`. Each is a separate tool with its own schema and health status.
|
||||
|
||||
### Simulation (playground execution)
|
||||
```
|
||||
toolId
|
||||
userPrompt (what the user asked)
|
||||
parameters (JSON, what was passed to the tool)
|
||||
status (pending | running | success | error | timeout)
|
||||
executionTimeMs, output, error
|
||||
model, agentSteps
|
||||
```
|
||||
|
||||
We track every playground execution. Not for surveillance—for debugging and improving the system.
|
||||
|
||||
---
|
||||
|
||||
## The API
|
||||
|
||||
### Search & Discovery
|
||||
|
||||
```
|
||||
GET /api/tools
|
||||
?q=scrape
|
||||
&category=web-scraping
|
||||
&importHealth=HEALTHY
|
||||
&executionHealth=HEALTHY
|
||||
&limit=20
|
||||
&offset=0
|
||||
|
||||
→ Returns tools sorted by quality score
|
||||
```
|
||||
|
||||
```
|
||||
GET /api/tools/search
|
||||
?q=I need to extract text from PDFs
|
||||
|
||||
→ BM25-ranked semantic search
|
||||
```
|
||||
|
||||
### Execution
|
||||
|
||||
```
|
||||
POST /api/tools/execute/{toolId}
|
||||
{
|
||||
"prompt": "Scrape the homepage of Hacker News",
|
||||
"parameters": { "url": "https://news.ycombinator.com" }
|
||||
}
|
||||
|
||||
→ Server-Sent Events stream with:
|
||||
- Agent reasoning steps
|
||||
- Tool call results
|
||||
- Final output
|
||||
```
|
||||
|
||||
Rate limited: 10 requests per IP per hour. We're not a free compute platform.
|
||||
|
||||
### Schema Operations
|
||||
|
||||
```
|
||||
POST /api/tools/extract-schema
|
||||
{ "packageName": "@acme/my-tool", "toolName": "scrapeUrl" }
|
||||
|
||||
→ Forces re-extraction of schema from source
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## The Playground
|
||||
|
||||
A Next.js app where you can:
|
||||
|
||||
1. **Browse tools** by category, health status, quality score
|
||||
2. **Inspect schemas** before you commit to anything
|
||||
3. **Test execution** with an AI agent
|
||||
4. **See real responses** with actual latency and token usage
|
||||
|
||||
It's not a demo. It's a debugging tool. "Does this tool do what I think it does?" Answer that question in 30 seconds instead of 30 minutes.
|
||||
|
||||
---
|
||||
|
||||
## What This Enables
|
||||
|
||||
### For Engineers Building Agents
|
||||
|
||||
Before TPMJS:
|
||||
```
|
||||
1. Search npm for "web scraper"
|
||||
2. Get 500 results
|
||||
3. Click through 20 of them
|
||||
4. Read READMEs that say "easy to use!"
|
||||
5. npm install three of them
|
||||
6. Write test code for each
|
||||
7. Find out two are broken
|
||||
8. Pick the one that works
|
||||
9. Hope it keeps working
|
||||
```
|
||||
|
||||
After TPMJS:
|
||||
```
|
||||
1. Search tpmjs.com for "web scraper"
|
||||
2. Filter by HEALTHY status
|
||||
3. Sort by quality score
|
||||
4. Click top result
|
||||
5. See exact input schema
|
||||
6. Test in playground
|
||||
7. Integrate
|
||||
```
|
||||
|
||||
### For Tool Authors
|
||||
|
||||
Before TPMJS:
|
||||
```
|
||||
Publish to npm → hope someone finds it → no visibility into usage
|
||||
```
|
||||
|
||||
After TPMJS:
|
||||
```
|
||||
Publish to npm with tpmjs keyword → indexed within 2 minutes →
|
||||
schema auto-extracted → quality scored → discoverable by search →
|
||||
execution stats tracked
|
||||
```
|
||||
|
||||
Your tool becomes findable. Not just by humans grepping npm, but by agents querying the registry API.
|
||||
|
||||
### For Agents (Yes, Really)
|
||||
|
||||
Agents can query TPMJS at runtime:
|
||||
|
||||
```typescript
|
||||
const tools = await fetch('https://tpmjs.com/api/tools?' + new URLSearchParams({
|
||||
q: 'send email',
|
||||
executionHealth: 'HEALTHY',
|
||||
limit: '5'
|
||||
})).then(r => r.json());
|
||||
|
||||
// Agent now has 5 working email tools with full schemas
|
||||
// It can pick the best one for this specific task
|
||||
```
|
||||
|
||||
This is the endgame. Not humans browsing a registry—agents dynamically selecting tools based on capability, health, and fit.
|
||||
|
||||
---
|
||||
|
||||
## What TPMJS Is Not
|
||||
|
||||
**Not a package manager.** We don't host packages. npm does that. We index and enrich.
|
||||
|
||||
**Not an execution platform.** The playground runs tools for testing. Production execution is your responsibility.
|
||||
|
||||
**Not a security guarantee.** We check if tools work. We don't audit them for malice. Same rules as npm: don't run untrusted code.
|
||||
|
||||
**Not magic.** We're not using AI to understand what tools do. We're extracting schemas and running health checks. Boring, reliable, debuggable.
|
||||
|
||||
---
|
||||
|
||||
## The Technical Stack
|
||||
|
||||
- **Database**: PostgreSQL via Prisma
|
||||
- **Web**: Next.js 16 (App Router)
|
||||
- **Deployment**: Vercel (web) + Railway (sandbox executor)
|
||||
- **Sync**: Vercel Cron + GitHub Actions backup
|
||||
- **AI**: Vercel AI SDK for playground execution
|
||||
- **Monorepo**: Turborepo + pnpm
|
||||
|
||||
Key internal packages:
|
||||
- `@tpmjs/npm-client` — npm registry integration
|
||||
- `@tpmjs/package-executor` — sandbox execution client
|
||||
- `@tpmjs/types` — schema validation and migration
|
||||
- `@tpmjs/db` — Prisma client and models
|
||||
|
||||
---
|
||||
|
||||
## Current State
|
||||
|
||||
- **~100 tools indexed** (and growing with every npm publish)
|
||||
- **12 categories** covering most agent use cases
|
||||
- **Automated sync** running 24/7
|
||||
- **Health checks** on every tool
|
||||
- **Schema extraction** working for TypeScript and JavaScript
|
||||
- **Playground** functional for testing
|
||||
|
||||
---
|
||||
|
||||
## The Pitch (Finally)
|
||||
|
||||
Tools are the API surface of AI agents. The ecosystem is a mess. TPMJS is the index.
|
||||
|
||||
We don't compete with npm—we sit on top of it. We don't replace tool authors—we make them discoverable. We don't build agents—we give agents a way to find their tools.
|
||||
|
||||
Discovery is the bottleneck. We're fixing discovery.
|
||||
|
||||
---
|
||||
|
||||
## Try It
|
||||
|
||||
- **Browse**: https://tpmjs.com/tool-search
|
||||
- **Playground**: https://tpmjs.com/playground
|
||||
- **Publish**: Add `tpmjs` keyword + `tpmjs` field to your package.json
|
||||
- **API**: `GET https://tpmjs.com/api/tools`
|
||||
|
||||
---
|
||||
|
||||
*Tools are inevitable. Discovery chaos isn't.*
|
||||
|
|
@ -1,324 +0,0 @@
|
|||
# 🔥 Vercel API Routes Fix - Action Checklist
|
||||
|
||||
## Problem Summary
|
||||
|
||||
**API routes are not deploying because Vercel is not detecting your project as Next.js.**
|
||||
|
||||
When Vercel doesn't detect Next.js, it treats your deployment as a static site and **drops all App Router API routes** from the build output. Pages work because they're static files, but API routes require serverless function generation which only happens when Next.js is properly detected.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Fix Checklist (Complete in Order)
|
||||
|
||||
### 1️⃣ Fix Root Directory
|
||||
|
||||
**Where:** Vercel Dashboard → Project `tpmjs-web` → Settings → General → Root Directory
|
||||
|
||||
**Current (likely):** Empty, `.`, or wrong path
|
||||
**Required:** `apps/web` (exactly this, no leading/trailing slashes)
|
||||
|
||||
**Validation:**
|
||||
```
|
||||
✓ Must be exactly: apps/web
|
||||
✗ NOT: /apps/web/
|
||||
✗ NOT: ./apps/web
|
||||
✗ NOT: tpmjs/apps/web
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2️⃣ Set Framework Preset
|
||||
|
||||
**Where:** Vercel Dashboard → Project `tpmjs-web` → Settings → General → Framework Preset
|
||||
|
||||
**Current (likely):** "Other"
|
||||
**Required:** "Next.js"
|
||||
|
||||
**Why this matters:**
|
||||
- Framework = "Other" → Uses `@vercel/static-builder` → No API routes
|
||||
- Framework = "Next.js" → Uses `@vercel/next` → API routes deployed
|
||||
|
||||
---
|
||||
|
||||
### 3️⃣ Remove Domain from Old Project
|
||||
|
||||
**Where:** Vercel Dashboard → Project `v0-tool-registry-page` → Settings → Domains
|
||||
|
||||
**Action:** Remove these domains:
|
||||
- `tpmjs.com`
|
||||
- `www.tpmjs.com`
|
||||
|
||||
**Then:** Verify both domains are ONLY assigned to the `tpmjs` project
|
||||
|
||||
**Why this matters:**
|
||||
The "Redirecting..." message is coming from the old project. Having two projects with the same domain causes shadow routing and API requests hitting the wrong deployment.
|
||||
|
||||
---
|
||||
|
||||
### 4️⃣ Clear Custom Build Commands
|
||||
|
||||
**Where:** Vercel Dashboard → Project `tpmjs-web` → Settings → Build & Development Settings
|
||||
|
||||
**Set ALL to default/empty:**
|
||||
```
|
||||
Build Command: (empty - let Vercel auto-detect)
|
||||
Install Command: (empty - let Vercel auto-detect)
|
||||
Output Directory: .next (default)
|
||||
```
|
||||
|
||||
**Why this matters:**
|
||||
Custom build commands bypass Vercel's Next.js detection. Vercel should automatically:
|
||||
- Detect monorepo structure
|
||||
- Run `pnpm install`
|
||||
- Run `pnpm build` in the correct workspace
|
||||
- Use `@vercel/next` builder
|
||||
|
||||
**If you must use custom commands, use:**
|
||||
```
|
||||
Build Command: pnpm turbo run build --filter=@tpmjs/web
|
||||
Install Command: pnpm install
|
||||
```
|
||||
|
||||
But try empty first.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Verification Steps
|
||||
|
||||
### Before Deploying
|
||||
|
||||
Run this locally to confirm Next.js detection:
|
||||
```bash
|
||||
cd apps/web
|
||||
vercel build
|
||||
```
|
||||
|
||||
**Expected output should include:**
|
||||
```
|
||||
● route (app) /api/health
|
||||
● route (app) /api/tools
|
||||
● route (app) /api/sync/changes
|
||||
λ /api/health
|
||||
λ /api/tools
|
||||
λ /api/sync/changes
|
||||
```
|
||||
|
||||
**If you DON'T see this, Vercel won't deploy API routes.**
|
||||
|
||||
### After Deploying
|
||||
|
||||
1. **Check Build Output:**
|
||||
```bash
|
||||
vercel inspect <deployment-url>
|
||||
```
|
||||
|
||||
Should show:
|
||||
```
|
||||
Builds
|
||||
├── λ api/health (XXX KB) [region]
|
||||
├── λ api/tools (XXX KB) [region]
|
||||
├── λ api/sync/changes (XXX KB) [region]
|
||||
├── λ tool/[slug] (XXX KB) [region]
|
||||
...
|
||||
```
|
||||
|
||||
2. **Test API Routes:**
|
||||
```bash
|
||||
# Should return JSON (not timeout, not "Redirecting...")
|
||||
curl https://tpmjs.com/api/health
|
||||
|
||||
# Should return tool data
|
||||
curl https://tpmjs.com/api/tools
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Expected Results
|
||||
|
||||
### ✅ Success Indicators
|
||||
|
||||
- [ ] `vercel inspect` shows API routes as `λ` functions
|
||||
- [ ] `curl https://tpmjs.com/api/health` returns JSON
|
||||
- [ ] `curl https://tpmjs.com/api/tools` returns tool data
|
||||
- [ ] No "Redirecting..." messages
|
||||
- [ ] No timeouts on direct Vercel URLs
|
||||
- [ ] Build logs show "route (app) /api/*"
|
||||
|
||||
### ❌ Failure Indicators (Need to revisit steps)
|
||||
|
||||
- [ ] Only pages listed in `vercel inspect`, no API routes
|
||||
- [ ] API endpoints return "Redirecting..."
|
||||
- [ ] API endpoints timeout (exit code 28)
|
||||
- [ ] Build logs don't mention API routes
|
||||
- [ ] Framework Preset still shows "Other"
|
||||
|
||||
---
|
||||
|
||||
## 🚨 Common Mistakes
|
||||
|
||||
### Mistake 1: Wrong Root Directory Format
|
||||
```
|
||||
✗ /apps/web/ (leading/trailing slashes)
|
||||
✗ ./apps/web (relative path notation)
|
||||
✗ apps/web/ (trailing slash)
|
||||
✓ apps/web (correct)
|
||||
```
|
||||
|
||||
### Mistake 2: Leaving Custom Build Commands
|
||||
If you have:
|
||||
```json
|
||||
{
|
||||
"buildCommand": "cd ../.. && turbo build --filter=@tpmjs/web"
|
||||
}
|
||||
```
|
||||
|
||||
This MIGHT work, but can break Next.js detection. Start with empty and only add if needed.
|
||||
|
||||
### Mistake 3: Not Removing Domain from Old Project
|
||||
If `v0-tool-registry-page` still has `tpmjs.com`, your requests will route to the wrong project randomly based on:
|
||||
- DNS propagation
|
||||
- Edge cache
|
||||
- Vercel's routing priority
|
||||
|
||||
### Mistake 4: Not Verifying Framework Preset
|
||||
"Other" is Vercel's default when it can't detect a framework. This is the #1 cause of missing API routes in monorepos.
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
### If API routes STILL don't deploy after all 4 steps:
|
||||
|
||||
1. **Check Build Logs:**
|
||||
- Does it say "Detected Next.js"?
|
||||
- Does it list "route (app) /api/*"?
|
||||
- Does it show `@vercel/next` builder?
|
||||
|
||||
2. **Check package.json location:**
|
||||
```
|
||||
✓ Should exist: apps/web/package.json
|
||||
✗ Should NOT be at root ONLY
|
||||
```
|
||||
|
||||
3. **Check next.config.ts location:**
|
||||
```
|
||||
✓ Should exist: apps/web/next.config.ts
|
||||
```
|
||||
|
||||
4. **Verify pnpm workspace:**
|
||||
```bash
|
||||
# Should show @tpmjs/web
|
||||
pnpm list --depth 0 --filter @tpmjs/web
|
||||
```
|
||||
|
||||
5. **Test local build with Vercel CLI:**
|
||||
```bash
|
||||
cd apps/web
|
||||
vercel build --debug
|
||||
```
|
||||
Look for "Framework: nextjs" in output.
|
||||
|
||||
---
|
||||
|
||||
## 📞 When to Contact Vercel Support
|
||||
|
||||
If after completing all 4 steps:
|
||||
- Build logs show "Detected Next.js"
|
||||
- Build logs show "route (app) /api/*"
|
||||
- BUT `vercel inspect` still doesn't list API functions
|
||||
|
||||
Then you have a Vercel platform bug. Contact support with:
|
||||
- This checklist
|
||||
- Build logs
|
||||
- `vercel inspect` output
|
||||
- Link to `API_ROUTES_TIMEOUT_INVESTIGATION.md`
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Quick Win Test
|
||||
|
||||
**Don't want to change production settings yet?**
|
||||
|
||||
1. Create a NEW Vercel project
|
||||
2. Import the SAME repo
|
||||
3. Set Root Directory to `apps/web`
|
||||
4. Set Framework Preset to "Next.js"
|
||||
5. Deploy
|
||||
|
||||
If API routes work in the new project → confirms the fix
|
||||
If API routes still fail → deeper issue (contact support)
|
||||
|
||||
---
|
||||
|
||||
## ✨ Post-Fix Cleanup
|
||||
|
||||
Once API routes are working:
|
||||
|
||||
### Optional: Re-add www redirect
|
||||
|
||||
Now that API routes work, you can safely add the www redirect back:
|
||||
|
||||
**Option A - Vercel Project Settings:**
|
||||
Vercel Dashboard → Domains → tpmjs.com → Redirect www to apex
|
||||
|
||||
**Option B - Next.js config:**
|
||||
```typescript
|
||||
// apps/web/next.config.ts
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
source: '/:path((?!api).*)*', // Exclude /api/*
|
||||
has: [{ type: 'host', value: 'www.tpmjs.com' }],
|
||||
destination: 'https://tpmjs.com/:path*',
|
||||
permanent: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
**Option C - vercel.json (not recommended):**
|
||||
Only use if you understand the implications.
|
||||
|
||||
### Optional: Remove maxDuration exports
|
||||
|
||||
The `export const maxDuration = 60;` in your route files isn't needed unless you actually need longer timeouts. Default is 10s (Hobby) or 15s (Pro).
|
||||
|
||||
---
|
||||
|
||||
## 📊 Summary
|
||||
|
||||
| Issue | Root Cause | Fix |
|
||||
|-------|------------|-----|
|
||||
| API routes timeout | Vercel doesn't detect Next.js | Set Framework Preset to "Next.js" |
|
||||
| No λ functions in build | Wrong Root Directory | Set to `apps/web` exactly |
|
||||
| "Redirecting..." on API calls | Domain on two projects | Remove from old project |
|
||||
| Build doesn't find API routes | Custom build commands break detection | Clear custom commands |
|
||||
|
||||
**Time to fix:** 5 minutes (just changing dashboard settings)
|
||||
**Deployments needed:** 1 (changes take effect on next deploy)
|
||||
**Code changes needed:** 0 (this is pure configuration)
|
||||
|
||||
---
|
||||
|
||||
## 🎉 When It Works
|
||||
|
||||
You'll know it's fixed when:
|
||||
|
||||
```bash
|
||||
$ curl https://tpmjs.com/api/health
|
||||
{"status":"ok","timestamp":"2025-11-28T...","env":{"hasDatabase":true,"nodeEnv":"production"}}
|
||||
|
||||
$ curl https://tpmjs.com/api/tools
|
||||
{"data":[...],"pagination":{...}}
|
||||
```
|
||||
|
||||
And `vercel inspect <url>` shows:
|
||||
```
|
||||
Builds
|
||||
├── λ api/health
|
||||
├── λ api/tools
|
||||
├── λ api/stats
|
||||
... (ALL your API routes)
|
||||
```
|
||||
|
||||
**That's it. No code changes. Just fix the Vercel project configuration.**
|
||||
1
apps/omega-mac/.gitignore
vendored
Normal file
1
apps/omega-mac/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
.build
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"colors": [
|
||||
{
|
||||
"color": {
|
||||
"color-space": "srgb",
|
||||
"components": {
|
||||
"alpha": "1.000",
|
||||
"blue": "0.996",
|
||||
"green": "0.475",
|
||||
"red": "0.325"
|
||||
}
|
||||
},
|
||||
"idiom": "universal"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
"images": [
|
||||
{
|
||||
"idiom": "mac",
|
||||
"scale": "1x",
|
||||
"size": "16x16"
|
||||
},
|
||||
{
|
||||
"idiom": "mac",
|
||||
"scale": "2x",
|
||||
"size": "16x16"
|
||||
},
|
||||
{
|
||||
"idiom": "mac",
|
||||
"scale": "1x",
|
||||
"size": "32x32"
|
||||
},
|
||||
{
|
||||
"idiom": "mac",
|
||||
"scale": "2x",
|
||||
"size": "32x32"
|
||||
},
|
||||
{
|
||||
"idiom": "mac",
|
||||
"scale": "1x",
|
||||
"size": "128x128"
|
||||
},
|
||||
{
|
||||
"idiom": "mac",
|
||||
"scale": "2x",
|
||||
"size": "128x128"
|
||||
},
|
||||
{
|
||||
"idiom": "mac",
|
||||
"scale": "1x",
|
||||
"size": "256x256"
|
||||
},
|
||||
{
|
||||
"idiom": "mac",
|
||||
"scale": "2x",
|
||||
"size": "256x256"
|
||||
},
|
||||
{
|
||||
"idiom": "mac",
|
||||
"scale": "1x",
|
||||
"size": "512x512"
|
||||
},
|
||||
{
|
||||
"idiom": "mac",
|
||||
"scale": "2x",
|
||||
"size": "512x512"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
6
apps/omega-mac/OmegaMac/Assets.xcassets/Contents.json
Normal file
6
apps/omega-mac/OmegaMac/Assets.xcassets/Contents.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
41
apps/omega-mac/OmegaMac/Models/Conversation.swift
Normal file
41
apps/omega-mac/OmegaMac/Models/Conversation.swift
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
final class Conversation {
|
||||
var id: UUID
|
||||
var title: String?
|
||||
var createdAt: Date
|
||||
var updatedAt: Date
|
||||
var executionState: String // "idle" | "running"
|
||||
var inputTokensTotal: Int
|
||||
var outputTokensTotal: Int
|
||||
|
||||
@Relationship(deleteRule: .cascade, inverse: \Message.conversation)
|
||||
var messages: [Message]
|
||||
|
||||
@Relationship(deleteRule: .cascade, inverse: \ToolCallRecord.conversation)
|
||||
var toolRuns: [ToolCallRecord]
|
||||
|
||||
init(
|
||||
title: String? = nil
|
||||
) {
|
||||
self.id = UUID()
|
||||
self.title = title
|
||||
self.createdAt = Date()
|
||||
self.updatedAt = Date()
|
||||
self.executionState = "idle"
|
||||
self.inputTokensTotal = 0
|
||||
self.outputTokensTotal = 0
|
||||
self.messages = []
|
||||
self.toolRuns = []
|
||||
}
|
||||
|
||||
var displayTitle: String {
|
||||
title ?? "New Conversation"
|
||||
}
|
||||
|
||||
var sortedMessages: [Message] {
|
||||
messages.sorted { $0.createdAt < $1.createdAt }
|
||||
}
|
||||
}
|
||||
18
apps/omega-mac/OmegaMac/Models/EnvVar.swift
Normal file
18
apps/omega-mac/OmegaMac/Models/EnvVar.swift
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
final class EnvVar {
|
||||
var id: UUID
|
||||
var keyName: String
|
||||
/// Last 4 characters of the value (for display hint)
|
||||
var valueHint: String
|
||||
var createdAt: Date
|
||||
|
||||
init(keyName: String, valueHint: String) {
|
||||
self.id = UUID()
|
||||
self.keyName = keyName
|
||||
self.valueHint = valueHint
|
||||
self.createdAt = Date()
|
||||
}
|
||||
}
|
||||
141
apps/omega-mac/OmegaMac/Models/Message.swift
Normal file
141
apps/omega-mac/OmegaMac/Models/Message.swift
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
enum MessageRole: String, Codable {
|
||||
case user = "USER"
|
||||
case assistant = "ASSISTANT"
|
||||
case tool = "TOOL"
|
||||
case system = "SYSTEM"
|
||||
}
|
||||
|
||||
@Model
|
||||
final class Message {
|
||||
var id: UUID
|
||||
var role: MessageRole
|
||||
var content: String
|
||||
var createdAt: Date
|
||||
var inputTokens: Int?
|
||||
var outputTokens: Int?
|
||||
|
||||
/// JSON-encoded array of tool calls (for assistant messages)
|
||||
var toolCallsJSON: Data?
|
||||
|
||||
var conversation: Conversation?
|
||||
|
||||
init(
|
||||
role: MessageRole,
|
||||
content: String,
|
||||
conversation: Conversation? = nil,
|
||||
inputTokens: Int? = nil,
|
||||
outputTokens: Int? = nil,
|
||||
toolCalls: [ToolCallData]? = nil
|
||||
) {
|
||||
self.id = UUID()
|
||||
self.role = role
|
||||
self.content = content
|
||||
self.createdAt = Date()
|
||||
self.inputTokens = inputTokens
|
||||
self.outputTokens = outputTokens
|
||||
self.conversation = conversation
|
||||
if let toolCalls {
|
||||
self.toolCallsJSON = try? JSONEncoder().encode(toolCalls)
|
||||
}
|
||||
}
|
||||
|
||||
var toolCalls: [ToolCallData] {
|
||||
get {
|
||||
guard let data = toolCallsJSON else { return [] }
|
||||
return (try? JSONDecoder().decode([ToolCallData].self, from: data)) ?? []
|
||||
}
|
||||
set {
|
||||
toolCallsJSON = try? JSONEncoder().encode(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Serializable tool call data stored in messages
|
||||
struct ToolCallData: Codable, Identifiable {
|
||||
var id: String { toolCallId }
|
||||
let toolCallId: String
|
||||
let toolName: String
|
||||
let args: JSONValue?
|
||||
let output: JSONValue?
|
||||
|
||||
init(toolCallId: String, toolName: String, args: JSONValue? = nil, output: JSONValue? = nil) {
|
||||
self.toolCallId = toolCallId
|
||||
self.toolName = toolName
|
||||
self.args = args
|
||||
self.output = output
|
||||
}
|
||||
}
|
||||
|
||||
/// A type-erased JSON value for encoding/decoding arbitrary JSON
|
||||
enum JSONValue: Codable, Equatable, Sendable {
|
||||
case string(String)
|
||||
case number(Double)
|
||||
case bool(Bool)
|
||||
case object([String: JSONValue])
|
||||
case array([JSONValue])
|
||||
case null
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
if container.decodeNil() {
|
||||
self = .null
|
||||
} else if let b = try? container.decode(Bool.self) {
|
||||
self = .bool(b)
|
||||
} else if let n = try? container.decode(Double.self) {
|
||||
self = .number(n)
|
||||
} else if let s = try? container.decode(String.self) {
|
||||
self = .string(s)
|
||||
} else if let arr = try? container.decode([JSONValue].self) {
|
||||
self = .array(arr)
|
||||
} else if let obj = try? container.decode([String: JSONValue].self) {
|
||||
self = .object(obj)
|
||||
} else {
|
||||
self = .null
|
||||
}
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
switch self {
|
||||
case .string(let s): try container.encode(s)
|
||||
case .number(let n): try container.encode(n)
|
||||
case .bool(let b): try container.encode(b)
|
||||
case .object(let o): try container.encode(o)
|
||||
case .array(let a): try container.encode(a)
|
||||
case .null: try container.encodeNil()
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert any Codable/Sendable value to JSONValue
|
||||
static func from(_ value: Any) -> JSONValue {
|
||||
if let s = value as? String { return .string(s) }
|
||||
if let n = value as? NSNumber {
|
||||
if CFBooleanGetTypeID() == CFGetTypeID(n) {
|
||||
return .bool(n.boolValue)
|
||||
}
|
||||
return .number(n.doubleValue)
|
||||
}
|
||||
if let b = value as? Bool { return .bool(b) }
|
||||
if let i = value as? Int { return .number(Double(i)) }
|
||||
if let d = value as? Double { return .number(d) }
|
||||
if let arr = value as? [Any] { return .array(arr.map { from($0) }) }
|
||||
if let obj = value as? [String: Any] {
|
||||
return .object(obj.mapValues { from($0) })
|
||||
}
|
||||
return .null
|
||||
}
|
||||
|
||||
/// Pretty-print JSON
|
||||
var prettyString: String {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
||||
guard let data = try? encoder.encode(self),
|
||||
let str = String(data: data, encoding: .utf8) else {
|
||||
return "null"
|
||||
}
|
||||
return str
|
||||
}
|
||||
}
|
||||
51
apps/omega-mac/OmegaMac/Models/ToolCallRecord.swift
Normal file
51
apps/omega-mac/OmegaMac/Models/ToolCallRecord.swift
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
final class ToolCallRecord {
|
||||
var id: UUID
|
||||
var toolName: String
|
||||
var toolCallId: String
|
||||
var status: String // "running" | "success" | "error"
|
||||
var inputJSON: Data?
|
||||
var outputJSON: Data?
|
||||
var errorMessage: String?
|
||||
var executionTimeMs: Int?
|
||||
var createdAt: Date
|
||||
var completedAt: Date?
|
||||
|
||||
var conversation: Conversation?
|
||||
|
||||
init(
|
||||
toolName: String,
|
||||
toolCallId: String,
|
||||
conversation: Conversation? = nil
|
||||
) {
|
||||
self.id = UUID()
|
||||
self.toolName = toolName
|
||||
self.toolCallId = toolCallId
|
||||
self.status = "running"
|
||||
self.createdAt = Date()
|
||||
self.conversation = conversation
|
||||
}
|
||||
|
||||
var input: JSONValue? {
|
||||
get {
|
||||
guard let data = inputJSON else { return nil }
|
||||
return try? JSONDecoder().decode(JSONValue.self, from: data)
|
||||
}
|
||||
set {
|
||||
inputJSON = try? JSONEncoder().encode(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
var output: JSONValue? {
|
||||
get {
|
||||
guard let data = outputJSON else { return nil }
|
||||
return try? JSONDecoder().decode(JSONValue.self, from: data)
|
||||
}
|
||||
set {
|
||||
outputJSON = try? JSONEncoder().encode(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
17
apps/omega-mac/OmegaMac/Models/UserSettings.swift
Normal file
17
apps/omega-mac/OmegaMac/Models/UserSettings.swift
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
final class UserSettings {
|
||||
var id: UUID
|
||||
var systemPrompt: String?
|
||||
var selectedModel: String
|
||||
var pinnedToolIds: [String]
|
||||
|
||||
init() {
|
||||
self.id = UUID()
|
||||
self.systemPrompt = nil
|
||||
self.selectedModel = "gpt-4.1-mini"
|
||||
self.pinnedToolIds = []
|
||||
}
|
||||
}
|
||||
14
apps/omega-mac/OmegaMac/OmegaMac.entitlements
Normal file
14
apps/omega-mac/OmegaMac/OmegaMac.entitlements
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?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>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)com.tpmjs.omega-mac</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue