Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52716db649 | ||
|
|
9ca4b464ea | ||
|
|
204a31b6bb | ||
|
|
813d287a09 | ||
|
|
4dd9f33eba | ||
|
|
5953378a12 | ||
|
|
b419eed295 | ||
|
|
52deb7f352 | ||
|
|
a4f3aecbaa | ||
|
|
49ff6a852a | ||
|
|
7f537d2e98 | ||
|
|
753443b16f | ||
|
|
33c63be980 | ||
|
|
b6efca42b4 | ||
|
|
fa6eadc39a | ||
|
|
8789acefa6 | ||
|
|
0e280017e6 | ||
|
|
17e8322c29 | ||
|
|
96eda740cd | ||
|
|
fa84612357 | ||
|
|
cf1f63eda3 | ||
|
|
9704f5ce89 | ||
|
|
0eaec2af82 | ||
|
|
398d35dc97 | ||
|
|
5efeaae093 | ||
|
|
cb2dd34a5e | ||
|
|
7112a706b8 | ||
|
|
025a47d01f | ||
|
|
13f89fdb8f | ||
|
|
cc78d50ef6 | ||
|
|
a8985b1849 | ||
|
|
6a1552f65c | ||
|
|
776091cc23 | ||
|
|
f385524f48 | ||
|
|
350982e636 | ||
|
|
5854455815 | ||
|
|
9ecaf618db | ||
|
|
95b667d21e | ||
|
|
a0b689c140 | ||
|
|
ea52ed41be | ||
|
|
5a50d54fda | ||
|
|
35d118b0c4 | ||
|
|
ea7c213f5d | ||
|
|
70dd6dd394 | ||
|
|
049510afbd | ||
|
|
c120447fd0 | ||
|
|
feb1f36126 | ||
|
|
d6ef47bb2d | ||
|
|
50fd416d49 | ||
|
|
aef6904247 | ||
|
|
0bf40faf95 | ||
|
|
c90987c4b0 | ||
|
|
0e08655407 | ||
|
|
427887db9c | ||
|
|
a718622498 | ||
|
|
4e83107d79 | ||
|
|
04b6e72820 | ||
|
|
501a2539c7 | ||
|
|
6a9856d480 | ||
|
|
2c8d42d997 | ||
|
|
9c237f0bfb | ||
|
|
63bfe76720 | ||
|
|
99d7ff47c4 | ||
|
|
3ff0eb3065 | ||
|
|
4d2b265dc4 | ||
|
|
1854245bd3 | ||
|
|
4d07034930 | ||
|
|
98031173b6 | ||
|
|
e8e474597c | ||
|
|
382758790c | ||
|
|
c33920f59d | ||
|
|
33f004d4b6 | ||
|
|
8963b536ee | ||
|
|
51455e2a1e | ||
|
|
30d6a26e3e | ||
|
|
cd4fabd11b | ||
|
|
9a8b8f26ac | ||
|
|
2f73b16b57 | ||
|
|
df9952c291 | ||
|
|
ee946d8128 | ||
|
|
ec8f2e078e | ||
|
|
335f46122b | ||
|
|
73eae191e9 | ||
|
|
14e823e938 | ||
|
|
2fbd462e6e | ||
|
|
e1cc98d448 | ||
|
|
0ce64962d4 | ||
|
|
338229193f | ||
|
|
57644a4be8 | ||
|
|
da2099137a | ||
|
|
09bc8d9ca4 | ||
|
|
d95f724303 | ||
|
|
c413c3ed8f | ||
|
|
5f56be0ad4 | ||
|
|
ef441d5cff | ||
|
|
16a188c524 | ||
|
|
50c40a8d99 | ||
|
|
4114c8715c | ||
|
|
ced5fdbe70 | ||
|
|
b16aa81e0d | ||
|
|
b44971668c | ||
|
|
0ff4c284e2 | ||
|
|
e8db95be16 | ||
|
|
69c2dd53ad | ||
|
|
14a910bd64 | ||
|
|
52f97ffdc9 | ||
|
|
a1e87f6cd9 | ||
|
|
c2fc41dcd5 | ||
|
|
b62c7943e7 | ||
|
|
64caeeb12d | ||
|
|
e8ac4a1e99 | ||
|
|
19c8654195 | ||
|
|
00d7aed797 | ||
|
|
4477132448 | ||
|
|
eaeea45ace | ||
|
|
e404bf33b1 | ||
|
|
79a7edea5e | ||
|
|
2b05fe2859 | ||
|
|
f8996f0a90 | ||
|
|
eb04cdac41 | ||
|
|
125938c7a1 | ||
|
|
ec1260d8aa | ||
|
|
9eabbe2e76 | ||
|
|
5f35c579e2 | ||
|
|
99a23bdc8f | ||
|
|
d914a08896 | ||
|
|
ceccc30cb8 | ||
|
|
3366a71155 | ||
|
|
cd67804412 | ||
|
|
4a95db6013 | ||
|
|
8bf552ae25 | ||
|
|
ccd0c2382f | ||
|
|
d74663bf53 | ||
|
|
020ee56f25 | ||
|
|
87b295bc3d | ||
|
|
b9b1e92788 | ||
|
|
4000705701 | ||
|
|
673dbeee09 | ||
|
|
5288041782 | ||
|
|
4273fa9ccf | ||
|
|
01bcb9dff9 | ||
|
|
101f86ad1f | ||
|
|
2575dc2db0 | ||
|
|
b4e6f128d7 | ||
|
|
7d5e6718dc | ||
|
|
072aa7569c | ||
|
|
62871283e2 | ||
|
|
0f1dd24f97 | ||
|
|
995f551e80 | ||
|
|
15facd8cfd | ||
|
|
57bd47a446 | ||
|
|
0568c943ab | ||
|
|
b1aaa8570e | ||
|
|
3b2aa7e91d | ||
|
|
997aacf7f0 | ||
|
|
3183978c6b | ||
|
|
6a0b20456f | ||
|
|
8841112b07 | ||
|
|
16dbac6026 | ||
|
|
9b6d03c497 | ||
|
|
d3ea044619 | ||
|
|
364355c8e1 | ||
|
|
88c4e95428 | ||
|
|
9403f6ced5 | ||
|
|
56fe84e516 | ||
|
|
69d1381ba3 | ||
|
|
ccec8c4792 | ||
|
|
83c47e0ed7 | ||
|
|
6d630901b6 | ||
|
|
320622fc27 | ||
|
|
fb8ef1f27b | ||
|
|
333948711d | ||
|
|
001b4be0ae | ||
|
|
427c62d052 | ||
|
|
99097d418b | ||
|
|
75654afeaa | ||
|
|
f10d18956a | ||
|
|
b9253d0b3b | ||
|
|
2458e7597b | ||
|
|
23a721f0a2 | ||
|
|
9bd6be5c6d | ||
|
|
dd6113c9d1 | ||
|
|
d39bcd9c55 | ||
|
|
823d7da4c1 | ||
|
|
7fff191c57 | ||
|
|
9e44085a69 | ||
|
|
5230b91b25 | ||
|
|
493b3d72e4 | ||
|
|
337590c450 | ||
|
|
8e1c7cfe89 | ||
|
|
c60cb6c329 | ||
|
|
acf1dd8500 | ||
|
|
3fb57044d1 | ||
|
|
46a76a778a | ||
|
|
a9a2c23736 | ||
|
|
ccde319937 | ||
|
|
0ed7fac5fb | ||
|
|
80b9cd1292 | ||
|
|
b6c1df41fb | ||
|
|
125af820d0 | ||
|
|
8167e90801 | ||
|
|
82ebf66cba | ||
|
|
883ed4d424 | ||
|
|
e6bf1754c3 | ||
|
|
bcb494d5d1 | ||
|
|
75c0c0a098 | ||
|
|
840d2694b4 | ||
|
|
abdc7b276a | ||
|
|
d4f6deb9ef | ||
|
|
502e85b903 | ||
|
|
ac1e2bfd49 | ||
|
|
b9b071c744 | ||
|
|
83186b6fed | ||
|
|
a3a239967f | ||
|
|
b4fd4bb257 | ||
|
|
eb009d5959 | ||
|
|
4a81bd0f50 | ||
|
|
bbc9142fc5 | ||
|
|
7413c2715c | ||
|
|
7579c3bb15 | ||
|
|
24683058fd | ||
|
|
bf81e3108c | ||
|
|
5ade90416e | ||
|
|
8f2a8086c0 | ||
|
|
38b70f7877 | ||
|
|
5e112a17a5 | ||
|
|
59a3e7e3cc | ||
|
|
2c93f065cb | ||
|
|
488d33c1ed | ||
|
|
de4660ac12 | ||
|
|
27ae341684 | ||
|
|
25b3846694 | ||
|
|
8e2f9f6544 | ||
|
|
76b5870f89 | ||
|
|
604891e793 | ||
|
|
5814df7eaa | ||
|
|
2509d03f42 | ||
|
|
a256df9823 | ||
|
|
af96ec5a30 | ||
|
|
55df80b80e | ||
|
|
7d11986a0a | ||
|
|
d75d90c53e | ||
|
|
35fead2eca | ||
|
|
9bb2efd3ef | ||
|
|
30ffcaa667 | ||
|
|
ba11455786 | ||
|
|
d69ba27f84 | ||
|
|
448b72d046 | ||
|
|
d96d89bcb8 | ||
|
|
7a013d4f40 | ||
|
|
e4597df3b9 | ||
|
|
2b014fcd75 | ||
|
|
c2bf6975f8 | ||
|
|
733e1f79ac | ||
|
|
2a6cbfd5fd | ||
|
|
6173b69a8b | ||
|
|
fc72cfe784 | ||
|
|
768c81cdfd | ||
|
|
bcea8ed593 | ||
|
|
f93bb1dd21 | ||
|
|
b92e8510f9 | ||
|
|
1b692ec7eb | ||
|
|
4c97e2e8bb | ||
|
|
b652198979 | ||
|
|
31c4a1d853 | ||
|
|
6afdb5c0e5 | ||
|
|
bdcf864678 | ||
|
|
0dd5039668 | ||
|
|
a0831eade1 | ||
|
|
f1dc9818b6 | ||
|
|
b52b7c6ded | ||
|
|
e03a41144a | ||
|
|
37bb07e7a3 | ||
|
|
78a6325b64 | ||
|
|
c96923d2c9 | ||
|
|
59742fbfee | ||
|
|
2938a25ec5 | ||
|
|
d163eb3888 | ||
|
|
75c29d4d1c | ||
|
|
e103fb1f93 | ||
|
|
bd79ff87cc | ||
|
|
ac21ec2f46 | ||
|
|
5bcf017c10 | ||
|
|
85d99198b5 | ||
|
|
7f183f7404 | ||
|
|
85284df725 | ||
|
|
87054ee983 | ||
|
|
81245c2548 | ||
|
|
6f82b321d8 | ||
|
|
f4593c6653 | ||
|
|
15902cf54d | ||
|
|
d1102c33ac | ||
|
|
aabbd3383c | ||
|
|
afb55cb7d4 | ||
|
|
ade794a937 | ||
|
|
34271a82ff | ||
|
|
b20a31098a | ||
|
|
b5a039e5ae | ||
|
|
986cc0a01c | ||
|
|
b20fd36c48 | ||
|
|
e4e6bf66e1 | ||
|
|
3ae27273c6 | ||
|
|
eefb3c43dd | ||
|
|
cc229e726e | ||
|
|
49408c00e9 | ||
|
|
76192fbced | ||
|
|
a1c76c79de | ||
|
|
db9e2b1aac | ||
|
|
45c4970d68 | ||
|
|
1d7a9309d6 | ||
|
|
f5ac98251e | ||
|
|
78239045ba | ||
|
|
d8b60875c4 | ||
|
|
48949a6e9d | ||
|
|
082a330ea3 | ||
|
|
adf7df0d5c | ||
|
|
a76ad48563 | ||
|
|
00f991162f | ||
|
|
d6cdd24fad | ||
|
|
c9473756df | ||
|
|
b5d0c56b4c | ||
|
|
96a2f5268c | ||
|
|
5083f9c9c2 | ||
|
|
e34df15ff5 | ||
|
|
26ec87803a | ||
|
|
037e8d4555 | ||
|
|
08a366c4dc | ||
|
|
670e1523e0 | ||
|
|
416f2964b5 | ||
|
|
e018e16898 | ||
|
|
d16c8c9f0f | ||
|
|
fffe20cbe5 | ||
|
|
f6da3c467b | ||
|
|
c0d9f21c0f | ||
|
|
a67b616139 | ||
|
|
2991547974 | ||
|
|
b59def2e4a | ||
|
|
d842353f39 | ||
|
|
d20ef569de | ||
|
|
ca2b871810 | ||
|
|
23ea8ba1ce | ||
|
|
c417fec246 | ||
|
|
5413b16b57 | ||
|
|
43c021ed80 | ||
|
|
3b005d29d7 | ||
|
|
635f70f477 | ||
|
|
adbb6037ac | ||
|
|
598d6d00e4 | ||
|
|
cf934357c9 | ||
|
|
8063e645c7 | ||
|
|
8ab206b443 | ||
|
|
ec5c96e10d | ||
|
|
d2a61290b9 | ||
|
|
10faf9e717 | ||
|
|
cba239bc8f | ||
|
|
6f5e3ddfb3 | ||
|
|
d412ba264a | ||
|
|
9780f2b792 | ||
|
|
5aa2078852 | ||
|
|
eb975bb89c | ||
|
|
9479fe3ce6 | ||
|
|
4393cf8dbe | ||
|
|
447a4ca8c3 | ||
|
|
40ac2549ff | ||
|
|
a9c56b813a | ||
|
|
b1b73c9deb | ||
|
|
774377330b | ||
|
|
e7a157ef8f | ||
|
|
3989b9fc7f | ||
|
|
8bfcdf4831 | ||
|
|
3632ba3785 | ||
|
|
b7b3824d76 | ||
|
|
b12efb2023 | ||
|
|
bd91cf220c | ||
|
|
9eb6731c21 | ||
|
|
11373755d9 | ||
|
|
00b5e9f6ca | ||
|
|
6b3f424e4d | ||
|
|
e7dfeec9c4 | ||
|
|
97893bd7e6 | ||
|
|
bfefdb3752 | ||
|
|
12b79c581e | ||
|
|
ac9b4c7ebf | ||
|
|
208af232ff | ||
|
|
600c6b4973 | ||
|
|
61007a9b94 | ||
|
|
52fe1a5ac5 | ||
|
|
468927e06a | ||
|
|
61562dd9f0 | ||
|
|
c86dd91310 | ||
|
|
9c85a37811 | ||
|
|
51bba6e634 | ||
|
|
e1089bc5de | ||
|
|
618c654aa0 | ||
|
|
4703e859bd | ||
|
|
a1dc4ebbe4 | ||
|
|
e4e6096510 | ||
|
|
c472734933 | ||
|
|
9d068c20bb | ||
|
|
48e4f2f45d | ||
|
|
bbf4574476 | ||
|
|
8bad513140 | ||
|
|
1ff5d888c2 | ||
|
|
5d25758400 | ||
|
|
16fdc90976 | ||
|
|
793542230f | ||
|
|
9de1242d9b | ||
|
|
b3afa84058 | ||
|
|
024a10bbb5 | ||
|
|
bef9ac96e2 | ||
|
|
24bb293136 | ||
|
|
45180104fe | ||
|
|
edd86e3fb7 | ||
|
|
4a72d57534 | ||
|
|
0068cb305f | ||
|
|
90044196bf | ||
|
|
963a926db2 | ||
|
|
0d3d48bb59 | ||
|
|
66eaba4bdc | ||
|
|
21b6e5404e | ||
|
|
a0fe59ab75 | ||
|
|
81ebf56cf1 | ||
|
|
429708e3d5 | ||
|
|
d50f825c6d | ||
|
|
47bfae52c0 | ||
|
|
52cf9e3423 | ||
|
|
a9b6debfa2 | ||
|
|
d6bf475749 | ||
|
|
f22580e943 | ||
|
|
6d98db57c7 | ||
|
|
59f127a250 | ||
|
|
3068e7dcf7 | ||
|
|
f83d62191a | ||
|
|
3b72857124 | ||
|
|
68cd105d9d | ||
|
|
e09af2cb4b | ||
|
|
14bd3b1d30 | ||
|
|
3a9c2152f7 | ||
|
|
7283bfa480 | ||
|
|
37d5099728 | ||
|
|
d45fc030b2 | ||
|
|
c7042c807f | ||
|
|
202f6f1be9 | ||
|
|
759635eefa | ||
|
|
a9981441ae | ||
|
|
71302de4f1 | ||
|
|
333b8e907b | ||
|
|
13f319b64f | ||
|
|
b573eadd9e | ||
|
|
50bfff89c0 | ||
|
|
fc5fc2c570 | ||
|
|
4069999b78 | ||
|
|
5ba9b47b3c | ||
|
|
7c0cc94023 | ||
|
|
3ed1bd2e8e | ||
|
|
ce6436280a | ||
|
|
e49204bd33 | ||
|
|
856c87d05c | ||
|
|
de35c3fb84 | ||
|
|
4359719f9a | ||
|
|
5e13527416 | ||
|
|
aba94c658f | ||
|
|
6e318ba567 | ||
|
|
ddddecf88a | ||
|
|
16cb77c094 | ||
|
|
a5564f730e | ||
|
|
a15c97bbfe | ||
|
|
a398eed8b8 | ||
|
|
a10fd8ca5c | ||
|
|
ff7513238b | ||
|
|
af1cd60d3e | ||
|
|
c66def2049 | ||
|
|
008ccb4729 | ||
|
|
bc232045a1 | ||
|
|
16cab556df | ||
|
|
66148df74b | ||
|
|
4611e08f09 | ||
|
|
bf6204f577 | ||
|
|
17cde9feb7 | ||
|
|
7eccbdc4ac | ||
|
|
ab072290fc | ||
|
|
ad9d83748c | ||
|
|
55b57e1aae | ||
|
|
21b7877beb | ||
|
|
de50234a1a | ||
|
|
d60102ba52 | ||
|
|
066a876f3d | ||
|
|
c07a241ca8 | ||
|
|
0a2fffa9b5 | ||
|
|
bdfa213ccf | ||
|
|
7f0b2ce1ac | ||
|
|
0a2d7af179 | ||
|
|
37652f48fb | ||
|
|
8b19c6c7e4 | ||
|
|
a5365ce294 | ||
|
|
f4a4514a9f | ||
|
|
154006469c | ||
|
|
a1214fff2e | ||
|
|
9fd43ec616 | ||
|
|
5731c268b6 | ||
|
|
f4d892d4e1 | ||
|
|
10b3702938 | ||
|
|
e96442310c | ||
|
|
5c722bf8c4 | ||
|
|
58cc5cdf2a | ||
|
|
3c6dcad2af | ||
|
|
2535f9febf | ||
|
|
25678fa504 | ||
|
|
d7f4f3ec1f | ||
|
|
16ccb39459 | ||
|
|
f8630fb188 | ||
|
|
72e604744d | ||
|
|
832be6e7eb | ||
|
|
8ba48ed71d | ||
|
|
cf266f6162 | ||
|
|
1e6589526d | ||
|
|
f6b3ffaf64 | ||
|
|
5d765d63d4 | ||
|
|
0e12dd62a3 | ||
|
|
2b957b5d1c | ||
|
|
31c7a0157c | ||
|
|
e728b94bca | ||
|
|
49040c0130 | ||
|
|
0d05238ee6 | ||
|
|
9b8a7da1e6 | ||
|
|
61fd21182c | ||
|
|
487c2b5e76 | ||
|
|
0e4703b227 | ||
|
|
84e0232bd5 | ||
|
|
35fbb011b2 | ||
|
|
6527a123f0 | ||
|
|
0377cfd37c | ||
|
|
edc933d816 | ||
|
|
0d608f6014 | ||
|
|
69a45ef7d7 | ||
|
|
1056b36eae | ||
|
|
35c737ac68 | ||
|
|
725a2c2e95 | ||
|
|
c724d2392f | ||
|
|
f5230d1f02 | ||
|
|
078111bd96 | ||
|
|
736f8882f5 | ||
|
|
37cf365927 | ||
|
|
b939470302 | ||
|
|
ef4b2baedc | ||
|
|
64d28ea457 | ||
|
|
2520780846 | ||
|
|
986c60353e | ||
|
|
5fc26c958a | ||
|
|
c1cf9cda6a | ||
|
|
10d376eab2 | ||
|
|
53fc8a861b | ||
|
|
1d8330331c | ||
|
|
7a03c7fe38 | ||
|
|
09bd32169c | ||
|
|
7ec32f834e | ||
|
|
205492c7e8 | ||
|
|
4c2e888709 | ||
|
|
c78fd097d1 | ||
|
|
340966195b | ||
|
|
92604b391b | ||
|
|
0c51feb9c2 | ||
|
|
d0b4169a6b | ||
|
|
1fc6c6fb2a | ||
|
|
14f9b95557 | ||
|
|
d3bf1fa1fa | ||
|
|
a8836c5615 | ||
|
|
779a27693a | ||
|
|
829d86840a | ||
|
|
e225294dd4 | ||
|
|
a673e3650d | ||
|
|
ff462dfd7a | ||
|
|
73443585e5 | ||
|
|
609ab069a9 | ||
|
|
ec3579d7cb | ||
|
|
f80a3fea31 | ||
|
|
43a8d1b1ae | ||
|
|
09fa84ccfc | ||
|
|
b981f0a205 | ||
|
|
767038afc3 | ||
|
|
a7774115c5 | ||
|
|
288bc88e40 | ||
|
|
6d36dbf9de | ||
|
|
4ab4baf3a4 | ||
|
|
90f05eb9c2 | ||
|
|
b63b6d04c6 | ||
|
|
8addaa7e08 | ||
|
|
a96bf8e62d | ||
|
|
c8bda598f5 | ||
|
|
c857cff585 | ||
|
|
fd9d2db755 | ||
|
|
b19fd14f80 | ||
|
|
a0f469095c | ||
|
|
0ccb26df94 | ||
|
|
71fd5966ad | ||
|
|
c02230de4f | ||
|
|
aa2e2c76c0 | ||
|
|
7c2d4ee79a | ||
|
|
e3a2728fa3 | ||
|
|
18260b037b | ||
|
|
ad83dd3ad9 | ||
|
|
6f37315cd1 | ||
|
|
d81dce6a82 | ||
|
|
0bd11e970b | ||
|
|
7e29e1dd23 | ||
|
|
491a2adf8d | ||
|
|
c07d6487a8 | ||
|
|
9990e84d37 | ||
|
|
0b86adbe99 | ||
|
|
834a2c09d5 | ||
|
|
f13c17e654 | ||
|
|
a0611d92e4 | ||
|
|
0b001c3e80 | ||
|
|
53b7cb62c4 | ||
|
|
c5e096c76a | ||
|
|
e1fc4a756b | ||
|
|
e5bc4cbbcf | ||
|
|
459d5ec19b | ||
|
|
8baa222621 | ||
|
|
ce1397cc34 | ||
|
|
dc7c5ced4c | ||
|
|
b8e8fe7e31 | ||
|
|
890085758f | ||
|
|
85f15893bc | ||
|
|
98be75b17c | ||
|
|
b5cc27b8ea | ||
|
|
05937b52cc | ||
|
|
62b82570e1 | ||
|
|
4bf75c0b44 | ||
|
|
a8a06c4983 |
@@ -1,3 +1,7 @@
|
|||||||
|
#
|
||||||
|
# This file is intentionally in the wrong dir, will move and add later....
|
||||||
|
#
|
||||||
|
|
||||||
name: Guidelines Check
|
name: Guidelines Check
|
||||||
|
|
||||||
on:
|
on:
|
||||||
@@ -28,14 +28,14 @@ jobs:
|
|||||||
const versionPattern = /[v]?1\.0\./i;
|
const versionPattern = /[v]?1\.0\./i;
|
||||||
const isVersionRelated = versionPattern.test(title) || versionPattern.test(description);
|
const isVersionRelated = versionPattern.test(title) || versionPattern.test(description);
|
||||||
|
|
||||||
|
// Check for "nix" keyword
|
||||||
|
const nixPattern = /\bnix\b/i;
|
||||||
|
const isNixRelated = nixPattern.test(title) || nixPattern.test(description);
|
||||||
|
|
||||||
|
const labels = [];
|
||||||
|
|
||||||
if (isWebRelated) {
|
if (isWebRelated) {
|
||||||
// Add web label
|
labels.push('web');
|
||||||
await github.rest.issues.addLabels({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: issue.number,
|
|
||||||
labels: ['web']
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assign to adamdotdevin
|
// Assign to adamdotdevin
|
||||||
await github.rest.issues.addAssignees({
|
await github.rest.issues.addAssignees({
|
||||||
@@ -46,10 +46,18 @@ jobs:
|
|||||||
});
|
});
|
||||||
} else if (isVersionRelated) {
|
} else if (isVersionRelated) {
|
||||||
// Only add opentui if NOT web-related
|
// Only add opentui if NOT web-related
|
||||||
|
labels.push('opentui');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNixRelated) {
|
||||||
|
labels.push('nix');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (labels.length > 0) {
|
||||||
await github.rest.issues.addLabels({
|
await github.rest.issues.addLabels({
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
issue_number: issue.number,
|
issue_number: issue.number,
|
||||||
labels: ['opentui']
|
labels: labels
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ jobs:
|
|||||||
|
|
||||||
- uses: ./.github/actions/setup-bun
|
- uses: ./.github/actions/setup-bun
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "24"
|
||||||
|
|
||||||
- run: bun sst deploy --stage=${{ github.ref_name }}
|
- run: bun sst deploy --stage=${{ github.ref_name }}
|
||||||
env:
|
env:
|
||||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
|||||||
@@ -21,18 +21,18 @@ jobs:
|
|||||||
|
|
||||||
- name: Check for duplicate issues
|
- name: Check for duplicate issues
|
||||||
env:
|
env:
|
||||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
OPENCODE_PERMISSION: |
|
OPENCODE_PERMISSION: |
|
||||||
{
|
{
|
||||||
"bash": {
|
"bash": {
|
||||||
"gh issue*": "allow",
|
"gh issue*": "allow",
|
||||||
"*": "deny"
|
"*": "deny"
|
||||||
},
|
},
|
||||||
"webfetch": "deny"
|
"webfetch": "deny"
|
||||||
}
|
}
|
||||||
run: |
|
run: |
|
||||||
opencode run -m anthropic/claude-sonnet-4-20250514 "A new issue has been created:'
|
opencode run -m opencode/claude-haiku-4-5 "A new issue has been created:'
|
||||||
|
|
||||||
Issue number:
|
Issue number:
|
||||||
${{ github.event.issue.number }}
|
${{ github.event.issue.number }}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ name: opencode
|
|||||||
on:
|
on:
|
||||||
issue_comment:
|
issue_comment:
|
||||||
types: [created]
|
types: [created]
|
||||||
|
pull_request_review_comment:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
opencode:
|
opencode:
|
||||||
@@ -21,9 +23,11 @@ jobs:
|
|||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: ./.github/actions/setup-bun
|
||||||
|
|
||||||
- name: Run opencode
|
- name: Run opencode
|
||||||
uses: sst/opencode/github@latest
|
uses: sst/opencode/github@latest
|
||||||
env:
|
env:
|
||||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||||
with:
|
with:
|
||||||
model: opencode/glm-4.6
|
model: opencode/claude-haiku-4-5
|
||||||
|
|||||||
@@ -61,6 +61,13 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
|
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Publish
|
- name: Publish
|
||||||
run: |
|
run: |
|
||||||
./script/publish.ts
|
./script/publish.ts
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
name: snapshot
|
name: snapshot
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- dev
|
- dev
|
||||||
- opentui
|
- test-bedrock
|
||||||
- v0
|
- v0
|
||||||
|
- otui-diffs
|
||||||
|
|
||||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
|
||||||
|
|||||||
@@ -28,3 +28,9 @@ jobs:
|
|||||||
bun turbo test
|
bun turbo test
|
||||||
env:
|
env:
|
||||||
CI: true
|
CI: true
|
||||||
|
|
||||||
|
- name: Check SDK is up to date
|
||||||
|
run: |
|
||||||
|
bun ./packages/sdk/js/script/build.ts
|
||||||
|
git diff --exit-code packages/sdk/js/src/gen packages/sdk/js/dist
|
||||||
|
continue-on-error: false
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
name: Update Nix Hashes
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- "bun.lock"
|
||||||
|
- "package.json"
|
||||||
|
- "packages/*/package.json"
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "bun.lock"
|
||||||
|
- "package.json"
|
||||||
|
- "packages/*/package.json"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update:
|
||||||
|
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
SYSTEM: x86_64-linux
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
fetch-depth: 0
|
||||||
|
ref: ${{ github.head_ref || github.ref_name }}
|
||||||
|
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
|
||||||
|
|
||||||
|
- name: Setup Nix
|
||||||
|
uses: DeterminateSystems/nix-installer-action@v20
|
||||||
|
|
||||||
|
- name: Configure git
|
||||||
|
run: |
|
||||||
|
git config --global user.email "action@github.com"
|
||||||
|
git config --global user.name "Github Action"
|
||||||
|
|
||||||
|
- name: Update flake.lock
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
echo "📦 Updating flake.lock..."
|
||||||
|
nix flake update
|
||||||
|
echo "✅ flake.lock updated successfully"
|
||||||
|
|
||||||
|
- name: Update node_modules hash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
echo "🔄 Updating node_modules hash..."
|
||||||
|
nix/scripts/update-hashes.sh
|
||||||
|
echo "✅ node_modules hash updated successfully"
|
||||||
|
|
||||||
|
- name: Commit hash changes
|
||||||
|
env:
|
||||||
|
TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "🔍 Checking for changes in tracked Nix files..."
|
||||||
|
|
||||||
|
summarize() {
|
||||||
|
local status="$1"
|
||||||
|
{
|
||||||
|
echo "### Nix Hash Update"
|
||||||
|
echo ""
|
||||||
|
echo "- ref: ${GITHUB_REF_NAME}"
|
||||||
|
echo "- status: ${status}"
|
||||||
|
} >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
if [ -n "${GITHUB_SERVER_URL:-}" ] && [ -n "${GITHUB_REPOSITORY:-}" ] && [ -n "${GITHUB_RUN_ID:-}" ]; then
|
||||||
|
echo "- run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
fi
|
||||||
|
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
}
|
||||||
|
|
||||||
|
FILES=(flake.lock flake.nix nix/node-modules.nix nix/hashes.json)
|
||||||
|
STATUS="$(git status --short -- "${FILES[@]}" || true)"
|
||||||
|
if [ -z "$STATUS" ]; then
|
||||||
|
echo "✅ No changes detected. Hashes are already up to date."
|
||||||
|
summarize "no changes"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "📝 Changes detected:"
|
||||||
|
echo "$STATUS"
|
||||||
|
echo "🔗 Staging files..."
|
||||||
|
git add "${FILES[@]}"
|
||||||
|
echo "💾 Committing changes..."
|
||||||
|
git commit -m "Update Nix flake.lock and hashes"
|
||||||
|
echo "✅ Changes committed"
|
||||||
|
|
||||||
|
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
|
||||||
|
echo "🌳 Pulling latest from branch: $BRANCH"
|
||||||
|
git pull --rebase origin "$BRANCH"
|
||||||
|
echo "🚀 Pushing changes to branch: $BRANCH"
|
||||||
|
git push origin HEAD:"$BRANCH"
|
||||||
|
echo "✅ Changes pushed successfully"
|
||||||
|
|
||||||
|
summarize "committed $(git rev-parse --short HEAD)"
|
||||||
@@ -13,3 +13,8 @@ dist
|
|||||||
.turbo
|
.turbo
|
||||||
**/.serena
|
**/.serena
|
||||||
.serena/
|
.serena/
|
||||||
|
/result
|
||||||
|
refs
|
||||||
|
Session.vim
|
||||||
|
opencode.json
|
||||||
|
a.out
|
||||||
|
|||||||
@@ -1,2 +1,9 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
# Check if bun version matches package.json
|
||||||
|
EXPECTED_VERSION=$(grep '"packageManager"' package.json | sed 's/.*"bun@\([^"]*\)".*/\1/')
|
||||||
|
CURRENT_VERSION=$(bun --version)
|
||||||
|
if [ "$CURRENT_VERSION" != "$EXPECTED_VERSION" ]; then
|
||||||
|
echo "Error: Bun version $CURRENT_VERSION does not match expected version $EXPECTED_VERSION from package.json"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
bun typecheck
|
bun typecheck
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
description: "Find issue(s) on github"
|
||||||
|
model: opencode/claude-haiku-4-5
|
||||||
|
---
|
||||||
|
|
||||||
|
Search through existing issues in sst/opencode using the gh cli to find issues matching this query:
|
||||||
|
|
||||||
|
$ARGUMENTS
|
||||||
|
|
||||||
|
Consider:
|
||||||
|
|
||||||
|
1. Similar titles or descriptions
|
||||||
|
2. Same error messages or symptoms
|
||||||
|
3. Related functionality or components
|
||||||
|
4. Similar feature requests
|
||||||
|
|
||||||
|
Please list any matching issues with:
|
||||||
|
|
||||||
|
- Issue number and title
|
||||||
|
- Brief explanation of why it matches the query
|
||||||
|
- Link to the issue
|
||||||
|
|
||||||
|
If no clear matches are found, say so.
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://opencode.ai/config.json",
|
|
||||||
"plugin": ["opencode-openai-codex-auth"]
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"plugin": ["opencode-openai-codex-auth"],
|
||||||
|
// "enterprise": {
|
||||||
|
// "url": "https://enterprise.dev.opencode.ai",
|
||||||
|
// },
|
||||||
|
"provider": {
|
||||||
|
"opencode": {
|
||||||
|
"options": {
|
||||||
|
// "baseURL": "http://localhost:8080",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"mcp": {
|
||||||
|
"exa": {
|
||||||
|
"type": "remote",
|
||||||
|
"url": "https://mcp.exa.ai/mcp",
|
||||||
|
},
|
||||||
|
"morph": {
|
||||||
|
"type": "local",
|
||||||
|
"command": ["bunx", "@morphllm/morphmcp"],
|
||||||
|
"environment": {
|
||||||
|
"ENABLED_TOOLS": "warp_grep",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/theme.json",
|
||||||
|
"defs": {
|
||||||
|
"nord0": "#2E3440",
|
||||||
|
"nord1": "#3B4252",
|
||||||
|
"nord2": "#434C5E",
|
||||||
|
"nord3": "#4C566A",
|
||||||
|
"nord4": "#D8DEE9",
|
||||||
|
"nord5": "#E5E9F0",
|
||||||
|
"nord6": "#ECEFF4",
|
||||||
|
"nord7": "#8FBCBB",
|
||||||
|
"nord8": "#88C0D0",
|
||||||
|
"nord9": "#81A1C1",
|
||||||
|
"nord10": "#5E81AC",
|
||||||
|
"nord11": "#BF616A",
|
||||||
|
"nord12": "#D08770",
|
||||||
|
"nord13": "#EBCB8B",
|
||||||
|
"nord14": "#A3BE8C",
|
||||||
|
"nord15": "#B48EAD"
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"primary": {
|
||||||
|
"dark": "nord8",
|
||||||
|
"light": "nord10"
|
||||||
|
},
|
||||||
|
"secondary": {
|
||||||
|
"dark": "nord9",
|
||||||
|
"light": "nord9"
|
||||||
|
},
|
||||||
|
"accent": {
|
||||||
|
"dark": "nord7",
|
||||||
|
"light": "nord7"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"dark": "nord11",
|
||||||
|
"light": "nord11"
|
||||||
|
},
|
||||||
|
"warning": {
|
||||||
|
"dark": "nord12",
|
||||||
|
"light": "nord12"
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"dark": "nord14",
|
||||||
|
"light": "nord14"
|
||||||
|
},
|
||||||
|
"info": {
|
||||||
|
"dark": "nord8",
|
||||||
|
"light": "nord10"
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"dark": "nord4",
|
||||||
|
"light": "nord0"
|
||||||
|
},
|
||||||
|
"textMuted": {
|
||||||
|
"dark": "nord3",
|
||||||
|
"light": "nord1"
|
||||||
|
},
|
||||||
|
"background": {
|
||||||
|
"dark": "nord0",
|
||||||
|
"light": "nord6"
|
||||||
|
},
|
||||||
|
"backgroundPanel": {
|
||||||
|
"dark": "nord1",
|
||||||
|
"light": "nord5"
|
||||||
|
},
|
||||||
|
"backgroundElement": {
|
||||||
|
"dark": "nord1",
|
||||||
|
"light": "nord4"
|
||||||
|
},
|
||||||
|
"border": {
|
||||||
|
"dark": "nord2",
|
||||||
|
"light": "nord3"
|
||||||
|
},
|
||||||
|
"borderActive": {
|
||||||
|
"dark": "nord3",
|
||||||
|
"light": "nord2"
|
||||||
|
},
|
||||||
|
"borderSubtle": {
|
||||||
|
"dark": "nord2",
|
||||||
|
"light": "nord3"
|
||||||
|
},
|
||||||
|
"diffAdded": {
|
||||||
|
"dark": "nord14",
|
||||||
|
"light": "nord14"
|
||||||
|
},
|
||||||
|
"diffRemoved": {
|
||||||
|
"dark": "nord11",
|
||||||
|
"light": "nord11"
|
||||||
|
},
|
||||||
|
"diffContext": {
|
||||||
|
"dark": "nord3",
|
||||||
|
"light": "nord3"
|
||||||
|
},
|
||||||
|
"diffHunkHeader": {
|
||||||
|
"dark": "nord3",
|
||||||
|
"light": "nord3"
|
||||||
|
},
|
||||||
|
"diffHighlightAdded": {
|
||||||
|
"dark": "nord14",
|
||||||
|
"light": "nord14"
|
||||||
|
},
|
||||||
|
"diffHighlightRemoved": {
|
||||||
|
"dark": "nord11",
|
||||||
|
"light": "nord11"
|
||||||
|
},
|
||||||
|
"diffAddedBg": {
|
||||||
|
"dark": "#3B4252",
|
||||||
|
"light": "#E5E9F0"
|
||||||
|
},
|
||||||
|
"diffRemovedBg": {
|
||||||
|
"dark": "#3B4252",
|
||||||
|
"light": "#E5E9F0"
|
||||||
|
},
|
||||||
|
"diffContextBg": {
|
||||||
|
"dark": "nord1",
|
||||||
|
"light": "nord5"
|
||||||
|
},
|
||||||
|
"diffLineNumber": {
|
||||||
|
"dark": "nord2",
|
||||||
|
"light": "nord4"
|
||||||
|
},
|
||||||
|
"diffAddedLineNumberBg": {
|
||||||
|
"dark": "#3B4252",
|
||||||
|
"light": "#E5E9F0"
|
||||||
|
},
|
||||||
|
"diffRemovedLineNumberBg": {
|
||||||
|
"dark": "#3B4252",
|
||||||
|
"light": "#E5E9F0"
|
||||||
|
},
|
||||||
|
"markdownText": {
|
||||||
|
"dark": "nord4",
|
||||||
|
"light": "nord0"
|
||||||
|
},
|
||||||
|
"markdownHeading": {
|
||||||
|
"dark": "nord8",
|
||||||
|
"light": "nord10"
|
||||||
|
},
|
||||||
|
"markdownLink": {
|
||||||
|
"dark": "nord9",
|
||||||
|
"light": "nord9"
|
||||||
|
},
|
||||||
|
"markdownLinkText": {
|
||||||
|
"dark": "nord7",
|
||||||
|
"light": "nord7"
|
||||||
|
},
|
||||||
|
"markdownCode": {
|
||||||
|
"dark": "nord14",
|
||||||
|
"light": "nord14"
|
||||||
|
},
|
||||||
|
"markdownBlockQuote": {
|
||||||
|
"dark": "nord3",
|
||||||
|
"light": "nord3"
|
||||||
|
},
|
||||||
|
"markdownEmph": {
|
||||||
|
"dark": "nord12",
|
||||||
|
"light": "nord12"
|
||||||
|
},
|
||||||
|
"markdownStrong": {
|
||||||
|
"dark": "nord13",
|
||||||
|
"light": "nord13"
|
||||||
|
},
|
||||||
|
"markdownHorizontalRule": {
|
||||||
|
"dark": "nord3",
|
||||||
|
"light": "nord3"
|
||||||
|
},
|
||||||
|
"markdownListItem": {
|
||||||
|
"dark": "nord8",
|
||||||
|
"light": "nord10"
|
||||||
|
},
|
||||||
|
"markdownListEnumeration": {
|
||||||
|
"dark": "nord7",
|
||||||
|
"light": "nord7"
|
||||||
|
},
|
||||||
|
"markdownImage": {
|
||||||
|
"dark": "nord9",
|
||||||
|
"light": "nord9"
|
||||||
|
},
|
||||||
|
"markdownImageText": {
|
||||||
|
"dark": "nord7",
|
||||||
|
"light": "nord7"
|
||||||
|
},
|
||||||
|
"markdownCodeBlock": {
|
||||||
|
"dark": "nord4",
|
||||||
|
"light": "nord0"
|
||||||
|
},
|
||||||
|
"syntaxComment": {
|
||||||
|
"dark": "nord3",
|
||||||
|
"light": "nord3"
|
||||||
|
},
|
||||||
|
"syntaxKeyword": {
|
||||||
|
"dark": "nord9",
|
||||||
|
"light": "nord9"
|
||||||
|
},
|
||||||
|
"syntaxFunction": {
|
||||||
|
"dark": "nord8",
|
||||||
|
"light": "nord8"
|
||||||
|
},
|
||||||
|
"syntaxVariable": {
|
||||||
|
"dark": "nord7",
|
||||||
|
"light": "nord7"
|
||||||
|
},
|
||||||
|
"syntaxString": {
|
||||||
|
"dark": "nord14",
|
||||||
|
"light": "nord14"
|
||||||
|
},
|
||||||
|
"syntaxNumber": {
|
||||||
|
"dark": "nord15",
|
||||||
|
"light": "nord15"
|
||||||
|
},
|
||||||
|
"syntaxType": {
|
||||||
|
"dark": "nord7",
|
||||||
|
"light": "nord7"
|
||||||
|
},
|
||||||
|
"syntaxOperator": {
|
||||||
|
"dark": "nord9",
|
||||||
|
"light": "nord9"
|
||||||
|
},
|
||||||
|
"syntaxPunctuation": {
|
||||||
|
"dark": "nord4",
|
||||||
|
"light": "nord0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+11
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "bun",
|
||||||
|
"request": "attach",
|
||||||
|
"name": "opencode (attach)",
|
||||||
|
"url": "ws://localhost:6499/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"oven.bun-vscode"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -42,6 +42,38 @@ Want to take on an issue? Leave a comment and a maintainer may assign it to you
|
|||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> After touching `packages/opencode/src/server/server.ts`, run "./packages/sdk/js/script/build.ts" to regenerate the JS sdk.
|
> After touching `packages/opencode/src/server/server.ts`, run "./packages/sdk/js/script/build.ts" to regenerate the JS sdk.
|
||||||
|
|
||||||
|
### Setting up a Debugger
|
||||||
|
|
||||||
|
Bun debugging is currently rough around the edges. We hope this guide helps you get set up and avoid some pain points.
|
||||||
|
|
||||||
|
The most reliable way to debug OpenCode is to run it manually in a terminal via `bun run --inspect=<url> dev ...` and attach
|
||||||
|
your debugger via that URL. Other methods can result in breakpoints being mapped incorrectly, at least in VSCode (YMMV).
|
||||||
|
|
||||||
|
Caveats:
|
||||||
|
|
||||||
|
- `*.tsx` files won't have their breakpoints correctly mapped. This seems due to Bun currently not supporting source maps on code transformed
|
||||||
|
via `BunPlugin`s (currently necessary due to our dependency on `@opentui/solid`). Currently, the best you can do in terms of debugging `*.tsx`
|
||||||
|
files is writing a `debugger;` statement. Debugging facilities like stepping won't work, but at least you will be informed if a specific code
|
||||||
|
is triggered.
|
||||||
|
- If you want to run the OpenCode TUI and have breakpoints triggered in the server code, you might need to run `bun dev spawn` instead of
|
||||||
|
the usual `bun dev`. This is because `bun dev` runs the server in a worker thread and breakpoints might not work there.
|
||||||
|
|
||||||
|
Other tips and tricks:
|
||||||
|
|
||||||
|
- You might want to use `--inspect-wait` or `--inspect-brk` instead of `--inspect`, depending on your workflow
|
||||||
|
- Specifying `--inspect=ws://localhost:6499/` on every invocation can be tiresome, you may want to `export BUN_OPTIONS=--inspect=ws://localhost:6499/` instead
|
||||||
|
|
||||||
|
#### VSCode Setup
|
||||||
|
|
||||||
|
If you use VSCode, you can use our example configurations [.vscode/settings.example.json](.vscode/settings.example.json) and [.vscode/launch.example.json](.vscode/launch.example.json).
|
||||||
|
|
||||||
|
Some debug methods that can be problematic:
|
||||||
|
|
||||||
|
- Debug configurations with `"request": "launch"` can have breakpoints incorrectly mapped and thus unusable
|
||||||
|
- The same problem arises when running OpenCode in the VSCode `JavaScript Debug Terminal`
|
||||||
|
|
||||||
|
With that said, you may want to try these methods, as they might work for you.
|
||||||
|
|
||||||
## Pull Request Expectations
|
## Pull Request Expectations
|
||||||
|
|
||||||
- Try to keep pull requests small and focused.
|
- Try to keep pull requests small and focused.
|
||||||
|
|||||||
@@ -28,8 +28,10 @@ curl -fsSL https://opencode.ai/install | bash
|
|||||||
npm i -g opencode-ai@latest # or bun/pnpm/yarn
|
npm i -g opencode-ai@latest # or bun/pnpm/yarn
|
||||||
scoop bucket add extras; scoop install extras/opencode # Windows
|
scoop bucket add extras; scoop install extras/opencode # Windows
|
||||||
choco install opencode # Windows
|
choco install opencode # Windows
|
||||||
brew install opencode # macOS and Linux
|
brew install opencode # macOS and Linux
|
||||||
paru -S opencode-bin # Arch Linux
|
paru -S opencode-bin # Arch Linux
|
||||||
|
mise use --pin -g ubi:sst/opencode # Any OS
|
||||||
|
nix run nixpkgs#opencode # or github:sst/opencode for latest dev branch
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
@@ -50,6 +52,22 @@ OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bas
|
|||||||
XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
|
XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Agents
|
||||||
|
|
||||||
|
OpenCode includes two built-in agents you can switch between,
|
||||||
|
you can switch between these using the `Tab` key.
|
||||||
|
|
||||||
|
- **build** - Default, full access agent for development work
|
||||||
|
- **plan** - Read-only agent for analysis and code exploration
|
||||||
|
- Denies file edits by default
|
||||||
|
- Asks permission before running bash commands
|
||||||
|
- Ideal for exploring unfamiliar codebases or planning changes
|
||||||
|
|
||||||
|
Also, included is a **general** subagent for complex searches and multi-step tasks.
|
||||||
|
This is used internally and can be invoked using `@general` in messages.
|
||||||
|
|
||||||
|
Learn more about [agents](https://opencode.ai/docs/agents).
|
||||||
|
|
||||||
### Documentation
|
### Documentation
|
||||||
|
|
||||||
For more info on how to configure OpenCode [**head over to our docs**](https://opencode.ai/docs).
|
For more info on how to configure OpenCode [**head over to our docs**](https://opencode.ai/docs).
|
||||||
@@ -58,6 +76,10 @@ For more info on how to configure OpenCode [**head over to our docs**](https://o
|
|||||||
|
|
||||||
If you're interested in contributing to OpenCode, please read our [contributing docs](./CONTRIBUTING.md) before submitting a pull request.
|
If you're interested in contributing to OpenCode, please read our [contributing docs](./CONTRIBUTING.md) before submitting a pull request.
|
||||||
|
|
||||||
|
### Building on OpenCode
|
||||||
|
|
||||||
|
If you are working on a project that's related to OpenCode and is using "opencode" as a part of its name; for example, "opencode-dashboard" or "opencode-mobile", please add a note to your README to clarify that it is not built by the OpenCode team and is not affiliated with us in anyway.
|
||||||
|
|
||||||
### FAQ
|
### FAQ
|
||||||
|
|
||||||
#### How is this different than Claude Code?
|
#### How is this different than Claude Code?
|
||||||
@@ -65,7 +87,7 @@ If you're interested in contributing to OpenCode, please read our [contributing
|
|||||||
It's very similar to Claude Code in terms of capability. Here are the key differences:
|
It's very similar to Claude Code in terms of capability. Here are the key differences:
|
||||||
|
|
||||||
- 100% open source
|
- 100% open source
|
||||||
- Not coupled to any provider. Although Anthropic is recommended, OpenCode can be used with OpenAI, Google or even local models. As models evolve the gaps between them will close and pricing will drop so being provider-agnostic is important.
|
- Not coupled to any provider. Although we recommend the models we provide through [OpenCode Zen](https://opencode.ai/zen); OpenCode can be used with Claude, OpenAI, Google or even local models. As models evolve the gaps between them will close and pricing will drop so being provider-agnostic is important.
|
||||||
- Out of the box LSP support
|
- Out of the box LSP support
|
||||||
- A focus on TUI. OpenCode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal.
|
- A focus on TUI. OpenCode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal.
|
||||||
- A client/server architecture. This for example can allow OpenCode to run on your computer, while you can drive it remotely from a mobile app. Meaning that the TUI frontend is just one of the possible clients.
|
- A client/server architecture. This for example can allow OpenCode to run on your computer, while you can drive it remotely from a mobile app. Meaning that the TUI frontend is just one of the possible clients.
|
||||||
|
|||||||
@@ -137,3 +137,20 @@
|
|||||||
| 2025-11-09 | 713,462 (+7,427) | 660,459 (+6,970) | 1,373,921 (+14,397) |
|
| 2025-11-09 | 713,462 (+7,427) | 660,459 (+6,970) | 1,373,921 (+14,397) |
|
||||||
| 2025-11-10 | 722,288 (+8,826) | 668,225 (+7,766) | 1,390,513 (+16,592) |
|
| 2025-11-10 | 722,288 (+8,826) | 668,225 (+7,766) | 1,390,513 (+16,592) |
|
||||||
| 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) |
|
| 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) |
|
||||||
|
| 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) |
|
||||||
|
| 2025-11-13 | 749,905 (+9,725) | 696,157 (+9,703) | 1,446,062 (+19,428) |
|
||||||
|
| 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) |
|
||||||
|
| 2025-11-15 | 765,955 (+6,027) | 712,870 (+7,633) | 1,478,825 (+13,660) |
|
||||||
|
| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) |
|
||||||
|
| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) |
|
||||||
|
| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) |
|
||||||
|
| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) |
|
||||||
|
| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) |
|
||||||
|
| 2025-11-21 | 826,309 (+11,689) | 769,307 (+11,400) | 1,595,616 (+23,089) |
|
||||||
|
| 2025-11-22 | 837,269 (+10,960) | 780,996 (+11,689) | 1,618,265 (+22,649) |
|
||||||
|
| 2025-11-23 | 846,609 (+9,340) | 795,069 (+14,073) | 1,641,678 (+23,413) |
|
||||||
|
| 2025-11-24 | 856,733 (+10,124) | 804,033 (+8,964) | 1,660,766 (+19,088) |
|
||||||
|
| 2025-11-25 | 869,423 (+12,690) | 817,339 (+13,306) | 1,686,762 (+25,996) |
|
||||||
|
| 2025-11-26 | 881,414 (+11,991) | 832,518 (+15,179) | 1,713,932 (+27,170) |
|
||||||
|
| 2025-11-27 | 893,960 (+12,546) | 846,180 (+13,662) | 1,740,140 (+26,208) |
|
||||||
|
| 2025-11-28 | 901,741 (+7,781) | 856,482 (+10,302) | 1,758,223 (+18,083) |
|
||||||
|
|||||||
Generated
+27
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1764290847,
|
||||||
|
"narHash": "sha256-VwPgoDgnd628GdE3KyLqTyPF1WWh0VwT5UoKygoi8sg=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "cd5fedfc384cb98d9fd3827b55f4522f49efda42",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
{
|
||||||
|
description = "OpenCode development flake";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
{
|
||||||
|
nixpkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
systems = [
|
||||||
|
"aarch64-linux"
|
||||||
|
"x86_64-linux"
|
||||||
|
"aarch64-darwin"
|
||||||
|
"x86_64-darwin"
|
||||||
|
];
|
||||||
|
lib = nixpkgs.lib;
|
||||||
|
forEachSystem = lib.genAttrs systems;
|
||||||
|
pkgsFor = system: nixpkgs.legacyPackages.${system};
|
||||||
|
packageJson = builtins.fromJSON (builtins.readFile ./packages/opencode/package.json);
|
||||||
|
bunTarget = {
|
||||||
|
"aarch64-linux" = "bun-linux-arm64";
|
||||||
|
"x86_64-linux" = "bun-linux-x64";
|
||||||
|
"aarch64-darwin" = "bun-darwin-arm64";
|
||||||
|
"x86_64-darwin" = "bun-darwin-x64";
|
||||||
|
};
|
||||||
|
defaultNodeModules = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
|
||||||
|
hashesFile = "${./nix}/hashes.json";
|
||||||
|
hashesData =
|
||||||
|
if builtins.pathExists hashesFile then builtins.fromJSON (builtins.readFile hashesFile) else { };
|
||||||
|
nodeModulesHash = hashesData.nodeModules or defaultNodeModules;
|
||||||
|
modelsDev = forEachSystem (
|
||||||
|
system:
|
||||||
|
let
|
||||||
|
pkgs = pkgsFor system;
|
||||||
|
in
|
||||||
|
pkgs."models-dev"
|
||||||
|
);
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShells = forEachSystem (
|
||||||
|
system:
|
||||||
|
let
|
||||||
|
pkgs = pkgsFor system;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
default = pkgs.mkShell {
|
||||||
|
packages = with pkgs; [
|
||||||
|
bun
|
||||||
|
nodejs_20
|
||||||
|
pkg-config
|
||||||
|
openssl
|
||||||
|
git
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
packages = forEachSystem (
|
||||||
|
system:
|
||||||
|
let
|
||||||
|
pkgs = pkgsFor system;
|
||||||
|
mkNodeModules = pkgs.callPackage ./nix/node-modules.nix {
|
||||||
|
hash = nodeModulesHash;
|
||||||
|
};
|
||||||
|
mkPackage = pkgs.callPackage ./nix/opencode.nix { };
|
||||||
|
in
|
||||||
|
{
|
||||||
|
default = mkPackage {
|
||||||
|
version = packageJson.version;
|
||||||
|
src = ./.;
|
||||||
|
scripts = ./nix/scripts;
|
||||||
|
target = bunTarget.${system};
|
||||||
|
modelsDev = "${modelsDev.${system}}/dist/_api.json";
|
||||||
|
mkNodeModules = mkNodeModules;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
apps = forEachSystem (
|
||||||
|
system:
|
||||||
|
let
|
||||||
|
pkgs = pkgsFor system;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
opencode-dev = {
|
||||||
|
type = "app";
|
||||||
|
meta = {
|
||||||
|
description = "Nix devshell shell for OpenCode";
|
||||||
|
runtimeInputs = [ pkgs.bun ];
|
||||||
|
};
|
||||||
|
program = "${
|
||||||
|
pkgs.writeShellApplication {
|
||||||
|
name = "opencode-dev";
|
||||||
|
text = ''
|
||||||
|
exec bun run dev "$@"
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
}/bin/opencode-dev";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -30,6 +30,24 @@ Leave the following comment on a GitHub PR. opencode will implement the requeste
|
|||||||
Delete the attachment from S3 when the note is removed /oc
|
Delete the attachment from S3 when the note is removed /oc
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Review specific code lines
|
||||||
|
|
||||||
|
Leave a comment directly on code lines in the PR's "Files" tab. opencode will automatically detect the file, line numbers, and diff context to provide precise responses.
|
||||||
|
|
||||||
|
```
|
||||||
|
[Comment on specific lines in Files tab]
|
||||||
|
/oc add error handling here
|
||||||
|
```
|
||||||
|
|
||||||
|
When commenting on specific lines, opencode receives:
|
||||||
|
|
||||||
|
- The exact file being reviewed
|
||||||
|
- The specific lines of code
|
||||||
|
- The surrounding diff context
|
||||||
|
- Line number information
|
||||||
|
|
||||||
|
This allows for more targeted requests without needing to specify file paths or line numbers manually.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Run the following command in the terminal from your GitHub repo:
|
Run the following command in the terminal from your GitHub repo:
|
||||||
@@ -51,6 +69,8 @@ This will walk you through installing the GitHub app, creating the workflow, and
|
|||||||
on:
|
on:
|
||||||
issue_comment:
|
issue_comment:
|
||||||
types: [created]
|
types: [created]
|
||||||
|
pull_request_review_comment:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
opencode:
|
opencode:
|
||||||
@@ -135,3 +155,9 @@ Replace the image URL `https://github.com/user-attachments/assets/xxxxxxxx` with
|
|||||||
```
|
```
|
||||||
MOCK_EVENT='{"eventName":"issue_comment","repo":{"owner":"sst","repo":"hello-world"},"actor":"fwang","payload":{"issue":{"number":4,"pull_request":{}},"comment":{"id":1,"body":"hey opencode, summarize thread"}}}'
|
MOCK_EVENT='{"eventName":"issue_comment","repo":{"owner":"sst","repo":"hello-world"},"actor":"fwang","payload":{"issue":{"number":4,"pull_request":{}},"comment":{"id":1,"body":"hey opencode, summarize thread"}}}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### PR review comment event
|
||||||
|
|
||||||
|
```
|
||||||
|
MOCK_EVENT='{"eventName":"pull_request_review_comment","repo":{"owner":"sst","repo":"hello-world"},"actor":"fwang","payload":{"pull_request":{"number":7},"comment":{"id":1,"body":"hey opencode, add error handling","path":"src/components/Button.tsx","diff_hunk":"@@ -45,8 +45,11 @@\n- const handleClick = () => {\n- console.log('clicked')\n+ const handleClick = useCallback(() => {\n+ console.log('clicked')\n+ doSomething()\n+ }, [doSomething])","line":47,"original_line":45,"position":10,"commit_id":"abc123","original_commit_id":"def456"}}}'
|
||||||
|
```
|
||||||
|
|||||||
+44
-7
@@ -5,7 +5,7 @@ import { graphql } from "@octokit/graphql"
|
|||||||
import * as core from "@actions/core"
|
import * as core from "@actions/core"
|
||||||
import * as github from "@actions/github"
|
import * as github from "@actions/github"
|
||||||
import type { Context as GitHubContext } from "@actions/github/lib/context"
|
import type { Context as GitHubContext } from "@actions/github/lib/context"
|
||||||
import type { IssueCommentEvent } from "@octokit/webhooks-types"
|
import type { IssueCommentEvent, PullRequestReviewCommentEvent } from "@octokit/webhooks-types"
|
||||||
import { createOpencodeClient } from "@opencode-ai/sdk"
|
import { createOpencodeClient } from "@opencode-ai/sdk"
|
||||||
import { spawn } from "node:child_process"
|
import { spawn } from "node:child_process"
|
||||||
|
|
||||||
@@ -124,7 +124,7 @@ let exitCode = 0
|
|||||||
type PromptFiles = Awaited<ReturnType<typeof getUserPrompt>>["promptFiles"]
|
type PromptFiles = Awaited<ReturnType<typeof getUserPrompt>>["promptFiles"]
|
||||||
|
|
||||||
try {
|
try {
|
||||||
assertContextEvent("issue_comment")
|
assertContextEvent("issue_comment", "pull_request_review_comment")
|
||||||
assertPayloadKeyword()
|
assertPayloadKeyword()
|
||||||
await assertOpencodeConnected()
|
await assertOpencodeConnected()
|
||||||
|
|
||||||
@@ -241,19 +241,43 @@ function createOpencode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function assertPayloadKeyword() {
|
function assertPayloadKeyword() {
|
||||||
const payload = useContext().payload as IssueCommentEvent
|
const payload = useContext().payload as IssueCommentEvent | PullRequestReviewCommentEvent
|
||||||
const body = payload.comment.body.trim()
|
const body = payload.comment.body.trim()
|
||||||
if (!body.match(/(?:^|\s)(?:\/opencode|\/oc)(?=$|\s)/)) {
|
if (!body.match(/(?:^|\s)(?:\/opencode|\/oc)(?=$|\s)/)) {
|
||||||
throw new Error("Comments must mention `/opencode` or `/oc`")
|
throw new Error("Comments must mention `/opencode` or `/oc`")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getReviewCommentContext() {
|
||||||
|
const context = useContext()
|
||||||
|
if (context.eventName !== "pull_request_review_comment") {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = context.payload as PullRequestReviewCommentEvent
|
||||||
|
return {
|
||||||
|
file: payload.comment.path,
|
||||||
|
diffHunk: payload.comment.diff_hunk,
|
||||||
|
line: payload.comment.line,
|
||||||
|
originalLine: payload.comment.original_line,
|
||||||
|
position: payload.comment.position,
|
||||||
|
commitId: payload.comment.commit_id,
|
||||||
|
originalCommitId: payload.comment.original_commit_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function assertOpencodeConnected() {
|
async function assertOpencodeConnected() {
|
||||||
let retry = 0
|
let retry = 0
|
||||||
let connected = false
|
let connected = false
|
||||||
do {
|
do {
|
||||||
try {
|
try {
|
||||||
await client.app.get<true>()
|
await client.app.log<true>({
|
||||||
|
body: {
|
||||||
|
service: "github-workflow",
|
||||||
|
level: "info",
|
||||||
|
message: "Prepare to react to Github Workflow event",
|
||||||
|
},
|
||||||
|
})
|
||||||
connected = true
|
connected = true
|
||||||
break
|
break
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
@@ -383,11 +407,24 @@ async function createComment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getUserPrompt() {
|
async function getUserPrompt() {
|
||||||
|
const context = useContext()
|
||||||
|
const payload = context.payload as IssueCommentEvent | PullRequestReviewCommentEvent
|
||||||
|
const reviewContext = getReviewCommentContext()
|
||||||
|
|
||||||
let prompt = (() => {
|
let prompt = (() => {
|
||||||
const payload = useContext().payload as IssueCommentEvent
|
|
||||||
const body = payload.comment.body.trim()
|
const body = payload.comment.body.trim()
|
||||||
if (body === "/opencode" || body === "/oc") return "Summarize this thread"
|
if (body === "/opencode" || body === "/oc") {
|
||||||
if (body.includes("/opencode") || body.includes("/oc")) return body
|
if (reviewContext) {
|
||||||
|
return `Review this code change and suggest improvements for the commented lines:\n\nFile: ${reviewContext.file}\nLines: ${reviewContext.line}\n\n${reviewContext.diffHunk}`
|
||||||
|
}
|
||||||
|
return "Summarize this thread"
|
||||||
|
}
|
||||||
|
if (body.includes("/opencode") || body.includes("/oc")) {
|
||||||
|
if (reviewContext) {
|
||||||
|
return `${body}\n\nContext: You are reviewing a comment on file "${reviewContext.file}" at line ${reviewContext.line}.\n\nDiff context:\n${reviewContext.diffHunk}`
|
||||||
|
}
|
||||||
|
return body
|
||||||
|
}
|
||||||
throw new Error("Comments must mention `/opencode` or `/oc`")
|
throw new Error("Comments must mention `/opencode` or `/oc`")
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
|||||||
+10
-4
@@ -97,8 +97,12 @@ export const stripeWebhook = new stripe.WebhookEndpoint("StripeWebhookEndpoint",
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
const ZEN_MODELS1 = new sst.Secret("ZEN_MODELS1")
|
const ZEN_MODELS = [
|
||||||
const ZEN_MODELS2 = new sst.Secret("ZEN_MODELS2")
|
new sst.Secret("ZEN_MODELS1"),
|
||||||
|
new sst.Secret("ZEN_MODELS2"),
|
||||||
|
new sst.Secret("ZEN_MODELS3"),
|
||||||
|
new sst.Secret("ZEN_MODELS4"),
|
||||||
|
]
|
||||||
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
|
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
|
||||||
const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
|
const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
|
||||||
properties: { value: auth.url.apply((url) => url!) },
|
properties: { value: auth.url.apply((url) => url!) },
|
||||||
@@ -112,6 +116,8 @@ const gatewayKv = new sst.cloudflare.Kv("GatewayKv")
|
|||||||
// CONSOLE
|
// CONSOLE
|
||||||
////////////////
|
////////////////
|
||||||
|
|
||||||
|
const bucket = new sst.cloudflare.Bucket("ConsoleData")
|
||||||
|
|
||||||
const AWS_SES_ACCESS_KEY_ID = new sst.Secret("AWS_SES_ACCESS_KEY_ID")
|
const AWS_SES_ACCESS_KEY_ID = new sst.Secret("AWS_SES_ACCESS_KEY_ID")
|
||||||
const AWS_SES_SECRET_ACCESS_KEY = new sst.Secret("AWS_SES_SECRET_ACCESS_KEY")
|
const AWS_SES_SECRET_ACCESS_KEY = new sst.Secret("AWS_SES_SECRET_ACCESS_KEY")
|
||||||
|
|
||||||
@@ -128,15 +134,15 @@ new sst.cloudflare.x.SolidStart("Console", {
|
|||||||
domain,
|
domain,
|
||||||
path: "packages/console/app",
|
path: "packages/console/app",
|
||||||
link: [
|
link: [
|
||||||
|
bucket,
|
||||||
database,
|
database,
|
||||||
AUTH_API_URL,
|
AUTH_API_URL,
|
||||||
STRIPE_WEBHOOK_SECRET,
|
STRIPE_WEBHOOK_SECRET,
|
||||||
STRIPE_SECRET_KEY,
|
STRIPE_SECRET_KEY,
|
||||||
ZEN_MODELS1,
|
|
||||||
ZEN_MODELS2,
|
|
||||||
EMAILOCTOPUS_API_KEY,
|
EMAILOCTOPUS_API_KEY,
|
||||||
AWS_SES_ACCESS_KEY_ID,
|
AWS_SES_ACCESS_KEY_ID,
|
||||||
AWS_SES_SECRET_ACCESS_KEY,
|
AWS_SES_SECRET_ACCESS_KEY,
|
||||||
|
...ZEN_MODELS,
|
||||||
...($dev
|
...($dev
|
||||||
? [
|
? [
|
||||||
new sst.Secret("CLOUDFLARE_DEFAULT_ACCOUNT_ID", process.env.CLOUDFLARE_DEFAULT_ACCOUNT_ID!),
|
new sst.Secret("CLOUDFLARE_DEFAULT_ACCOUNT_ID", process.env.CLOUDFLARE_DEFAULT_ACCOUNT_ID!),
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { SECRET } from "./secret"
|
||||||
|
import { domain } from "./stage"
|
||||||
|
|
||||||
|
const storage = new sst.cloudflare.Bucket("EnterpriseStorage")
|
||||||
|
|
||||||
|
const enterprise = new sst.cloudflare.x.SolidStart("Enterprise", {
|
||||||
|
domain: "enterprise." + domain,
|
||||||
|
path: "packages/enterprise",
|
||||||
|
buildCommand: "bun run build:cloudflare",
|
||||||
|
environment: {
|
||||||
|
OPENCODE_STORAGE_ADAPTER: "r2",
|
||||||
|
OPENCODE_STORAGE_ACCOUNT_ID: sst.cloudflare.DEFAULT_ACCOUNT_ID,
|
||||||
|
OPENCODE_STORAGE_ACCESS_KEY_ID: SECRET.R2AccessKey.value,
|
||||||
|
OPENCODE_STORAGE_SECRET_ACCESS_KEY: SECRET.R2SecretKey.value,
|
||||||
|
OPENCODE_STORAGE_BUCKET: storage.name,
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export const SECRET = {
|
||||||
|
R2AccessKey: new sst.Secret("R2AccessKey", "unknown"),
|
||||||
|
R2SecretKey: new sst.Secret("R2SecretKey", "unknown"),
|
||||||
|
}
|
||||||
@@ -2,9 +2,8 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
APP=opencode
|
APP=opencode
|
||||||
|
|
||||||
|
MUTED='\033[0;2m'
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
ORANGE='\033[38;2;255;140;0m'
|
ORANGE='\033[38;2;255;140;0m'
|
||||||
NC='\033[0m' # No Color
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
@@ -12,39 +11,94 @@ requested_version=${VERSION:-}
|
|||||||
|
|
||||||
raw_os=$(uname -s)
|
raw_os=$(uname -s)
|
||||||
os=$(echo "$raw_os" | tr '[:upper:]' '[:lower:]')
|
os=$(echo "$raw_os" | tr '[:upper:]' '[:lower:]')
|
||||||
# Normalize various Unix-like identifiers
|
|
||||||
case "$raw_os" in
|
case "$raw_os" in
|
||||||
Darwin*) os="darwin" ;;
|
Darwin*) os="darwin" ;;
|
||||||
Linux*) os="linux" ;;
|
Linux*) os="linux" ;;
|
||||||
MINGW*|MSYS*|CYGWIN*) os="windows" ;;
|
MINGW*|MSYS*|CYGWIN*) os="windows" ;;
|
||||||
esac
|
esac
|
||||||
arch=$(uname -m)
|
|
||||||
|
|
||||||
|
arch=$(uname -m)
|
||||||
if [[ "$arch" == "aarch64" ]]; then
|
if [[ "$arch" == "aarch64" ]]; then
|
||||||
arch="arm64"
|
arch="arm64"
|
||||||
elif [[ "$arch" == "x86_64" ]]; then
|
fi
|
||||||
|
if [[ "$arch" == "x86_64" ]]; then
|
||||||
arch="x64"
|
arch="x64"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
filename="$APP-$os-$arch.zip"
|
if [ "$os" = "darwin" ] && [ "$arch" = "x64" ]; then
|
||||||
|
rosetta_flag=$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0)
|
||||||
|
if [ "$rosetta_flag" = "1" ]; then
|
||||||
|
arch="arm64"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
combo="$os-$arch"
|
||||||
case "$filename" in
|
case "$combo" in
|
||||||
*"-linux-"*)
|
linux-x64|linux-arm64|darwin-x64|darwin-arm64|windows-x64)
|
||||||
[[ "$arch" == "x64" || "$arch" == "arm64" ]] || exit 1
|
|
||||||
;;
|
;;
|
||||||
*"-darwin-"*)
|
*)
|
||||||
[[ "$arch" == "x64" || "$arch" == "arm64" ]] || exit 1
|
echo -e "${RED}Unsupported OS/Arch: $os/$arch${NC}"
|
||||||
;;
|
exit 1
|
||||||
*"-windows-"*)
|
|
||||||
[[ "$arch" == "x64" ]] || exit 1
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo -e "${RED}Unsupported OS/Arch: $os/$arch${NC}"
|
|
||||||
exit 1
|
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
archive_ext=".zip"
|
||||||
|
if [ "$os" = "linux" ]; then
|
||||||
|
archive_ext=".tar.gz"
|
||||||
|
fi
|
||||||
|
|
||||||
|
is_musl=false
|
||||||
|
if [ "$os" = "linux" ]; then
|
||||||
|
if [ -f /etc/alpine-release ]; then
|
||||||
|
is_musl=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v ldd >/dev/null 2>&1; then
|
||||||
|
if ldd --version 2>&1 | grep -qi musl; then
|
||||||
|
is_musl=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
needs_baseline=false
|
||||||
|
if [ "$arch" = "x64" ]; then
|
||||||
|
if [ "$os" = "linux" ]; then
|
||||||
|
if ! grep -qi avx2 /proc/cpuinfo 2>/dev/null; then
|
||||||
|
needs_baseline=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$os" = "darwin" ]; then
|
||||||
|
avx2=$(sysctl -n hw.optional.avx2_0 2>/dev/null || echo 0)
|
||||||
|
if [ "$avx2" != "1" ]; then
|
||||||
|
needs_baseline=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
target="$os-$arch"
|
||||||
|
if [ "$needs_baseline" = "true" ]; then
|
||||||
|
target="$target-baseline"
|
||||||
|
fi
|
||||||
|
if [ "$is_musl" = "true" ]; then
|
||||||
|
target="$target-musl"
|
||||||
|
fi
|
||||||
|
|
||||||
|
filename="$APP-$target$archive_ext"
|
||||||
|
|
||||||
|
|
||||||
|
if [ "$os" = "linux" ]; then
|
||||||
|
if ! command -v tar >/dev/null 2>&1; then
|
||||||
|
echo -e "${RED}Error: 'tar' is required but not installed.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if ! command -v unzip >/dev/null 2>&1; then
|
||||||
|
echo -e "${RED}Error: 'unzip' is required but not installed.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
INSTALL_DIR=$HOME/.opencode/bin
|
INSTALL_DIR=$HOME/.opencode/bin
|
||||||
mkdir -p "$INSTALL_DIR"
|
mkdir -p "$INSTALL_DIR"
|
||||||
|
|
||||||
@@ -67,8 +121,8 @@ print_message() {
|
|||||||
local color=""
|
local color=""
|
||||||
|
|
||||||
case $level in
|
case $level in
|
||||||
info) color="${GREEN}" ;;
|
info) color="${NC}" ;;
|
||||||
warning) color="${YELLOW}" ;;
|
warning) color="${NC}" ;;
|
||||||
error) color="${RED}" ;;
|
error) color="${RED}" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
@@ -86,19 +140,119 @@ check_version() {
|
|||||||
installed_version=$(echo $installed_version | awk '{print $2}')
|
installed_version=$(echo $installed_version | awk '{print $2}')
|
||||||
|
|
||||||
if [[ "$installed_version" != "$specific_version" ]]; then
|
if [[ "$installed_version" != "$specific_version" ]]; then
|
||||||
print_message info "Installed version: ${YELLOW}$installed_version."
|
print_message info "${MUTED}Installed version: ${NC}$installed_version."
|
||||||
else
|
else
|
||||||
print_message info "Version ${YELLOW}$specific_version${GREEN} already installed"
|
print_message info "${MUTED}Version ${NC}$specific_version${MUTED} already installed"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unbuffered_sed() {
|
||||||
|
if echo | sed -u -e "" >/dev/null 2>&1; then
|
||||||
|
sed -nu "$@"
|
||||||
|
elif echo | sed -l -e "" >/dev/null 2>&1; then
|
||||||
|
sed -nl "$@"
|
||||||
|
else
|
||||||
|
local pad="$(printf "\n%512s" "")"
|
||||||
|
sed -ne "s/$/\\${pad}/" "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
print_progress() {
|
||||||
|
local bytes="$1"
|
||||||
|
local length="$2"
|
||||||
|
[ "$length" -gt 0 ] || return 0
|
||||||
|
|
||||||
|
local width=50
|
||||||
|
local percent=$(( bytes * 100 / length ))
|
||||||
|
[ "$percent" -gt 100 ] && percent=100
|
||||||
|
local on=$(( percent * width / 100 ))
|
||||||
|
local off=$(( width - on ))
|
||||||
|
|
||||||
|
local filled=$(printf "%*s" "$on" "")
|
||||||
|
filled=${filled// /■}
|
||||||
|
local empty=$(printf "%*s" "$off" "")
|
||||||
|
empty=${empty// /・}
|
||||||
|
|
||||||
|
printf "\r${ORANGE}%s%s %3d%%${NC}" "$filled" "$empty" "$percent" >&4
|
||||||
|
}
|
||||||
|
|
||||||
|
download_with_progress() {
|
||||||
|
local url="$1"
|
||||||
|
local output="$2"
|
||||||
|
|
||||||
|
if [ -t 2 ]; then
|
||||||
|
exec 4>&2
|
||||||
|
else
|
||||||
|
exec 4>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
local tmp_dir=${TMPDIR:-/tmp}
|
||||||
|
local basename="${tmp_dir}/opencode_install_$$"
|
||||||
|
local tracefile="${basename}.trace"
|
||||||
|
|
||||||
|
rm -f "$tracefile"
|
||||||
|
mkfifo "$tracefile"
|
||||||
|
|
||||||
|
# Hide cursor
|
||||||
|
printf "\033[?25l" >&4
|
||||||
|
|
||||||
|
trap "trap - RETURN; rm -f \"$tracefile\"; printf '\033[?25h' >&4; exec 4>&-" RETURN
|
||||||
|
|
||||||
|
(
|
||||||
|
curl --trace-ascii "$tracefile" -s -L -o "$output" "$url"
|
||||||
|
) &
|
||||||
|
local curl_pid=$!
|
||||||
|
|
||||||
|
unbuffered_sed \
|
||||||
|
-e 'y/ACDEGHLNORTV/acdeghlnortv/' \
|
||||||
|
-e '/^0000: content-length:/p' \
|
||||||
|
-e '/^<= recv data/p' \
|
||||||
|
"$tracefile" | \
|
||||||
|
{
|
||||||
|
local length=0
|
||||||
|
local bytes=0
|
||||||
|
|
||||||
|
while IFS=" " read -r -a line; do
|
||||||
|
[ "${#line[@]}" -lt 2 ] && continue
|
||||||
|
local tag="${line[0]} ${line[1]}"
|
||||||
|
|
||||||
|
if [ "$tag" = "0000: content-length:" ]; then
|
||||||
|
length="${line[2]}"
|
||||||
|
length=$(echo "$length" | tr -d '\r')
|
||||||
|
bytes=0
|
||||||
|
elif [ "$tag" = "<= recv" ]; then
|
||||||
|
local size="${line[3]}"
|
||||||
|
bytes=$(( bytes + size ))
|
||||||
|
if [ "$length" -gt 0 ]; then
|
||||||
|
print_progress "$bytes" "$length"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
wait $curl_pid
|
||||||
|
local ret=$?
|
||||||
|
echo "" >&4
|
||||||
|
return $ret
|
||||||
|
}
|
||||||
|
|
||||||
download_and_install() {
|
download_and_install() {
|
||||||
print_message info "Downloading ${ORANGE}opencode ${GREEN}version: ${YELLOW}$specific_version ${GREEN}..."
|
print_message info "\n${MUTED}Installing ${NC}opencode ${MUTED}version: ${NC}$specific_version"
|
||||||
mkdir -p opencodetmp && cd opencodetmp
|
mkdir -p opencodetmp && cd opencodetmp
|
||||||
curl -# -L -o "$filename" "$url"
|
|
||||||
unzip -q "$filename"
|
if [[ "$os" == "windows" ]] || ! download_with_progress "$url" "$filename"; then
|
||||||
|
# Fallback to standard curl on Windows or if custom progress fails
|
||||||
|
curl -# -L -o "$filename" "$url"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$os" = "linux" ]; then
|
||||||
|
tar -xzf "$filename"
|
||||||
|
else
|
||||||
|
unzip -q "$filename"
|
||||||
|
fi
|
||||||
|
|
||||||
mv opencode "$INSTALL_DIR"
|
mv opencode "$INSTALL_DIR"
|
||||||
chmod 755 "${INSTALL_DIR}/opencode"
|
chmod 755 "${INSTALL_DIR}/opencode"
|
||||||
cd .. && rm -rf opencodetmp
|
cd .. && rm -rf opencodetmp
|
||||||
@@ -117,7 +271,7 @@ add_to_path() {
|
|||||||
elif [[ -w $config_file ]]; then
|
elif [[ -w $config_file ]]; then
|
||||||
echo -e "\n# opencode" >> "$config_file"
|
echo -e "\n# opencode" >> "$config_file"
|
||||||
echo "$command" >> "$config_file"
|
echo "$command" >> "$config_file"
|
||||||
print_message info "Successfully added ${ORANGE}opencode ${GREEN}to \$PATH in $config_file"
|
print_message info "${MUTED}Successfully added ${NC}opencode ${MUTED}to \$PATH in ${NC}$config_file"
|
||||||
else
|
else
|
||||||
print_message warning "Manually add the directory to $config_file (or similar):"
|
print_message warning "Manually add the directory to $config_file (or similar):"
|
||||||
print_message info " $command"
|
print_message info " $command"
|
||||||
@@ -191,3 +345,20 @@ if [ -n "${GITHUB_ACTIONS-}" ] && [ "${GITHUB_ACTIONS}" == "true" ]; then
|
|||||||
echo "$INSTALL_DIR" >> $GITHUB_PATH
|
echo "$INSTALL_DIR" >> $GITHUB_PATH
|
||||||
print_message info "Added $INSTALL_DIR to \$GITHUB_PATH"
|
print_message info "Added $INSTALL_DIR to \$GITHUB_PATH"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo -e ""
|
||||||
|
echo -e "${MUTED} ${NC} ▄ "
|
||||||
|
echo -e "${MUTED}█▀▀█ █▀▀█ █▀▀█ █▀▀▄ ${NC}█▀▀▀ █▀▀█ █▀▀█ █▀▀█"
|
||||||
|
echo -e "${MUTED}█░░█ █░░█ █▀▀▀ █░░█ ${NC}█░░░ █░░█ █░░█ █▀▀▀"
|
||||||
|
echo -e "${MUTED}▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ${NC}▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀"
|
||||||
|
echo -e ""
|
||||||
|
echo -e ""
|
||||||
|
echo -e "${MUTED}To get started, navigate to a project and run:${NC}"
|
||||||
|
echo -e "opencode ${MUTED}Use free models${NC}"
|
||||||
|
echo -e "opencode auth login ${MUTED}Add paid provider API keys${NC}"
|
||||||
|
echo -e "opencode help ${MUTED}List commands and options${NC}"
|
||||||
|
echo -e ""
|
||||||
|
echo -e "${MUTED}For more information visit ${NC}https://opencode.ai/docs"
|
||||||
|
echo -e ""
|
||||||
|
echo -e ""
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
import solidPlugin from "./node_modules/@opentui/solid/scripts/solid-plugin"
|
||||||
|
import path from "path"
|
||||||
|
import fs from "fs"
|
||||||
|
|
||||||
|
const dir = process.cwd()
|
||||||
|
const parser = fs.realpathSync(path.join(dir, "node_modules/@opentui/core/parser.worker.js"))
|
||||||
|
const worker = "./src/cli/cmd/tui/worker.ts"
|
||||||
|
const version = process.env.OPENCODE_VERSION ?? "local"
|
||||||
|
const channel = process.env.OPENCODE_CHANNEL ?? "local"
|
||||||
|
|
||||||
|
fs.rmSync(path.join(dir, "dist"), { recursive: true, force: true })
|
||||||
|
|
||||||
|
const result = await Bun.build({
|
||||||
|
entrypoints: ["./src/index.ts", worker, parser],
|
||||||
|
outdir: "./dist",
|
||||||
|
target: "bun",
|
||||||
|
sourcemap: "none",
|
||||||
|
tsconfig: "./tsconfig.json",
|
||||||
|
plugins: [solidPlugin],
|
||||||
|
external: ["@opentui/core"],
|
||||||
|
define: {
|
||||||
|
OPENCODE_VERSION: `'${version}'`,
|
||||||
|
OPENCODE_CHANNEL: `'${channel}'`,
|
||||||
|
// Leave undefined so runtime picks bundled/dist worker or fallback in code.
|
||||||
|
OPENCODE_WORKER_PATH: "undefined",
|
||||||
|
OTUI_TREE_SITTER_WORKER_PATH: 'new URL("./cli/cmd/tui/parser.worker.js", import.meta.url).href',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
console.error("bundle failed")
|
||||||
|
for (const log of result.logs) console.error(log)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const parserOut = path.join(dir, "dist/src/cli/cmd/tui/parser.worker.js")
|
||||||
|
fs.mkdirSync(path.dirname(parserOut), { recursive: true })
|
||||||
|
await Bun.write(parserOut, Bun.file(parser))
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"nodeModules": "sha256-4OrnnZy44edgShLMUrYFsO+07gAnL//pGKO/ehWu5P4="
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
{ hash, lib, stdenvNoCC, bun, cacert, curl }:
|
||||||
|
args:
|
||||||
|
stdenvNoCC.mkDerivation {
|
||||||
|
pname = "opencode-node_modules";
|
||||||
|
version = args.version;
|
||||||
|
src = args.src;
|
||||||
|
|
||||||
|
impureEnvVars =
|
||||||
|
lib.fetchers.proxyImpureEnvVars
|
||||||
|
++ [
|
||||||
|
"GIT_PROXY_COMMAND"
|
||||||
|
"SOCKS_SERVER"
|
||||||
|
];
|
||||||
|
|
||||||
|
nativeBuildInputs = [ bun cacert curl ];
|
||||||
|
|
||||||
|
dontConfigure = true;
|
||||||
|
|
||||||
|
buildPhase = ''
|
||||||
|
runHook preBuild
|
||||||
|
export HOME=$(mktemp -d)
|
||||||
|
export BUN_INSTALL_CACHE_DIR=$(mktemp -d)
|
||||||
|
bun install \
|
||||||
|
--cpu="*" \
|
||||||
|
--os="*" \
|
||||||
|
--frozen-lockfile \
|
||||||
|
--ignore-scripts \
|
||||||
|
--no-progress \
|
||||||
|
--linker=isolated
|
||||||
|
bun --bun ${args.canonicalizeScript}
|
||||||
|
bun --bun ${args.normalizeBinsScript}
|
||||||
|
runHook postBuild
|
||||||
|
'';
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
runHook preInstall
|
||||||
|
mkdir -p $out
|
||||||
|
while IFS= read -r dir; do
|
||||||
|
rel="''${dir#./}"
|
||||||
|
dest="$out/$rel"
|
||||||
|
mkdir -p "$(dirname "$dest")"
|
||||||
|
cp -R "$dir" "$dest"
|
||||||
|
done < <(find . -type d -name node_modules -prune | sort)
|
||||||
|
runHook postInstall
|
||||||
|
'';
|
||||||
|
|
||||||
|
dontFixup = true;
|
||||||
|
|
||||||
|
outputHashAlgo = "sha256";
|
||||||
|
outputHashMode = "recursive";
|
||||||
|
outputHash = hash;
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
{ lib, stdenvNoCC, bun, fzf, ripgrep, makeBinaryWrapper }:
|
||||||
|
args:
|
||||||
|
let
|
||||||
|
scripts = args.scripts;
|
||||||
|
mkModules =
|
||||||
|
attrs:
|
||||||
|
args.mkNodeModules (
|
||||||
|
attrs
|
||||||
|
// {
|
||||||
|
canonicalizeScript = scripts + "/canonicalize-node-modules.ts";
|
||||||
|
normalizeBinsScript = scripts + "/normalize-bun-binaries.ts";
|
||||||
|
}
|
||||||
|
);
|
||||||
|
in
|
||||||
|
stdenvNoCC.mkDerivation (finalAttrs: {
|
||||||
|
pname = "opencode";
|
||||||
|
version = args.version;
|
||||||
|
|
||||||
|
src = args.src;
|
||||||
|
|
||||||
|
node_modules = mkModules {
|
||||||
|
version = finalAttrs.version;
|
||||||
|
src = finalAttrs.src;
|
||||||
|
};
|
||||||
|
|
||||||
|
nativeBuildInputs = [
|
||||||
|
bun
|
||||||
|
makeBinaryWrapper
|
||||||
|
];
|
||||||
|
|
||||||
|
env.MODELS_DEV_API_JSON = args.modelsDev;
|
||||||
|
env.OPENCODE_VERSION = args.version;
|
||||||
|
env.OPENCODE_CHANNEL = "stable";
|
||||||
|
dontConfigure = true;
|
||||||
|
|
||||||
|
buildPhase = ''
|
||||||
|
runHook preBuild
|
||||||
|
|
||||||
|
cp -r ${finalAttrs.node_modules}/node_modules .
|
||||||
|
cp -r ${finalAttrs.node_modules}/packages .
|
||||||
|
|
||||||
|
(
|
||||||
|
cd packages/opencode
|
||||||
|
|
||||||
|
chmod -R u+w ./node_modules
|
||||||
|
mkdir -p ./node_modules/@opencode-ai
|
||||||
|
rm -f ./node_modules/@opencode-ai/{script,sdk,plugin}
|
||||||
|
ln -s $(pwd)/../../packages/script ./node_modules/@opencode-ai/script
|
||||||
|
ln -s $(pwd)/../../packages/sdk/js ./node_modules/@opencode-ai/sdk
|
||||||
|
ln -s $(pwd)/../../packages/plugin ./node_modules/@opencode-ai/plugin
|
||||||
|
|
||||||
|
cp ${./bundle.ts} ./bundle.ts
|
||||||
|
chmod +x ./bundle.ts
|
||||||
|
bun run ./bundle.ts
|
||||||
|
)
|
||||||
|
|
||||||
|
runHook postBuild
|
||||||
|
'';
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
runHook preInstall
|
||||||
|
|
||||||
|
cd packages/opencode
|
||||||
|
if [ ! -d dist ]; then
|
||||||
|
echo "ERROR: dist directory missing after bundle step"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p $out/lib/opencode
|
||||||
|
cp -r dist $out/lib/opencode/
|
||||||
|
chmod -R u+w $out/lib/opencode/dist
|
||||||
|
|
||||||
|
# Select bundled worker assets deterministically (sorted find output)
|
||||||
|
worker_file=$(find "$out/lib/opencode/dist" -type f \( -path '*/tui/worker.*' -o -name 'worker.*' \) | sort | head -n1)
|
||||||
|
parser_worker_file=$(find "$out/lib/opencode/dist" -type f -name 'parser.worker.*' | sort | head -n1)
|
||||||
|
if [ -z "$worker_file" ]; then
|
||||||
|
echo "ERROR: bundled worker not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
main_wasm=$(printf '%s\n' "$out"/lib/opencode/dist/tree-sitter-*.wasm | sort | head -n1)
|
||||||
|
wasm_list=$(find "$out/lib/opencode/dist" -maxdepth 1 -name 'tree-sitter-*.wasm' -print)
|
||||||
|
for patch_file in "$worker_file" "$parser_worker_file"; do
|
||||||
|
[ -z "$patch_file" ] && continue
|
||||||
|
[ ! -f "$patch_file" ] && continue
|
||||||
|
if [ -n "$wasm_list" ] && grep -q 'tree-sitter' "$patch_file"; then
|
||||||
|
# Rewrite wasm references to absolute store paths to avoid runtime resolve failures.
|
||||||
|
bun --bun ${scripts + "/patch-wasm.ts"} "$patch_file" "$main_wasm" $wasm_list
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
mkdir -p $out/lib/opencode/node_modules
|
||||||
|
cp -r ../../node_modules/.bun $out/lib/opencode/node_modules/
|
||||||
|
mkdir -p $out/lib/opencode/node_modules/@opentui
|
||||||
|
|
||||||
|
mkdir -p $out/bin
|
||||||
|
makeWrapper ${bun}/bin/bun $out/bin/opencode \
|
||||||
|
--add-flags "run" \
|
||||||
|
--add-flags "$out/lib/opencode/dist/src/index.js" \
|
||||||
|
--prefix PATH : ${lib.makeBinPath [ fzf ripgrep ]} \
|
||||||
|
--argv0 opencode
|
||||||
|
|
||||||
|
runHook postInstall
|
||||||
|
'';
|
||||||
|
|
||||||
|
postInstall = ''
|
||||||
|
for pkg in $out/lib/opencode/node_modules/.bun/@opentui+core-* $out/lib/opencode/node_modules/.bun/@opentui+solid-* $out/lib/opencode/node_modules/.bun/@opentui+core@* $out/lib/opencode/node_modules/.bun/@opentui+solid@*; do
|
||||||
|
if [ -d "$pkg" ]; then
|
||||||
|
pkgName=$(basename "$pkg" | sed 's/@opentui+\([^@]*\)@.*/\1/')
|
||||||
|
ln -sf ../.bun/$(basename "$pkg")/node_modules/@opentui/$pkgName \
|
||||||
|
$out/lib/opencode/node_modules/@opentui/$pkgName
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
'';
|
||||||
|
|
||||||
|
dontFixup = true;
|
||||||
|
|
||||||
|
meta = {
|
||||||
|
description = "AI coding agent built for the terminal";
|
||||||
|
longDescription = ''
|
||||||
|
OpenCode is a terminal-based agent that can build anything.
|
||||||
|
It combines a TypeScript/JavaScript core with a Go-based TUI
|
||||||
|
to provide an interactive AI coding experience.
|
||||||
|
'';
|
||||||
|
homepage = "https://github.com/sst/opencode";
|
||||||
|
license = lib.licenses.mit;
|
||||||
|
platforms = [
|
||||||
|
"aarch64-linux"
|
||||||
|
"x86_64-linux"
|
||||||
|
"aarch64-darwin"
|
||||||
|
"x86_64-darwin"
|
||||||
|
];
|
||||||
|
mainProgram = "opencode";
|
||||||
|
};
|
||||||
|
})
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import solidPlugin from "./packages/opencode/node_modules/@opentui/solid/scripts/solid-plugin"
|
||||||
|
import path from "path"
|
||||||
|
import fs from "fs"
|
||||||
|
|
||||||
|
const version = "@VERSION@"
|
||||||
|
const pkg = path.join(process.cwd(), "packages/opencode")
|
||||||
|
const parser = fs.realpathSync(path.join(pkg, "./node_modules/@opentui/core/parser.worker.js"))
|
||||||
|
const worker = "./src/cli/cmd/tui/worker.ts"
|
||||||
|
const target = process.env["BUN_COMPILE_TARGET"]
|
||||||
|
|
||||||
|
if (!target) {
|
||||||
|
throw new Error("BUN_COMPILE_TARGET not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
process.chdir(pkg)
|
||||||
|
|
||||||
|
const manifestName = "opencode-assets.manifest"
|
||||||
|
const manifestPath = path.join(pkg, manifestName)
|
||||||
|
|
||||||
|
const readTrackedAssets = () => {
|
||||||
|
if (!fs.existsSync(manifestPath)) return []
|
||||||
|
return fs
|
||||||
|
.readFileSync(manifestPath, "utf8")
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter((line) => line.length > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeTrackedAssets = () => {
|
||||||
|
for (const file of readTrackedAssets()) {
|
||||||
|
const filePath = path.join(pkg, file)
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
fs.rmSync(filePath, { force: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const assets = new Set<string>()
|
||||||
|
|
||||||
|
const addAsset = async (p: string) => {
|
||||||
|
const file = path.basename(p)
|
||||||
|
const dest = path.join(pkg, file)
|
||||||
|
await Bun.write(dest, Bun.file(p))
|
||||||
|
assets.add(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTrackedAssets()
|
||||||
|
|
||||||
|
const result = await Bun.build({
|
||||||
|
conditions: ["browser"],
|
||||||
|
tsconfig: "./tsconfig.json",
|
||||||
|
plugins: [solidPlugin],
|
||||||
|
sourcemap: "external",
|
||||||
|
entrypoints: ["./src/index.ts", parser, worker],
|
||||||
|
define: {
|
||||||
|
OPENCODE_VERSION: `'@VERSION@'`,
|
||||||
|
OTUI_TREE_SITTER_WORKER_PATH: "/$bunfs/root/" + path.relative(pkg, parser).replace(/\\/g, "/"),
|
||||||
|
OPENCODE_CHANNEL: "'latest'",
|
||||||
|
},
|
||||||
|
compile: {
|
||||||
|
target,
|
||||||
|
outfile: "opencode",
|
||||||
|
execArgv: ["--user-agent=opencode/" + version, '--env-file=""', "--"],
|
||||||
|
windows: {},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
console.error("Build failed!")
|
||||||
|
for (const log of result.logs) {
|
||||||
|
console.error(log)
|
||||||
|
}
|
||||||
|
throw new Error("Compilation failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
const assetOutputs = result.outputs?.filter((x) => x.kind === "asset") ?? []
|
||||||
|
for (const x of assetOutputs) {
|
||||||
|
await addAsset(x.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
const bundle = await Bun.build({
|
||||||
|
entrypoints: [worker],
|
||||||
|
tsconfig: "./tsconfig.json",
|
||||||
|
plugins: [solidPlugin],
|
||||||
|
target: "bun",
|
||||||
|
outdir: "./.opencode-worker",
|
||||||
|
sourcemap: "none",
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!bundle.success) {
|
||||||
|
console.error("Worker build failed!")
|
||||||
|
for (const log of bundle.logs) {
|
||||||
|
console.error(log)
|
||||||
|
}
|
||||||
|
throw new Error("Worker compilation failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
const workerAssets = bundle.outputs?.filter((x) => x.kind === "asset") ?? []
|
||||||
|
for (const x of workerAssets) {
|
||||||
|
await addAsset(x.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = bundle.outputs.find((x) => x.kind === "entry-point")
|
||||||
|
if (!output) {
|
||||||
|
throw new Error("Worker build produced no entry-point output")
|
||||||
|
}
|
||||||
|
|
||||||
|
const dest = path.join(pkg, "opencode-worker.js")
|
||||||
|
await Bun.write(dest, Bun.file(output.path))
|
||||||
|
fs.rmSync(path.dirname(output.path), { recursive: true, force: true })
|
||||||
|
|
||||||
|
const list = Array.from(assets)
|
||||||
|
await Bun.write(manifestPath, list.length > 0 ? list.join("\n") + "\n" : "")
|
||||||
|
|
||||||
|
console.log("Build successful!")
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import { lstat, mkdir, readdir, rm, symlink } from "fs/promises"
|
||||||
|
import { join, relative } from "path"
|
||||||
|
|
||||||
|
type SemverLike = {
|
||||||
|
valid: (value: string) => string | null
|
||||||
|
rcompare: (left: string, right: string) => number
|
||||||
|
}
|
||||||
|
|
||||||
|
type Entry = {
|
||||||
|
dir: string
|
||||||
|
version: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = process.cwd()
|
||||||
|
const bunRoot = join(root, "node_modules/.bun")
|
||||||
|
const linkRoot = join(bunRoot, "node_modules")
|
||||||
|
const directories = (await readdir(bunRoot)).sort()
|
||||||
|
const versions = new Map<string, Entry[]>()
|
||||||
|
|
||||||
|
for (const entry of directories) {
|
||||||
|
const full = join(bunRoot, entry)
|
||||||
|
const info = await lstat(full)
|
||||||
|
if (!info.isDirectory()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const parsed = parseEntry(entry)
|
||||||
|
if (!parsed) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const list = versions.get(parsed.name) ?? []
|
||||||
|
list.push({ dir: full, version: parsed.version, label: entry })
|
||||||
|
versions.set(parsed.name, list)
|
||||||
|
}
|
||||||
|
|
||||||
|
const semverModule = (await import(join(bunRoot, "node_modules/semver"))) as
|
||||||
|
| SemverLike
|
||||||
|
| {
|
||||||
|
default: SemverLike
|
||||||
|
}
|
||||||
|
const semver = "default" in semverModule ? semverModule.default : semverModule
|
||||||
|
const selections = new Map<string, Entry>()
|
||||||
|
|
||||||
|
for (const [slug, list] of versions) {
|
||||||
|
list.sort((a, b) => {
|
||||||
|
const left = semver.valid(a.version)
|
||||||
|
const right = semver.valid(b.version)
|
||||||
|
if (left && right) {
|
||||||
|
const delta = semver.rcompare(left, right)
|
||||||
|
if (delta !== 0) {
|
||||||
|
return delta
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (left && !right) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if (!left && right) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return b.version.localeCompare(a.version)
|
||||||
|
})
|
||||||
|
selections.set(slug, list[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
await rm(linkRoot, { recursive: true, force: true })
|
||||||
|
await mkdir(linkRoot, { recursive: true })
|
||||||
|
|
||||||
|
const rewrites: string[] = []
|
||||||
|
|
||||||
|
for (const [slug, entry] of Array.from(selections.entries()).sort((a, b) => a[0].localeCompare(b[0]))) {
|
||||||
|
const parts = slug.split("/")
|
||||||
|
const leaf = parts.pop()
|
||||||
|
if (!leaf) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const parent = join(linkRoot, ...parts)
|
||||||
|
await mkdir(parent, { recursive: true })
|
||||||
|
const linkPath = join(parent, leaf)
|
||||||
|
const desired = join(entry.dir, "node_modules", slug)
|
||||||
|
const exists = await lstat(desired)
|
||||||
|
.then((info) => info.isDirectory())
|
||||||
|
.catch(() => false)
|
||||||
|
if (!exists) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const relativeTarget = relative(parent, desired)
|
||||||
|
const resolved = relativeTarget.length === 0 ? "." : relativeTarget
|
||||||
|
await rm(linkPath, { recursive: true, force: true })
|
||||||
|
await symlink(resolved, linkPath)
|
||||||
|
rewrites.push(slug + " -> " + resolved)
|
||||||
|
}
|
||||||
|
|
||||||
|
rewrites.sort()
|
||||||
|
console.log("[canonicalize-node-modules] rebuilt", rewrites.length, "links")
|
||||||
|
for (const line of rewrites.slice(0, 20)) {
|
||||||
|
console.log(" ", line)
|
||||||
|
}
|
||||||
|
if (rewrites.length > 20) {
|
||||||
|
console.log(" ...")
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseEntry(label: string) {
|
||||||
|
const marker = label.startsWith("@") ? label.indexOf("@", 1) : label.indexOf("@")
|
||||||
|
if (marker <= 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const name = label.slice(0, marker).replace(/\+/g, "/")
|
||||||
|
const version = label.slice(marker + 1)
|
||||||
|
if (!name || !version) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return { name, version }
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import { lstat, mkdir, readdir, rm, symlink } from "fs/promises"
|
||||||
|
import { join, relative } from "path"
|
||||||
|
|
||||||
|
type PackageManifest = {
|
||||||
|
name?: string
|
||||||
|
bin?: string | Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = process.cwd()
|
||||||
|
const bunRoot = join(root, "node_modules/.bun")
|
||||||
|
const bunEntries = (await safeReadDir(bunRoot)).sort()
|
||||||
|
let rewritten = 0
|
||||||
|
|
||||||
|
for (const entry of bunEntries) {
|
||||||
|
const modulesRoot = join(bunRoot, entry, "node_modules")
|
||||||
|
if (!(await exists(modulesRoot))) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const binRoot = join(modulesRoot, ".bin")
|
||||||
|
await rm(binRoot, { recursive: true, force: true })
|
||||||
|
await mkdir(binRoot, { recursive: true })
|
||||||
|
|
||||||
|
const packageDirs = await collectPackages(modulesRoot)
|
||||||
|
for (const packageDir of packageDirs) {
|
||||||
|
const manifest = await readManifest(packageDir)
|
||||||
|
if (!manifest) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const binField = manifest.bin
|
||||||
|
if (!binField) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const seen = new Set<string>()
|
||||||
|
if (typeof binField === "string") {
|
||||||
|
const fallback = manifest.name ?? packageDir.split("/").pop()
|
||||||
|
if (fallback) {
|
||||||
|
await linkBinary(binRoot, fallback, packageDir, binField, seen)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const entries = Object.entries(binField).sort((a, b) => a[0].localeCompare(b[0]))
|
||||||
|
for (const [name, target] of entries) {
|
||||||
|
await linkBinary(binRoot, name, packageDir, target, seen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[normalize-bun-binaries] rewrote ${rewritten} links`)
|
||||||
|
|
||||||
|
async function collectPackages(modulesRoot: string) {
|
||||||
|
const found: string[] = []
|
||||||
|
const topLevel = (await safeReadDir(modulesRoot)).sort()
|
||||||
|
for (const name of topLevel) {
|
||||||
|
if (name === ".bin" || name === ".bun") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const full = join(modulesRoot, name)
|
||||||
|
if (!(await isDirectory(full))) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (name.startsWith("@")) {
|
||||||
|
const scoped = (await safeReadDir(full)).sort()
|
||||||
|
for (const child of scoped) {
|
||||||
|
const scopedDir = join(full, child)
|
||||||
|
if (await isDirectory(scopedDir)) {
|
||||||
|
found.push(scopedDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
found.push(full)
|
||||||
|
}
|
||||||
|
return found.sort()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readManifest(dir: string) {
|
||||||
|
const file = Bun.file(join(dir, "package.json"))
|
||||||
|
if (!(await file.exists())) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const data = (await file.json()) as PackageManifest
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
async function linkBinary(binRoot: string, name: string, packageDir: string, target: string, seen: Set<string>) {
|
||||||
|
if (!name || !target) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const normalizedName = normalizeBinName(name)
|
||||||
|
if (seen.has(normalizedName)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const resolved = join(packageDir, target)
|
||||||
|
const script = Bun.file(resolved)
|
||||||
|
if (!(await script.exists())) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seen.add(normalizedName)
|
||||||
|
const destination = join(binRoot, normalizedName)
|
||||||
|
const relativeTarget = relative(binRoot, resolved) || "."
|
||||||
|
await rm(destination, { force: true })
|
||||||
|
await symlink(relativeTarget, destination)
|
||||||
|
rewritten++
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exists(path: string) {
|
||||||
|
try {
|
||||||
|
await lstat(path)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isDirectory(path: string) {
|
||||||
|
try {
|
||||||
|
const info = await lstat(path)
|
||||||
|
return info.isDirectory()
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function safeReadDir(path: string) {
|
||||||
|
try {
|
||||||
|
return await readdir(path)
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeBinName(name: string) {
|
||||||
|
const slash = name.lastIndexOf("/")
|
||||||
|
if (slash >= 0) {
|
||||||
|
return name.slice(slash + 1)
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
import fs from "fs"
|
||||||
|
import path from "path"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rewrite tree-sitter wasm references inside a JS file to absolute paths.
|
||||||
|
* argv: [node, script, file, mainWasm, ...wasmPaths]
|
||||||
|
*/
|
||||||
|
const [, , file, mainWasm, ...wasmPaths] = process.argv
|
||||||
|
|
||||||
|
if (!file || !mainWasm) {
|
||||||
|
console.error("usage: patch-wasm <file> <mainWasm> [wasmPaths...]")
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = fs.readFileSync(file, "utf8")
|
||||||
|
const byName = new Map<string, string>()
|
||||||
|
|
||||||
|
for (const wasm of wasmPaths) {
|
||||||
|
const name = path.basename(wasm)
|
||||||
|
byName.set(name, wasm)
|
||||||
|
}
|
||||||
|
|
||||||
|
let next = content
|
||||||
|
|
||||||
|
for (const [name, wasmPath] of byName) {
|
||||||
|
next = next.replaceAll(name, wasmPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
next = next.replaceAll("tree-sitter.wasm", mainWasm).replaceAll("web-tree-sitter/tree-sitter.wasm", mainWasm)
|
||||||
|
|
||||||
|
// Collapse any relative prefixes before absolute store paths (e.g., "../../../..//nix/store/...")
|
||||||
|
next = next.replace(/(\.\/)+/g, "./")
|
||||||
|
next = next.replace(/(\.\.\/)+\/?(\/nix\/store[^"']+)/g, "/$2")
|
||||||
|
next = next.replace(/(["'])\/{2,}(\/nix\/store[^"']+)(["'])/g, "$1/$2$3")
|
||||||
|
next = next.replace(/(["'])\/\/(nix\/store[^"']+)(["'])/g, "$1/$2$3")
|
||||||
|
|
||||||
|
if (next !== content) fs.writeFileSync(file, next)
|
||||||
Executable
+112
@@ -0,0 +1,112 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DUMMY="sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
|
||||||
|
SYSTEM=${SYSTEM:-x86_64-linux}
|
||||||
|
DEFAULT_HASH_FILE=${MODULES_HASH_FILE:-nix/hashes.json}
|
||||||
|
HASH_FILE=${HASH_FILE:-$DEFAULT_HASH_FILE}
|
||||||
|
|
||||||
|
if [ ! -f "$HASH_FILE" ]; then
|
||||||
|
cat >"$HASH_FILE" <<EOF
|
||||||
|
{
|
||||||
|
"nodeModules": "$DUMMY"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||||
|
if ! git ls-files --error-unmatch "$HASH_FILE" >/dev/null 2>&1; then
|
||||||
|
git add -N "$HASH_FILE" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
export DUMMY
|
||||||
|
export NIX_KEEP_OUTPUTS=1
|
||||||
|
export NIX_KEEP_DERIVATIONS=1
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -f "${JSON_OUTPUT:-}" "${BUILD_LOG:-}" "${TMP_EXPR:-}"
|
||||||
|
}
|
||||||
|
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
write_node_modules_hash() {
|
||||||
|
local value="$1"
|
||||||
|
local temp
|
||||||
|
temp=$(mktemp)
|
||||||
|
jq --arg value "$value" '.nodeModules = $value' "$HASH_FILE" >"$temp"
|
||||||
|
mv "$temp" "$HASH_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
TARGET="packages.${SYSTEM}.default"
|
||||||
|
MODULES_ATTR=".#packages.${SYSTEM}.default.node_modules"
|
||||||
|
CORRECT_HASH=""
|
||||||
|
|
||||||
|
DRV_PATH="$(nix eval --raw "${MODULES_ATTR}.drvPath")"
|
||||||
|
|
||||||
|
echo "Setting dummy node_modules outputHash for ${SYSTEM}..."
|
||||||
|
write_node_modules_hash "$DUMMY"
|
||||||
|
|
||||||
|
BUILD_LOG=$(mktemp)
|
||||||
|
JSON_OUTPUT=$(mktemp)
|
||||||
|
|
||||||
|
echo "Building node_modules for ${SYSTEM} to discover correct outputHash..."
|
||||||
|
echo "Attempting to realize derivation: ${DRV_PATH}"
|
||||||
|
REALISE_OUT=$(nix-store --realise "$DRV_PATH" --keep-failed 2>&1 | tee "$BUILD_LOG" || true)
|
||||||
|
|
||||||
|
BUILD_PATH=$(echo "$REALISE_OUT" | grep "^/nix/store/" | head -n1 || true)
|
||||||
|
if [ -n "$BUILD_PATH" ] && [ -d "$BUILD_PATH" ]; then
|
||||||
|
echo "Realized node_modules output: $BUILD_PATH"
|
||||||
|
CORRECT_HASH=$(nix hash path --sri "$BUILD_PATH" 2>/dev/null || true)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$CORRECT_HASH" ]; then
|
||||||
|
CORRECT_HASH="$(grep -E 'got:\s+sha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | awk '{print $2}' | head -n1 || true)"
|
||||||
|
|
||||||
|
if [ -z "$CORRECT_HASH" ]; then
|
||||||
|
CORRECT_HASH="$(grep -A2 'hash mismatch' "$BUILD_LOG" | grep 'got:' | awk '{print $2}' | sed 's/sha256:/sha256-/' || true)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$CORRECT_HASH" ]; then
|
||||||
|
echo "Searching for kept failed build directory..."
|
||||||
|
KEPT_DIR=$(grep -oE "build directory.*'[^']+'" "$BUILD_LOG" | grep -oE "'/[^']+'" | tr -d "'" | head -n1)
|
||||||
|
|
||||||
|
if [ -z "$KEPT_DIR" ]; then
|
||||||
|
KEPT_DIR=$(grep -oE '/nix/var/nix/builds/[^ ]+' "$BUILD_LOG" | head -n1)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$KEPT_DIR" ] && [ -d "$KEPT_DIR" ]; then
|
||||||
|
echo "Found kept build directory: $KEPT_DIR"
|
||||||
|
if [ -d "$KEPT_DIR/build" ]; then
|
||||||
|
HASH_PATH="$KEPT_DIR/build"
|
||||||
|
else
|
||||||
|
HASH_PATH="$KEPT_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Attempting to hash: $HASH_PATH"
|
||||||
|
ls -la "$HASH_PATH" || true
|
||||||
|
|
||||||
|
if [ -d "$HASH_PATH/node_modules" ]; then
|
||||||
|
CORRECT_HASH=$(nix hash path --sri "$HASH_PATH" 2>/dev/null || true)
|
||||||
|
echo "Computed hash from kept build: $CORRECT_HASH"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$CORRECT_HASH" ]; then
|
||||||
|
echo "Failed to determine correct node_modules hash for ${SYSTEM}."
|
||||||
|
echo "Build log:"
|
||||||
|
cat "$BUILD_LOG"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
write_node_modules_hash "$CORRECT_HASH"
|
||||||
|
|
||||||
|
jq -e --arg hash "$CORRECT_HASH" '.nodeModules == $hash' "$HASH_FILE" >/dev/null
|
||||||
|
|
||||||
|
echo "node_modules hash updated for ${SYSTEM}: $CORRECT_HASH"
|
||||||
|
|
||||||
|
rm -f "$BUILD_LOG"
|
||||||
|
unset BUILD_LOG
|
||||||
+16
-12
@@ -4,12 +4,13 @@
|
|||||||
"description": "AI-powered development tool",
|
"description": "AI-powered development tool",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "bun@1.3.2",
|
"packageManager": "bun@1.3.3",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
||||||
"typecheck": "bun turbo typecheck",
|
"typecheck": "bun turbo typecheck",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"random": "echo 'Random script'"
|
"random": "echo 'Random script'",
|
||||||
|
"hello": "echo 'Hello World!'"
|
||||||
},
|
},
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"packages": [
|
"packages": [
|
||||||
@@ -19,33 +20,37 @@
|
|||||||
"packages/slack"
|
"packages/slack"
|
||||||
],
|
],
|
||||||
"catalog": {
|
"catalog": {
|
||||||
"@types/bun": "1.3.0",
|
"@types/bun": "1.3.3",
|
||||||
"@hono/zod-validator": "0.4.2",
|
"@hono/zod-validator": "0.4.2",
|
||||||
"ulid": "3.0.1",
|
"ulid": "3.0.1",
|
||||||
"@kobalte/core": "0.13.11",
|
"@kobalte/core": "0.13.11",
|
||||||
|
"@types/luxon": "3.7.1",
|
||||||
"@types/node": "22.13.9",
|
"@types/node": "22.13.9",
|
||||||
"@tsconfig/node22": "22.0.2",
|
"@tsconfig/node22": "22.0.2",
|
||||||
"@tsconfig/bun": "1.0.9",
|
"@tsconfig/bun": "1.0.9",
|
||||||
"@cloudflare/workers-types": "4.20251008.0",
|
"@cloudflare/workers-types": "4.20251008.0",
|
||||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||||
"@pierre/precision-diffs": "0.4.4",
|
"@pierre/precision-diffs": "0.5.7",
|
||||||
"@solidjs/meta": "0.29.4",
|
|
||||||
"@tailwindcss/vite": "4.1.11",
|
"@tailwindcss/vite": "4.1.11",
|
||||||
"diff": "8.0.2",
|
"diff": "8.0.2",
|
||||||
"ai": "5.0.8",
|
"ai": "5.0.97",
|
||||||
"hono": "4.7.10",
|
"hono": "4.7.10",
|
||||||
|
"hono-openapi": "1.1.1",
|
||||||
"fuzzysort": "3.1.0",
|
"fuzzysort": "3.1.0",
|
||||||
"luxon": "3.6.1",
|
"luxon": "3.6.1",
|
||||||
"typescript": "5.8.2",
|
"typescript": "5.8.2",
|
||||||
"@typescript/native-preview": "7.0.0-dev.20251014.1",
|
"@typescript/native-preview": "7.0.0-dev.20251014.1",
|
||||||
"zod": "4.1.8",
|
"zod": "4.1.8",
|
||||||
"remeda": "2.26.0",
|
"remeda": "2.26.0",
|
||||||
"solid-js": "1.9.9",
|
|
||||||
"solid-list": "0.3.0",
|
"solid-list": "0.3.0",
|
||||||
"tailwindcss": "4.1.11",
|
"tailwindcss": "4.1.11",
|
||||||
"virtua": "0.42.3",
|
"virtua": "0.42.3",
|
||||||
"vite": "7.1.4",
|
"vite": "7.1.4",
|
||||||
"vite-plugin-solid": "2.11.8"
|
"@solidjs/meta": "0.29.4",
|
||||||
|
"@solidjs/router": "0.15.4",
|
||||||
|
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
|
||||||
|
"solid-js": "1.9.10",
|
||||||
|
"vite-plugin-solid": "2.11.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -56,8 +61,10 @@
|
|||||||
"turbo": "2.5.6"
|
"turbo": "2.5.6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "3.933.0",
|
||||||
"@opencode-ai/script": "workspace:*",
|
"@opencode-ai/script": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*"
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
|
"typescript": "catalog:"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -76,9 +83,6 @@
|
|||||||
"tree-sitter-bash",
|
"tree-sitter-bash",
|
||||||
"web-tree-sitter"
|
"web-tree-sitter"
|
||||||
],
|
],
|
||||||
"patchedDependencies": {
|
|
||||||
"@solidjs/start@1.1.7": "patches/@solidjs%2Fstart@1.1.7.patch"
|
|
||||||
},
|
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
"@types/node": "catalog:"
|
"@types/node": "catalog:"
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ dist
|
|||||||
.output
|
.output
|
||||||
.vercel
|
.vercel
|
||||||
.netlify
|
.netlify
|
||||||
.vinxi
|
|
||||||
app.config.timestamp_*.js
|
app.config.timestamp_*.js
|
||||||
|
|
||||||
# Environment
|
# Environment
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import { defineConfig } from "@solidjs/start/config"
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
middleware: "./src/middleware.ts",
|
|
||||||
vite: {
|
|
||||||
server: {
|
|
||||||
allowedHosts: true,
|
|
||||||
},
|
|
||||||
build: {
|
|
||||||
rollupOptions: {
|
|
||||||
external: ["cloudflare:workers"],
|
|
||||||
},
|
|
||||||
minify: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
compatibilityDate: "2024-09-19",
|
|
||||||
preset: "cloudflare_module",
|
|
||||||
cloudflare: {
|
|
||||||
nodeCompat: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -1,32 +1,37 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
|
"version": "1.0.120",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsgo --noEmit",
|
"typecheck": "tsgo --noEmit",
|
||||||
"dev": "vinxi dev --host 0.0.0.0",
|
"dev": "vite dev --host 0.0.0.0",
|
||||||
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
|
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
|
||||||
"build": "./script/generate-sitemap.ts && vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json",
|
"build": "./script/generate-sitemap.ts && vite build && ../../opencode/script/schema.ts ./.output/public/config.json",
|
||||||
"start": "vinxi start",
|
"start": "vite start"
|
||||||
"version": "1.0.56"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
|
"@jsx-email/render": "1.1.1",
|
||||||
|
"@kobalte/core": "catalog:",
|
||||||
|
"@openauthjs/openauth": "catalog:",
|
||||||
"@opencode-ai/console-core": "workspace:*",
|
"@opencode-ai/console-core": "workspace:*",
|
||||||
"@opencode-ai/console-mail": "workspace:*",
|
"@opencode-ai/console-mail": "workspace:*",
|
||||||
"@openauthjs/openauth": "catalog:",
|
|
||||||
"@kobalte/core": "catalog:",
|
|
||||||
"@jsx-email/render": "1.1.1",
|
|
||||||
"@opencode-ai/console-resource": "workspace:*",
|
"@opencode-ai/console-resource": "workspace:*",
|
||||||
"@solidjs/meta": "^0.29.4",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@solidjs/router": "^0.15.0",
|
"@solidjs/meta": "catalog:",
|
||||||
"@solidjs/start": "^1.1.0",
|
"@solidjs/router": "catalog:",
|
||||||
|
"@solidjs/start": "catalog:",
|
||||||
|
"chart.js": "4.5.1",
|
||||||
|
"nitro": "3.0.1-alpha.1",
|
||||||
"solid-js": "catalog:",
|
"solid-js": "catalog:",
|
||||||
"vinxi": "^0.5.7",
|
"vite": "catalog:",
|
||||||
"zod": "catalog:"
|
"zod": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@typescript/native-preview": "catalog:",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:"
|
"wrangler": "4.50.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22"
|
"node": ">=22"
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
../../../ui/src/assets/favicon/apple-touch-icon.png
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
../../../ui/src/assets/favicon/favicon-96x96.png
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<svg width="400" height="400" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="400" height="400" fill="#FDFCFC"/>
|
|
||||||
<path d="M96 122.001V70.001H148V122.001H96Z" fill="#17181C"/>
|
|
||||||
<path d="M148.004 122.001V70.001H200.004V122.001H148.004Z" fill="#17181C"/>
|
|
||||||
<path d="M200.008 122.001V70.001H252.008V122.001H200.008Z" fill="#17181C"/>
|
|
||||||
<path d="M251.996 122.001V70.001H303.996V122.001H251.996Z" fill="#17181C"/>
|
|
||||||
<path d="M251.996 173.988V121.988H303.996V173.988H251.996Z" fill="#17181C"/>
|
|
||||||
<path d="M96 225.998V173.998H148V225.998H96Z" fill="#CFCECD"/>
|
|
||||||
<rect width="52" height="52" transform="translate(148.004 173.998)" fill="#17181C"/>
|
|
||||||
<path d="M148.004 225.998V173.998H200.004V225.998H148.004Z" fill="#17181C" fill-opacity="0.1"/>
|
|
||||||
<path d="M200.008 225.998V173.998H252.008V225.998H200.008Z" fill="#17181C"/>
|
|
||||||
<path d="M252.016 225.998V173.998H304.016V225.998H252.016Z" fill="#CFCECD"/>
|
|
||||||
<rect width="52" height="52" transform="translate(96 226.002)" fill="#17181C"/>
|
|
||||||
<path d="M96 278.002V226.002H148V278.002H96Z" fill="#17181C" fill-opacity="0.1"/>
|
|
||||||
<rect width="52" height="52" transform="translate(148.004 226.002)" fill="white"/>
|
|
||||||
<path d="M148.004 278.002V226.002H200.004V278.002H148.004Z" fill="#CFCECD"/>
|
|
||||||
<path d="M200.008 278.002V226.002H252.008V278.002H200.008Z" fill="#CFCECD"/>
|
|
||||||
<path d="M252.016 278.002V226.002H304.016V278.002H252.016Z" fill="#CFCECD"/>
|
|
||||||
<path d="M96 330.012V278.012H148V330.012H96Z" fill="#17181C"/>
|
|
||||||
<path d="M148.004 330.012V278.012H200.004V330.012H148.004Z" fill="#17181C"/>
|
|
||||||
<path d="M200.008 329.99V277.99H252.008V329.99H200.008Z" fill="#17181C"/>
|
|
||||||
<path d="M251.996 330.012V278.012H303.996V330.012H251.996Z" fill="#17181C"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.6 KiB |
+1
@@ -0,0 +1 @@
|
|||||||
|
../../../ui/src/assets/favicon/favicon.ico
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<svg width="400" height="400" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="400" height="400" fill="#0E0E0E"/>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M312 340H88V60H312V340ZM256 116H144V284H256V116Z" fill="white"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 269 B After Width: | Height: | Size: 42 B |
+1
@@ -0,0 +1 @@
|
|||||||
|
../../../ui/src/assets/favicon/favicon.svg
|
||||||
|
Before Width: | Height: | Size: 269 B After Width: | Height: | Size: 42 B |
@@ -2,4 +2,5 @@ User-agent: *
|
|||||||
Allow: /
|
Allow: /
|
||||||
|
|
||||||
# Disallow shared content pages
|
# Disallow shared content pages
|
||||||
Disallow: /s/
|
Disallow: /s/
|
||||||
|
Disallow: /share/
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
../../../ui/src/assets/favicon/site.webmanifest
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
../../../ui/src/assets/favicon/web-app-manifest-192x192.png
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
../../../ui/src/assets/favicon/web-app-manifest-512x512.png
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { MetaProvider, Title, Meta } from "@solidjs/meta"
|
import { MetaProvider, Title, Meta } from "@solidjs/meta"
|
||||||
import { Router } from "@solidjs/router"
|
import { Router } from "@solidjs/router"
|
||||||
import { FileRoutes } from "@solidjs/start/router"
|
import { FileRoutes } from "@solidjs/start/router"
|
||||||
import { ErrorBoundary, Suspense } from "solid-js"
|
import { Suspense } from "solid-js"
|
||||||
|
import { Favicon } from "@opencode-ai/ui/favicon"
|
||||||
import "@ibm/plex/css/ibm-plex.css"
|
import "@ibm/plex/css/ibm-plex.css"
|
||||||
import "./app.css"
|
import "./app.css"
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ export default function App() {
|
|||||||
<MetaProvider>
|
<MetaProvider>
|
||||||
<Title>opencode</Title>
|
<Title>opencode</Title>
|
||||||
<Meta name="description" content="OpenCode - The AI coding agent built for the terminal." />
|
<Meta name="description" content="OpenCode - The AI coding agent built for the terminal." />
|
||||||
|
<Favicon />
|
||||||
<Suspense>{props.children}</Suspense>
|
<Suspense>{props.children}</Suspense>
|
||||||
</MetaProvider>
|
</MetaProvider>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -202,6 +202,14 @@ export function IconZai(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function IconGemini(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg {...props} viewBox="0 0 50 50" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M49.04,24.001l-1.082-0.043h-0.001C36.134,23.492,26.508,13.866,26.042,2.043L25.999,0.96C25.978,0.424,25.537,0,25,0 s-0.978,0.424-0.999,0.96l-0.043,1.083C23.492,13.866,13.866,23.492,2.042,23.958L0.96,24.001C0.424,24.022,0,24.463,0,25 c0,0.537,0.424,0.978,0.961,0.999l1.082,0.042c11.823,0.467,21.449,10.093,21.915,21.916l0.043,1.083C24.022,49.576,24.463,50,25,50 s0.978-0.424,0.999-0.96l0.043-1.083c0.466-11.823,10.092-21.449,21.915-21.916l1.082-0.042C49.576,25.978,50,25.537,50,25 C50,24.463,49.576,24.022,49.04,24.001z"></path>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function IconStealth(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
export function IconStealth(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||||
return (
|
return (
|
||||||
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 18" fill="none">
|
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 18" fill="none">
|
||||||
@@ -212,3 +220,30 @@ export function IconStealth(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
|||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function IconChevronLeft(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg {...props} viewBox="0 0 20 20" fill="none">
|
||||||
|
<path d="M12 15L7 10L12 5" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconChevronRight(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg {...props} viewBox="0 0 20 20" fill="none">
|
||||||
|
<path d="M8 5L13 10L8 15" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconBreakdown(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg {...props} width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<path d="M2 12L2 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||||
|
<path d="M6 12L6 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||||
|
<path d="M10 12L10 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||||
|
<path d="M14 12L14 9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ export const config = {
|
|||||||
|
|
||||||
// Static stats (used on landing page)
|
// Static stats (used on landing page)
|
||||||
stats: {
|
stats: {
|
||||||
contributors: "250",
|
contributors: "300",
|
||||||
commits: "3,500",
|
commits: "4,000",
|
||||||
monthlyUsers: "300,000",
|
monthlyUsers: "300,000",
|
||||||
},
|
},
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useSession } from "vinxi/http"
|
import { useSession } from "@solidjs/start/http"
|
||||||
|
|
||||||
export interface AuthSession {
|
export interface AuthSession {
|
||||||
account?: Record<
|
account?: Record<
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export default createHandler(
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" href="/favicon.svg" />
|
|
||||||
<meta property="og:image" content="/social-share.png" />
|
<meta property="og:image" content="/social-share.png" />
|
||||||
<meta property="twitter:image" content="/social-share.png" />
|
<meta property="twitter:image" content="/social-share.png" />
|
||||||
{assets}
|
{assets}
|
||||||
|
|||||||
Vendored
+4
@@ -1 +1,5 @@
|
|||||||
/// <reference types="@solidjs/start/env" />
|
/// <reference types="@solidjs/start/env" />
|
||||||
|
|
||||||
|
export declare module "@solidjs/start/server" {
|
||||||
|
export type APIEvent = { request: Request }
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { defineMiddleware } from "vinxi/http"
|
import { createMiddleware } from "@solidjs/start/middleware"
|
||||||
|
|
||||||
export default defineMiddleware({
|
export default createMiddleware({
|
||||||
onBeforeResponse() {},
|
onBeforeResponse() {},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ ${body.email}`.trim()
|
|||||||
to: "contact@anoma.ly",
|
to: "contact@anoma.ly",
|
||||||
subject: `Enterprise Inquiry from ${body.name}`,
|
subject: `Enterprise Inquiry from ${body.name}`,
|
||||||
body: emailContent,
|
body: emailContent,
|
||||||
|
replyTo: body.email,
|
||||||
})
|
})
|
||||||
|
|
||||||
return Response.json({ success: true, message: "Form submitted successfully" }, { status: 200 })
|
return Response.json({ success: true, message: "Form submitted successfully" }, { status: 200 })
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export async function GET(input: APIEvent) {
|
|||||||
return {
|
return {
|
||||||
...value,
|
...value,
|
||||||
account: {
|
account: {
|
||||||
|
...value.account,
|
||||||
[id]: {
|
[id]: {
|
||||||
id,
|
id,
|
||||||
email: decoded.subject.properties.email,
|
email: decoded.subject.properties.email,
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { redirect } from "@solidjs/router"
|
||||||
|
import { APIEvent } from "@solidjs/start"
|
||||||
|
import { useAuthSession } from "~/context/auth.session"
|
||||||
|
|
||||||
|
export async function GET(event: APIEvent) {
|
||||||
|
const auth = await useAuthSession()
|
||||||
|
const current = auth.data.current
|
||||||
|
if (current)
|
||||||
|
await auth.update((val) => {
|
||||||
|
delete val.account?.[current]
|
||||||
|
const first = Object.keys(val.account ?? {})[0]
|
||||||
|
val.current = first
|
||||||
|
event!.locals.actor = undefined
|
||||||
|
return val
|
||||||
|
})
|
||||||
|
return redirect("/zen")
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { APIEvent } from "@solidjs/start"
|
||||||
|
import { useAuthSession } from "~/context/auth.session"
|
||||||
|
|
||||||
|
export async function GET(input: APIEvent) {
|
||||||
|
const session = await useAuthSession()
|
||||||
|
return Response.json(session.data)
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import "./index.css"
|
import "./index.css"
|
||||||
import { Title, Meta, Link } from "@solidjs/meta"
|
import { Title, Meta, Link } from "@solidjs/meta"
|
||||||
import { HttpHeader } from "@solidjs/start"
|
// import { HttpHeader } from "@solidjs/start"
|
||||||
import video from "../asset/lander/opencode-min.mp4"
|
import video from "../asset/lander/opencode-min.mp4"
|
||||||
import videoPoster from "../asset/lander/opencode-poster.png"
|
import videoPoster from "../asset/lander/opencode-poster.png"
|
||||||
import { IconCopy, IconCheck } from "../component/icon"
|
import { IconCopy, IconCheck } from "../component/icon"
|
||||||
@@ -42,10 +42,9 @@ export default function Home() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<main data-page="opencode">
|
<main data-page="opencode">
|
||||||
<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />
|
{/*<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />*/}
|
||||||
<Title>OpenCode | The AI coding agent built for the terminal</Title>
|
<Title>OpenCode | The AI coding agent built for the terminal</Title>
|
||||||
<Link rel="canonical" href={config.baseUrl} />
|
<Link rel="canonical" href={config.baseUrl} />
|
||||||
<Link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
||||||
<Meta property="og:image" content="/social-share.png" />
|
<Meta property="og:image" content="/social-share.png" />
|
||||||
<Meta name="twitter:image" content="/social-share.png" />
|
<Meta name="twitter:image" content="/social-share.png" />
|
||||||
<div data-component="container">
|
<div data-component="container">
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
[data-slot="item"] {
|
[data-slot="item"] {
|
||||||
color: var(--color-danger);
|
color: var(--color-danger);
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { action, redirect } from "@solidjs/router"
|
import { action } from "@solidjs/router"
|
||||||
import { getRequestEvent } from "solid-js/web"
|
import { getRequestEvent } from "solid-js/web"
|
||||||
import { useAuthSession } from "~/context/auth.session"
|
import { useAuthSession } from "~/context/auth.session"
|
||||||
import { Dropdown } from "~/component/dropdown"
|
import { Dropdown } from "~/component/dropdown"
|
||||||
@@ -17,18 +17,15 @@ const logout = action(async () => {
|
|||||||
event!.locals.actor = undefined
|
event!.locals.actor = undefined
|
||||||
return val
|
return val
|
||||||
})
|
})
|
||||||
throw redirect("/zen")
|
}, "auth.logout")
|
||||||
})
|
|
||||||
|
|
||||||
export function UserMenu(props: { email: string | null | undefined }) {
|
export function UserMenu(props: { email: string | null | undefined }) {
|
||||||
return (
|
return (
|
||||||
<div data-component="user-menu">
|
<div data-component="user-menu">
|
||||||
<Dropdown trigger={props.email ?? ""} align="right">
|
<Dropdown trigger={props.email ?? ""} align="right">
|
||||||
<form action={logout} method="post">
|
<a href="/auth/logout" data-slot="item">
|
||||||
<button type="submit" formaction={logout} data-slot="item">
|
Logout
|
||||||
Logout
|
</a>
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { UserMenu } from "./user-menu"
|
|||||||
import { withActor } from "~/context/auth.withActor"
|
import { withActor } from "~/context/auth.withActor"
|
||||||
import { User } from "@opencode-ai/console-core/user.js"
|
import { User } from "@opencode-ai/console-core/user.js"
|
||||||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||||
import { Link } from "@solidjs/meta"
|
|
||||||
|
|
||||||
const getUserEmail = query(async (workspaceID: string) => {
|
const getUserEmail = query(async (workspaceID: string) => {
|
||||||
"use server"
|
"use server"
|
||||||
@@ -22,7 +21,6 @@ export default function WorkspaceLayout(props: RouteSectionProps) {
|
|||||||
const userEmail = createAsync(() => getUserEmail(params.id!))
|
const userEmail = createAsync(() => getUserEmail(params.id!))
|
||||||
return (
|
return (
|
||||||
<main data-page="workspace">
|
<main data-page="workspace">
|
||||||
<Link rel="icon" type="image/svg+xml" href="/favicon-zen.svg" />
|
|
||||||
<header data-component="workspace-header">
|
<header data-component="workspace-header">
|
||||||
<div data-slot="header-brand">
|
<div data-slot="header-brand">
|
||||||
<A href="/" data-component="site-title">
|
<A href="/" data-component="site-title">
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
.root {
|
||||||
|
[data-component="empty-state"] {
|
||||||
|
padding: var(--space-20) var(--space-6);
|
||||||
|
text-align: center;
|
||||||
|
border: 1px dashed var(--color-border);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
height: 400px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="filter-container"] {
|
||||||
|
margin-bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
|
||||||
|
[data-component="dropdown"] {
|
||||||
|
[data-slot="trigger"] {
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
box-shadow: 0 0 0 3px var(--color-accent-alpha);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="chevron"] {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="dropdown"] {
|
||||||
|
min-width: 200px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: var(--space-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="month-picker"] {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="month-button"] {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: none;
|
||||||
|
border: none !important;
|
||||||
|
color: var(--color-text);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="month-label"] {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-text);
|
||||||
|
line-height: 1.5;
|
||||||
|
min-width: 140px;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="model-item"] {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--color-text);
|
||||||
|
border: none !important;
|
||||||
|
background: none;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--color-bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
flex: 1;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="chart-container"] {
|
||||||
|
padding: var(--space-6);
|
||||||
|
background: var(--color-bg-secondary);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 40rem) {
|
||||||
|
[data-slot="chart-container"] {
|
||||||
|
height: 300px;
|
||||||
|
padding: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="empty-state"] {
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,423 @@
|
|||||||
|
import { and, Database, eq, gte, inArray, isNull, lte, or, sql, sum } from "@opencode-ai/console-core/drizzle/index.js"
|
||||||
|
import { UsageTable } from "@opencode-ai/console-core/schema/billing.sql.js"
|
||||||
|
import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js"
|
||||||
|
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
|
||||||
|
import { AuthTable } from "@opencode-ai/console-core/schema/auth.sql.js"
|
||||||
|
import { createAsync, query, useParams } from "@solidjs/router"
|
||||||
|
import { createEffect, createMemo, onCleanup, Show, For } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
import { withActor } from "~/context/auth.withActor"
|
||||||
|
import { Dropdown } from "~/component/dropdown"
|
||||||
|
import { IconChevronLeft, IconChevronRight } from "~/component/icon"
|
||||||
|
import styles from "./graph-section.module.css"
|
||||||
|
import {
|
||||||
|
Chart,
|
||||||
|
BarController,
|
||||||
|
BarElement,
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
type ChartConfiguration,
|
||||||
|
} from "chart.js"
|
||||||
|
|
||||||
|
Chart.register(BarController, BarElement, CategoryScale, LinearScale, Tooltip, Legend)
|
||||||
|
|
||||||
|
async function getCosts(workspaceID: string, year: number, month: number) {
|
||||||
|
"use server"
|
||||||
|
return withActor(async () => {
|
||||||
|
const startDate = new Date(year, month, 1)
|
||||||
|
const endDate = new Date(year, month + 1, 0)
|
||||||
|
|
||||||
|
// First query: get usage data without joining keys
|
||||||
|
const usageData = await Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.select({
|
||||||
|
date: sql<string>`DATE(${UsageTable.timeCreated})`,
|
||||||
|
model: UsageTable.model,
|
||||||
|
totalCost: sum(UsageTable.cost),
|
||||||
|
keyId: UsageTable.keyID,
|
||||||
|
})
|
||||||
|
.from(UsageTable)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(UsageTable.workspaceID, workspaceID),
|
||||||
|
gte(UsageTable.timeCreated, startDate),
|
||||||
|
lte(UsageTable.timeCreated, endDate),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.groupBy(sql`DATE(${UsageTable.timeCreated})`, UsageTable.model, UsageTable.keyID)
|
||||||
|
.then((x) =>
|
||||||
|
x.map((r) => ({
|
||||||
|
...r,
|
||||||
|
totalCost: r.totalCost ? parseInt(r.totalCost) : 0,
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get unique key IDs from usage
|
||||||
|
const usageKeyIds = new Set(usageData.map((r) => r.keyId).filter((id) => id !== null))
|
||||||
|
|
||||||
|
// Second query: get all existing keys plus any keys from usage
|
||||||
|
const keysData = await Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.select({
|
||||||
|
keyId: KeyTable.id,
|
||||||
|
keyName: KeyTable.name,
|
||||||
|
userEmail: AuthTable.subject,
|
||||||
|
timeDeleted: KeyTable.timeDeleted,
|
||||||
|
})
|
||||||
|
.from(KeyTable)
|
||||||
|
.innerJoin(UserTable, and(eq(KeyTable.userID, UserTable.id), eq(KeyTable.workspaceID, UserTable.workspaceID)))
|
||||||
|
.innerJoin(AuthTable, and(eq(UserTable.accountID, AuthTable.accountID), eq(AuthTable.provider, "email")))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(KeyTable.workspaceID, workspaceID),
|
||||||
|
usageKeyIds.size > 0
|
||||||
|
? or(inArray(KeyTable.id, Array.from(usageKeyIds)), isNull(KeyTable.timeDeleted))
|
||||||
|
: isNull(KeyTable.timeDeleted),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.orderBy(AuthTable.subject, KeyTable.name),
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
usage: usageData,
|
||||||
|
keys: keysData.map((key) => ({
|
||||||
|
id: key.keyId,
|
||||||
|
displayName:
|
||||||
|
key.timeDeleted !== null
|
||||||
|
? `${key.userEmail} - ${key.keyName} (deleted)`
|
||||||
|
: `${key.userEmail} - ${key.keyName}`,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}, workspaceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryCosts = query(getCosts, "costs.get")
|
||||||
|
|
||||||
|
const MODEL_COLORS: Record<string, string> = {
|
||||||
|
"claude-sonnet-4-5": "#D4745C",
|
||||||
|
"claude-sonnet-4": "#E8B4A4",
|
||||||
|
"claude-opus-4": "#C8A098",
|
||||||
|
"claude-haiku-4-5": "#F0D8D0",
|
||||||
|
"claude-3-5-haiku": "#F8E8E0",
|
||||||
|
"gpt-5.1": "#4A90E2",
|
||||||
|
"gpt-5.1-codex": "#6BA8F0",
|
||||||
|
"gpt-5": "#7DB8F8",
|
||||||
|
"gpt-5-codex": "#9FCAFF",
|
||||||
|
"gpt-5-nano": "#B8D8FF",
|
||||||
|
"grok-code": "#8B5CF6",
|
||||||
|
"big-pickle": "#10B981",
|
||||||
|
"kimi-k2": "#F59E0B",
|
||||||
|
"qwen3-coder": "#EC4899",
|
||||||
|
"glm-4.6": "#14B8A6",
|
||||||
|
}
|
||||||
|
|
||||||
|
function getModelColor(model: string): string {
|
||||||
|
if (MODEL_COLORS[model]) return MODEL_COLORS[model]
|
||||||
|
|
||||||
|
const hash = model.split("").reduce((acc, char) => char.charCodeAt(0) + ((acc << 5) - acc), 0)
|
||||||
|
const hue = Math.abs(hash) % 360
|
||||||
|
return `hsl(${hue}, 50%, 65%)`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateLabel(dateStr: string): string {
|
||||||
|
const date = new Date()
|
||||||
|
const [y, m, d] = dateStr.split("-").map(Number)
|
||||||
|
date.setFullYear(y)
|
||||||
|
date.setMonth(m - 1)
|
||||||
|
date.setDate(d)
|
||||||
|
date.setHours(0, 0, 0, 0)
|
||||||
|
const month = date.toLocaleDateString("en-US", { month: "short" })
|
||||||
|
const day = date.getUTCDate().toString().padStart(2, "0")
|
||||||
|
return `${month} ${day}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function addOpacityToColor(color: string, opacity: number): string {
|
||||||
|
if (color.startsWith("#")) {
|
||||||
|
const r = parseInt(color.slice(1, 3), 16)
|
||||||
|
const g = parseInt(color.slice(3, 5), 16)
|
||||||
|
const b = parseInt(color.slice(5, 7), 16)
|
||||||
|
return `rgba(${r}, ${g}, ${b}, ${opacity})`
|
||||||
|
}
|
||||||
|
if (color.startsWith("hsl")) return color.replace(")", `, ${opacity})`).replace("hsl", "hsla")
|
||||||
|
return color
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GraphSection() {
|
||||||
|
let canvasRef: HTMLCanvasElement | undefined
|
||||||
|
let chartInstance: Chart | undefined
|
||||||
|
const params = useParams()
|
||||||
|
const now = new Date()
|
||||||
|
const [store, setStore] = createStore({
|
||||||
|
data: null as Awaited<ReturnType<typeof getCosts>> | null,
|
||||||
|
year: now.getFullYear(),
|
||||||
|
month: now.getMonth(),
|
||||||
|
key: null as string | null,
|
||||||
|
model: null as string | null,
|
||||||
|
modelDropdownOpen: false,
|
||||||
|
keyDropdownOpen: false,
|
||||||
|
})
|
||||||
|
const initialData = createAsync(() => queryCosts(params.id!, store.year, store.month))
|
||||||
|
|
||||||
|
const onPreviousMonth = async () => {
|
||||||
|
const month = store.month === 0 ? 11 : store.month - 1
|
||||||
|
const year = store.month === 0 ? store.year - 1 : store.year
|
||||||
|
const data = await getCosts(params.id!, year, month)
|
||||||
|
setStore({ month, year, data })
|
||||||
|
}
|
||||||
|
|
||||||
|
const onNextMonth = async () => {
|
||||||
|
const month = store.month === 11 ? 0 : store.month + 1
|
||||||
|
const year = store.month === 11 ? store.year + 1 : store.year
|
||||||
|
setStore({ month, year, data: await getCosts(params.id!, year, month) })
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSelectModel = (model: string | null) => setStore({ model, modelDropdownOpen: false })
|
||||||
|
|
||||||
|
const onSelectKey = (keyID: string | null) => setStore({ key: keyID, keyDropdownOpen: false })
|
||||||
|
|
||||||
|
const getData = createMemo(() => store.data ?? initialData())
|
||||||
|
|
||||||
|
const getModels = createMemo(() => {
|
||||||
|
const data = getData()
|
||||||
|
if (!data?.usage) return []
|
||||||
|
return Array.from(new Set(data.usage.map((row) => row.model))).sort()
|
||||||
|
})
|
||||||
|
|
||||||
|
const getDates = createMemo(() => {
|
||||||
|
const daysInMonth = new Date(store.year, store.month + 1, 0).getDate()
|
||||||
|
return Array.from({ length: daysInMonth }, (_, i) => {
|
||||||
|
const date = new Date(store.year, store.month, i + 1)
|
||||||
|
return date.toISOString().split("T")[0]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const getKeyName = (keyID: string | null): string => {
|
||||||
|
if (!keyID || !store.data?.keys) return "All Keys"
|
||||||
|
const found = store.data.keys.find((k) => k.id === keyID)
|
||||||
|
return found?.displayName ?? "All Keys"
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatMonthYear = () =>
|
||||||
|
new Date(store.year, store.month, 1).toLocaleDateString("en-US", { month: "long", year: "numeric" })
|
||||||
|
|
||||||
|
const isCurrentMonth = () => store.year === now.getFullYear() && store.month === now.getMonth()
|
||||||
|
|
||||||
|
const chartConfig = createMemo((): ChartConfiguration | null => {
|
||||||
|
const data = getData()
|
||||||
|
const dates = getDates()
|
||||||
|
if (!data?.usage?.length) return null
|
||||||
|
|
||||||
|
const dailyData = new Map<string, Map<string, number>>()
|
||||||
|
for (const dateKey of dates) dailyData.set(dateKey, new Map())
|
||||||
|
|
||||||
|
data.usage
|
||||||
|
.filter((row) => (store.key ? row.keyId === store.key : true))
|
||||||
|
.forEach((row) => {
|
||||||
|
const dayMap = dailyData.get(row.date)
|
||||||
|
if (!dayMap) return
|
||||||
|
dayMap.set(row.model, (dayMap.get(row.model) ?? 0) + row.totalCost)
|
||||||
|
})
|
||||||
|
|
||||||
|
const filteredModels = store.model === null ? getModels() : [store.model]
|
||||||
|
|
||||||
|
const datasets = filteredModels.map((model) => {
|
||||||
|
const color = getModelColor(model)
|
||||||
|
return {
|
||||||
|
label: model,
|
||||||
|
data: dates.map((date) => (dailyData.get(date)?.get(model) || 0) / 100_000_000),
|
||||||
|
backgroundColor: color,
|
||||||
|
hoverBackgroundColor: color,
|
||||||
|
borderWidth: 0,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "bar",
|
||||||
|
data: {
|
||||||
|
labels: dates.map(formatDateLabel),
|
||||||
|
datasets,
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
stacked: true,
|
||||||
|
grid: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
maxRotation: 0,
|
||||||
|
autoSkipPadding: 20,
|
||||||
|
color: "rgba(255, 255, 255, 0.5)",
|
||||||
|
font: {
|
||||||
|
family: "monospace",
|
||||||
|
size: 11,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
stacked: true,
|
||||||
|
beginAtZero: true,
|
||||||
|
grid: {
|
||||||
|
color: "rgba(255, 255, 255, 0.1)",
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
color: "rgba(255, 255, 255, 0.5)",
|
||||||
|
font: {
|
||||||
|
family: "monospace",
|
||||||
|
size: 11,
|
||||||
|
},
|
||||||
|
callback: (value) => {
|
||||||
|
const num = Number(value)
|
||||||
|
return num >= 1000 ? `$${(num / 1000).toFixed(1)}k` : `$${num.toFixed(0)}`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
tooltip: {
|
||||||
|
mode: "index",
|
||||||
|
intersect: false,
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.9)",
|
||||||
|
titleColor: "rgba(255, 255, 255, 0.9)",
|
||||||
|
bodyColor: "rgba(255, 255, 255, 0.8)",
|
||||||
|
borderColor: "rgba(255, 255, 255, 0.1)",
|
||||||
|
borderWidth: 1,
|
||||||
|
padding: 12,
|
||||||
|
displayColors: true,
|
||||||
|
callbacks: {
|
||||||
|
label: (context) => {
|
||||||
|
const value = context.parsed.y
|
||||||
|
if (!value || value === 0) return
|
||||||
|
return `${context.dataset.label}: $${value.toFixed(2)}`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
position: "bottom",
|
||||||
|
labels: {
|
||||||
|
color: "rgba(255, 255, 255, 0.7)",
|
||||||
|
font: {
|
||||||
|
size: 12,
|
||||||
|
},
|
||||||
|
padding: 16,
|
||||||
|
boxWidth: 16,
|
||||||
|
boxHeight: 16,
|
||||||
|
usePointStyle: false,
|
||||||
|
},
|
||||||
|
onHover: (event, legendItem, legend) => {
|
||||||
|
const chart = legend.chart
|
||||||
|
chart.data.datasets?.forEach((dataset, i) => {
|
||||||
|
const meta = chart.getDatasetMeta(i)
|
||||||
|
const baseColor = getModelColor(dataset.label || "")
|
||||||
|
const color = i === legendItem.datasetIndex ? baseColor : addOpacityToColor(baseColor, 0.3)
|
||||||
|
meta.data.forEach((bar: any) => {
|
||||||
|
bar.options.backgroundColor = color
|
||||||
|
})
|
||||||
|
})
|
||||||
|
chart.update("none")
|
||||||
|
},
|
||||||
|
onLeave: (event, legendItem, legend) => {
|
||||||
|
const chart = legend.chart
|
||||||
|
chart.data.datasets?.forEach((dataset, i) => {
|
||||||
|
const meta = chart.getDatasetMeta(i)
|
||||||
|
const baseColor = getModelColor(dataset.label || "")
|
||||||
|
meta.data.forEach((bar: any) => {
|
||||||
|
bar.options.backgroundColor = baseColor
|
||||||
|
})
|
||||||
|
})
|
||||||
|
chart.update("none")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const config = chartConfig()
|
||||||
|
if (!config || !canvasRef) return
|
||||||
|
|
||||||
|
if (chartInstance) chartInstance.destroy()
|
||||||
|
chartInstance = new Chart(canvasRef, config)
|
||||||
|
})
|
||||||
|
|
||||||
|
onCleanup(() => chartInstance?.destroy())
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section class={styles.root}>
|
||||||
|
<div data-slot="section-title">
|
||||||
|
<h2>Cost</h2>
|
||||||
|
<p>Usage costs broken down by model.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={getData()}>
|
||||||
|
<div data-slot="filter-container">
|
||||||
|
<div data-slot="month-picker">
|
||||||
|
<button data-slot="month-button" onClick={onPreviousMonth}>
|
||||||
|
<IconChevronLeft />
|
||||||
|
</button>
|
||||||
|
<span data-slot="month-label">{formatMonthYear()}</span>
|
||||||
|
<button data-slot="month-button" onClick={onNextMonth} disabled={isCurrentMonth()}>
|
||||||
|
<IconChevronRight />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Dropdown
|
||||||
|
trigger={store.model === null ? "All Models" : store.model}
|
||||||
|
open={store.modelDropdownOpen}
|
||||||
|
onOpenChange={(open) => setStore({ modelDropdownOpen: open })}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<button data-slot="model-item" onClick={() => onSelectModel(null)}>
|
||||||
|
<span>All Models</span>
|
||||||
|
</button>
|
||||||
|
<For each={getModels()}>
|
||||||
|
{(model) => (
|
||||||
|
<button data-slot="model-item" onClick={() => onSelectModel(model)}>
|
||||||
|
<span>{model}</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</>
|
||||||
|
</Dropdown>
|
||||||
|
<Dropdown
|
||||||
|
trigger={getKeyName(store.key)}
|
||||||
|
open={store.keyDropdownOpen}
|
||||||
|
onOpenChange={(open) => setStore({ keyDropdownOpen: open })}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<button data-slot="model-item" onClick={() => onSelectKey(null)}>
|
||||||
|
<span>All Keys</span>
|
||||||
|
</button>
|
||||||
|
<For each={getData()?.keys || []}>
|
||||||
|
{(key) => (
|
||||||
|
<button data-slot="model-item" onClick={() => onSelectKey(key.id)}>
|
||||||
|
<span>{key.displayName}</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show
|
||||||
|
when={chartConfig()}
|
||||||
|
fallback={
|
||||||
|
<div data-component="empty-state">
|
||||||
|
<p>No usage data available for the selected period.</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div data-slot="chart-container">
|
||||||
|
<canvas ref={canvasRef} />
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { NewUserSection } from "./new-user-section"
|
|||||||
import { UsageSection } from "./usage-section"
|
import { UsageSection } from "./usage-section"
|
||||||
import { ModelSection } from "./model-section"
|
import { ModelSection } from "./model-section"
|
||||||
import { ProviderSection } from "./provider-section"
|
import { ProviderSection } from "./provider-section"
|
||||||
|
import { GraphSection } from "./graph-section"
|
||||||
import { IconLogo } from "~/component/icon"
|
import { IconLogo } from "~/component/icon"
|
||||||
import { querySessionInfo, queryBillingInfo, createCheckoutUrl, formatBalance } from "../common"
|
import { querySessionInfo, queryBillingInfo, createCheckoutUrl, formatBalance } from "../common"
|
||||||
|
|
||||||
@@ -66,6 +67,9 @@ export default function () {
|
|||||||
|
|
||||||
<div data-slot="sections">
|
<div data-slot="sections">
|
||||||
<NewUserSection />
|
<NewUserSection />
|
||||||
|
<Show when={userInfo()?.isAdmin}>
|
||||||
|
<GraphSection />
|
||||||
|
</Show>
|
||||||
<ModelSection />
|
<ModelSection />
|
||||||
<Show when={userInfo()?.isAdmin}>
|
<Show when={userInfo()?.isAdmin}>
|
||||||
<ProviderSection />
|
<ProviderSection />
|
||||||
|
|||||||
@@ -5,11 +5,21 @@ import { withActor } from "~/context/auth.withActor"
|
|||||||
import { ZenData } from "@opencode-ai/console-core/model.js"
|
import { ZenData } from "@opencode-ai/console-core/model.js"
|
||||||
import styles from "./model-section.module.css"
|
import styles from "./model-section.module.css"
|
||||||
import { querySessionInfo } from "../common"
|
import { querySessionInfo } from "../common"
|
||||||
import { IconAlibaba, IconAnthropic, IconMoonshotAI, IconOpenAI, IconStealth, IconXai, IconZai } from "~/component/icon"
|
import {
|
||||||
|
IconAlibaba,
|
||||||
|
IconAnthropic,
|
||||||
|
IconGemini,
|
||||||
|
IconMoonshotAI,
|
||||||
|
IconOpenAI,
|
||||||
|
IconStealth,
|
||||||
|
IconXai,
|
||||||
|
IconZai,
|
||||||
|
} from "~/component/icon"
|
||||||
|
|
||||||
const getModelLab = (modelId: string) => {
|
const getModelLab = (modelId: string) => {
|
||||||
if (modelId.startsWith("claude")) return "Anthropic"
|
if (modelId.startsWith("claude")) return "Anthropic"
|
||||||
if (modelId.startsWith("gpt")) return "OpenAI"
|
if (modelId.startsWith("gpt")) return "OpenAI"
|
||||||
|
if (modelId.startsWith("gemini")) return "Google"
|
||||||
if (modelId.startsWith("kimi")) return "Moonshot AI"
|
if (modelId.startsWith("kimi")) return "Moonshot AI"
|
||||||
if (modelId.startsWith("glm")) return "Z.ai"
|
if (modelId.startsWith("glm")) return "Z.ai"
|
||||||
if (modelId.startsWith("qwen")) return "Alibaba"
|
if (modelId.startsWith("qwen")) return "Alibaba"
|
||||||
@@ -22,9 +32,19 @@ const getModelsInfo = query(async (workspaceID: string) => {
|
|||||||
return withActor(async () => {
|
return withActor(async () => {
|
||||||
return {
|
return {
|
||||||
all: Object.entries(ZenData.list().models)
|
all: Object.entries(ZenData.list().models)
|
||||||
.filter(([id, _model]) => !["claude-3-5-haiku", "minimax-m2"].includes(id))
|
.filter(([id, _model]) => !["claude-3-5-haiku"].includes(id))
|
||||||
.filter(([id, _model]) => !id.startsWith("an-"))
|
.filter(([id, _model]) => !id.startsWith("alpha-"))
|
||||||
.sort(([_idA, modelA], [_idB, modelB]) => modelA.name.localeCompare(modelB.name))
|
.sort(([idA, modelA], [idB, modelB]) => {
|
||||||
|
const priority = ["big-pickle", "grok", "claude", "gpt", "gemini"]
|
||||||
|
const getPriority = (id: string) => {
|
||||||
|
const index = priority.findIndex((p) => id.startsWith(p))
|
||||||
|
return index === -1 ? Infinity : index
|
||||||
|
}
|
||||||
|
const pA = getPriority(idA)
|
||||||
|
const pB = getPriority(idB)
|
||||||
|
if (pA !== pB) return pA - pB
|
||||||
|
return modelA.name.localeCompare(modelB.name)
|
||||||
|
})
|
||||||
.map(([id, model]) => ({ id, name: model.name })),
|
.map(([id, model]) => ({ id, name: model.name })),
|
||||||
disabled: await Model.listDisabled(),
|
disabled: await Model.listDisabled(),
|
||||||
}
|
}
|
||||||
@@ -96,6 +116,8 @@ export function ModelSection() {
|
|||||||
return <IconOpenAI width={16} height={16} />
|
return <IconOpenAI width={16} height={16} />
|
||||||
case "Anthropic":
|
case "Anthropic":
|
||||||
return <IconAnthropic width={16} height={16} />
|
return <IconAnthropic width={16} height={16} />
|
||||||
|
case "Google":
|
||||||
|
return <IconGemini width={16} height={16} />
|
||||||
case "Moonshot AI":
|
case "Moonshot AI":
|
||||||
return <IconMoonshotAI width={16} height={16} />
|
return <IconMoonshotAI width={16} height={16} />
|
||||||
case "Z.ai":
|
case "Z.ai":
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import styles from "./provider-section.module.css"
|
|||||||
const PROVIDERS = [
|
const PROVIDERS = [
|
||||||
{ name: "OpenAI", key: "openai", prefix: "sk-" },
|
{ name: "OpenAI", key: "openai", prefix: "sk-" },
|
||||||
{ name: "Anthropic", key: "anthropic", prefix: "sk-ant-" },
|
{ name: "Anthropic", key: "anthropic", prefix: "sk-ant-" },
|
||||||
|
{ name: "Google Gemini", key: "google", prefix: "AI" },
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
type Provider = (typeof PROVIDERS)[number]
|
type Provider = (typeof PROVIDERS)[number]
|
||||||
|
|||||||
@@ -1,24 +1,23 @@
|
|||||||
.root {
|
.root {
|
||||||
|
/* Empty state */
|
||||||
[data-component="empty-state"] {
|
[data-component="empty-state"] {
|
||||||
padding: var(--space-20) var(--space-6);
|
padding: var(--space-20) var(--space-6);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border: 1px dashed var(--color-border);
|
border: 1px dashed var(--color-border);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--space-2);
|
|
||||||
|
|
||||||
p {
|
p {
|
||||||
line-height: 1.5;
|
|
||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Table container */
|
||||||
[data-slot="usage-table"] {
|
[data-slot="usage-table"] {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Table element */
|
||||||
[data-slot="usage-table-element"] {
|
[data-slot="usage-table-element"] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
@@ -48,7 +47,6 @@
|
|||||||
|
|
||||||
&[data-slot="usage-model"] {
|
&[data-slot="usage-model"] {
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
font-weight: 400;
|
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
@@ -56,33 +54,133 @@
|
|||||||
|
|
||||||
&[data-slot="usage-cost"] {
|
&[data-slot="usage-cost"] {
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="tokens-with-breakdown"] {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="breakdown-button"] {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="breakdown-popup"] {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 100%;
|
||||||
|
margin-top: var(--space-2);
|
||||||
|
background: var(--color-bg);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
padding: var(--space-2);
|
||||||
|
z-index: 10;
|
||||||
|
min-width: 180px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody tr {
|
tbody tr:last-child td {
|
||||||
&:last-child td {
|
border-bottom: none;
|
||||||
border-bottom: none;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pagination */
|
||||||
|
[data-slot="pagination"] {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding: var(--space-4) 0;
|
||||||
|
border-top: 1px solid var(--color-border-muted);
|
||||||
|
margin-top: var(--space-2);
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: var(--space-2) var(--space-4);
|
||||||
|
background: var(--color-bg-secondary);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: var(--color-bg-tertiary);
|
||||||
|
border-color: var(--color-border-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 40rem) {
|
/* Mobile responsive */
|
||||||
|
@media (max-width: 40rem) {
|
||||||
|
[data-slot="usage-table-element"] {
|
||||||
th,
|
th,
|
||||||
td {
|
td {
|
||||||
padding: var(--space-2) var(--space-3);
|
padding: var(--space-2) var(--space-3);
|
||||||
font-size: var(--font-size-xs);
|
font-size: var(--font-size-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
/* Hide Model column on mobile */
|
||||||
&:nth-child(2) /* Model */ {
|
th:nth-child(2),
|
||||||
display: none;
|
td:nth-child(2) {
|
||||||
}
|
display: none;
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
&:nth-child(2) /* Model */ {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Breakdown popup content */
|
||||||
|
[data-slot="breakdown-row"] {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-4);
|
||||||
|
padding: var(--space-1) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="breakdown-label"] {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="breakdown-value"] {
|
||||||
|
color: var(--color-text);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,81 +1,73 @@
|
|||||||
import { Billing } from "@opencode-ai/console-core/billing.js"
|
import { Billing } from "@opencode-ai/console-core/billing.js"
|
||||||
import { query, useParams, createAsync } from "@solidjs/router"
|
import { createAsync, query, useParams } from "@solidjs/router"
|
||||||
import { createMemo, For, Show } from "solid-js"
|
import { createMemo, For, Show, createEffect, createSignal } from "solid-js"
|
||||||
import { formatDateUTC, formatDateForTable } from "../common"
|
import { formatDateUTC, formatDateForTable } from "../common"
|
||||||
import { withActor } from "~/context/auth.withActor"
|
import { withActor } from "~/context/auth.withActor"
|
||||||
|
import { IconChevronLeft, IconChevronRight, IconBreakdown } from "~/component/icon"
|
||||||
import styles from "./usage-section.module.css"
|
import styles from "./usage-section.module.css"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
|
||||||
const getUsageInfo = query(async (workspaceID: string) => {
|
const PAGE_SIZE = 50
|
||||||
|
|
||||||
|
async function getUsageInfo(workspaceID: string, page: number) {
|
||||||
"use server"
|
"use server"
|
||||||
return withActor(async () => {
|
return withActor(async () => {
|
||||||
return await Billing.usages()
|
return await Billing.usages(page, PAGE_SIZE)
|
||||||
}, workspaceID)
|
}, workspaceID)
|
||||||
}, "usage.list")
|
}
|
||||||
|
|
||||||
|
const queryUsageInfo = query(getUsageInfo, "usage.list")
|
||||||
|
|
||||||
export function UsageSection() {
|
export function UsageSection() {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
// ORIGINAL CODE - COMMENTED OUT FOR TESTING
|
const usage = createAsync(() => queryUsageInfo(params.id!, 0))
|
||||||
const usage = createAsync(() => getUsageInfo(params.id!))
|
const [store, setStore] = createStore({ page: 0, usage: [] as Awaited<ReturnType<typeof getUsageInfo>> })
|
||||||
|
const [openBreakdownId, setOpenBreakdownId] = createSignal<string | null>(null)
|
||||||
|
|
||||||
// DUMMY DATA FOR TESTING
|
createEffect(() => {
|
||||||
// const usage = () => [
|
setStore({ usage: usage() })
|
||||||
// {
|
}, [usage])
|
||||||
// timeCreated: new Date(Date.now() - 86400000 * 0).toISOString(), // Today
|
|
||||||
// model: "claude-3-5-sonnet-20241022",
|
createEffect(() => {
|
||||||
// inputTokens: 1247,
|
if (!openBreakdownId()) return
|
||||||
// outputTokens: 423,
|
|
||||||
// cost: 125400000, // $1.254
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
// },
|
const target = e.target as HTMLElement
|
||||||
// {
|
if (!target.closest('[data-slot="tokens-with-breakdown"]')) {
|
||||||
// timeCreated: new Date(Date.now() - 86400000 * 0.5).toISOString(), // 12 hours ago
|
setOpenBreakdownId(null)
|
||||||
// model: "claude-3-haiku-20240307",
|
}
|
||||||
// inputTokens: 892,
|
}
|
||||||
// outputTokens: 156,
|
|
||||||
// cost: 23500000, // $0.235
|
document.addEventListener("click", handleClickOutside)
|
||||||
// },
|
return () => document.removeEventListener("click", handleClickOutside)
|
||||||
// {
|
})
|
||||||
// timeCreated: new Date(Date.now() - 86400000 * 1).toISOString(), // Yesterday
|
|
||||||
// model: "claude-3-5-sonnet-20241022",
|
const hasResults = createMemo(() => store.usage && store.usage.length > 0)
|
||||||
// inputTokens: 2134,
|
const canGoPrev = createMemo(() => store.page > 0)
|
||||||
// outputTokens: 687,
|
const canGoNext = createMemo(() => store.usage && store.usage.length === PAGE_SIZE)
|
||||||
// cost: 234700000, // $2.347
|
|
||||||
// },
|
const calculateTotalInputTokens = (u: Awaited<ReturnType<typeof getUsageInfo>>[0]) => {
|
||||||
// {
|
return u.inputTokens + (u.cacheReadTokens ?? 0) + (u.cacheWrite5mTokens ?? 0) + (u.cacheWrite1hTokens ?? 0)
|
||||||
// timeCreated: new Date(Date.now() - 86400000 * 1.3).toISOString(), // 1.3 days ago
|
}
|
||||||
// model: "gpt-4o-mini",
|
|
||||||
// inputTokens: 567,
|
const calculateTotalOutputTokens = (u: Awaited<ReturnType<typeof getUsageInfo>>[0]) => {
|
||||||
// outputTokens: 234,
|
return u.outputTokens + (u.reasoningTokens ?? 0)
|
||||||
// cost: 8900000, // $0.089
|
}
|
||||||
// },
|
|
||||||
// {
|
const goPrev = async () => {
|
||||||
// timeCreated: new Date(Date.now() - 86400000 * 2).toISOString(), // 2 days ago
|
const usage = await getUsageInfo(params.id!, store.page - 1)
|
||||||
// model: "claude-3-opus-20240229",
|
setStore({
|
||||||
// inputTokens: 1893,
|
page: store.page - 1,
|
||||||
// outputTokens: 945,
|
usage,
|
||||||
// cost: 445600000, // $4.456
|
})
|
||||||
// },
|
}
|
||||||
// {
|
const goNext = async () => {
|
||||||
// timeCreated: new Date(Date.now() - 86400000 * 2.7).toISOString(), // 2.7 days ago
|
const usage = await getUsageInfo(params.id!, store.page + 1)
|
||||||
// model: "gpt-4o",
|
setStore({
|
||||||
// inputTokens: 1456,
|
page: store.page + 1,
|
||||||
// outputTokens: 532,
|
usage,
|
||||||
// cost: 156800000, // $1.568
|
})
|
||||||
// },
|
}
|
||||||
// {
|
|
||||||
// timeCreated: new Date(Date.now() - 86400000 * 3).toISOString(), // 3 days ago
|
|
||||||
// model: "claude-3-haiku-20240307",
|
|
||||||
// inputTokens: 634,
|
|
||||||
// outputTokens: 89,
|
|
||||||
// cost: 12300000, // $0.123
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// timeCreated: new Date(Date.now() - 86400000 * 4).toISOString(), // 4 days ago
|
|
||||||
// model: "claude-3-5-sonnet-20241022",
|
|
||||||
// inputTokens: 3245,
|
|
||||||
// outputTokens: 1123,
|
|
||||||
// cost: 387200000, // $3.872
|
|
||||||
// },
|
|
||||||
// ]
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section class={styles.root}>
|
<section class={styles.root}>
|
||||||
@@ -85,7 +77,7 @@ export function UsageSection() {
|
|||||||
</div>
|
</div>
|
||||||
<div data-slot="usage-table">
|
<div data-slot="usage-table">
|
||||||
<Show
|
<Show
|
||||||
when={usage() && usage()!.length > 0}
|
when={hasResults()}
|
||||||
fallback={
|
fallback={
|
||||||
<div data-component="empty-state">
|
<div data-component="empty-state">
|
||||||
<p>Make your first API call to get started.</p>
|
<p>Make your first API call to get started.</p>
|
||||||
@@ -103,17 +95,80 @@ export function UsageSection() {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<For each={usage()!}>
|
<For each={store.usage}>
|
||||||
{(usage) => {
|
{(usage, index) => {
|
||||||
const date = createMemo(() => new Date(usage.timeCreated))
|
const date = createMemo(() => new Date(usage.timeCreated))
|
||||||
|
const totalInputTokens = createMemo(() => calculateTotalInputTokens(usage))
|
||||||
|
const totalOutputTokens = createMemo(() => calculateTotalOutputTokens(usage))
|
||||||
|
const inputBreakdownId = `input-breakdown-${index()}`
|
||||||
|
const outputBreakdownId = `output-breakdown-${index()}`
|
||||||
|
const isInputOpen = createMemo(() => openBreakdownId() === inputBreakdownId)
|
||||||
|
const isOutputOpen = createMemo(() => openBreakdownId() === outputBreakdownId)
|
||||||
|
const isClaude = usage.model.toLowerCase().includes("claude")
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td data-slot="usage-date" title={formatDateUTC(date())}>
|
<td data-slot="usage-date" title={formatDateUTC(date())}>
|
||||||
{formatDateForTable(date())}
|
{formatDateForTable(date())}
|
||||||
</td>
|
</td>
|
||||||
<td data-slot="usage-model">{usage.model}</td>
|
<td data-slot="usage-model">{usage.model}</td>
|
||||||
<td data-slot="usage-tokens">{usage.inputTokens}</td>
|
<td data-slot="usage-tokens">
|
||||||
<td data-slot="usage-tokens">{usage.outputTokens}</td>
|
<div data-slot="tokens-with-breakdown" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<button
|
||||||
|
data-slot="breakdown-button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setOpenBreakdownId(isInputOpen() ? null : inputBreakdownId)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconBreakdown />
|
||||||
|
</button>
|
||||||
|
<span onClick={() => setOpenBreakdownId(null)}>{totalInputTokens()}</span>
|
||||||
|
<Show when={isInputOpen()}>
|
||||||
|
<div data-slot="breakdown-popup" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<div data-slot="breakdown-row">
|
||||||
|
<span data-slot="breakdown-label">Input</span>
|
||||||
|
<span data-slot="breakdown-value">{usage.inputTokens}</span>
|
||||||
|
</div>
|
||||||
|
<div data-slot="breakdown-row">
|
||||||
|
<span data-slot="breakdown-label">Cache Read</span>
|
||||||
|
<span data-slot="breakdown-value">{usage.cacheReadTokens ?? 0}</span>
|
||||||
|
</div>
|
||||||
|
<Show when={isClaude}>
|
||||||
|
<div data-slot="breakdown-row">
|
||||||
|
<span data-slot="breakdown-label">Cache Write</span>
|
||||||
|
<span data-slot="breakdown-value">{usage.cacheWrite5mTokens ?? 0}</span>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td data-slot="usage-tokens">
|
||||||
|
<div data-slot="tokens-with-breakdown" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<button
|
||||||
|
data-slot="breakdown-button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setOpenBreakdownId(isOutputOpen() ? null : outputBreakdownId)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconBreakdown />
|
||||||
|
</button>
|
||||||
|
<span onClick={() => setOpenBreakdownId(null)}>{totalOutputTokens()}</span>
|
||||||
|
<Show when={isOutputOpen()}>
|
||||||
|
<div data-slot="breakdown-popup" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<div data-slot="breakdown-row">
|
||||||
|
<span data-slot="breakdown-label">Output</span>
|
||||||
|
<span data-slot="breakdown-value">{usage.outputTokens}</span>
|
||||||
|
</div>
|
||||||
|
<div data-slot="breakdown-row">
|
||||||
|
<span data-slot="breakdown-label">Reasoning</span>
|
||||||
|
<span data-slot="breakdown-value">{usage.reasoningTokens ?? 0}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td data-slot="usage-cost">${((usage.cost ?? 0) / 100000000).toFixed(4)}</td>
|
<td data-slot="usage-cost">${((usage.cost ?? 0) / 100000000).toFixed(4)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
@@ -121,6 +176,16 @@ export function UsageSection() {
|
|||||||
</For>
|
</For>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<Show when={canGoPrev() || canGoNext()}>
|
||||||
|
<div data-slot="pagination">
|
||||||
|
<button disabled={!canGoPrev()} onClick={goPrev}>
|
||||||
|
<IconChevronLeft />
|
||||||
|
</button>
|
||||||
|
<button disabled={!canGoNext()} onClick={goNext}>
|
||||||
|
<IconChevronRight />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import "./index.css"
|
import "./index.css"
|
||||||
import { createAsync, query, redirect } from "@solidjs/router"
|
import { createAsync, query, redirect } from "@solidjs/router"
|
||||||
import { Title, Meta, Link } from "@solidjs/meta"
|
import { Title, Meta, Link } from "@solidjs/meta"
|
||||||
import { HttpHeader } from "@solidjs/start"
|
// import { HttpHeader } from "@solidjs/start"
|
||||||
import zenLogoLight from "../../asset/zen-ornate-light.svg"
|
import zenLogoLight from "../../asset/zen-ornate-light.svg"
|
||||||
import { config } from "~/config"
|
import { config } from "~/config"
|
||||||
import zenLogoDark from "../../asset/zen-ornate-dark.svg"
|
import zenLogoDark from "../../asset/zen-ornate-dark.svg"
|
||||||
@@ -18,23 +18,24 @@ import { Legal } from "~/component/legal"
|
|||||||
import { Footer } from "~/component/footer"
|
import { Footer } from "~/component/footer"
|
||||||
import { Header } from "~/component/header"
|
import { Header } from "~/component/header"
|
||||||
import { getLastSeenWorkspaceID } from "../workspace/common"
|
import { getLastSeenWorkspaceID } from "../workspace/common"
|
||||||
|
import { IconGemini, IconZai } from "~/component/icon"
|
||||||
|
|
||||||
const checkLoggedIn = query(async () => {
|
const checkLoggedIn = query(async () => {
|
||||||
"use server"
|
"use server"
|
||||||
const workspaceID = await getLastSeenWorkspaceID()
|
const workspaceID = await getLastSeenWorkspaceID().catch(() => {})
|
||||||
if (workspaceID) throw redirect(`/workspace/${workspaceID}`)
|
if (workspaceID) throw redirect(`/workspace/${workspaceID}`)
|
||||||
}, "checkLoggedIn.get")
|
}, "checkLoggedIn.get")
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
createAsync(() => checkLoggedIn())
|
const loggedin = createAsync(() => checkLoggedIn())
|
||||||
return (
|
return (
|
||||||
<main data-page="zen">
|
<main data-page="zen">
|
||||||
<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />
|
{/*<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />*/}
|
||||||
<Title>OpenCode Zen | A curated set of reliable optimized models for coding agents</Title>
|
<Title>OpenCode Zen | A curated set of reliable optimized models for coding agents</Title>
|
||||||
<Link rel="canonical" href={`${config.baseUrl}/zen`} />
|
<Link rel="canonical" href={`${config.baseUrl}/zen`} />
|
||||||
<Link rel="icon" type="image/svg+xml" href="/favicon-zen.svg" />
|
|
||||||
<Meta property="og:image" content="/social-share-zen.png" />
|
<Meta property="og:image" content="/social-share-zen.png" />
|
||||||
<Meta name="twitter:image" content="/social-share-zen.png" />
|
<Meta name="twitter:image" content="/social-share-zen.png" />
|
||||||
|
<Meta name="opencode:auth" content={loggedin() ? "true" : "false"} />
|
||||||
|
|
||||||
<div data-component="container">
|
<div data-component="container">
|
||||||
<Header zen />
|
<Header zen />
|
||||||
@@ -81,6 +82,9 @@ export default function Home() {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<IconGemini width="24" height="24" />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path
|
<path
|
||||||
@@ -111,6 +115,9 @@ export default function Home() {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<IconZai width="24" height="24" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a href="/auth">
|
<a href="/auth">
|
||||||
<span>Get started with Zen </span>
|
<span>Get started with Zen </span>
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Resource, waitUntil } from "@opencode-ai/console-resource"
|
||||||
|
|
||||||
|
export function createDataDumper(sessionId: string, requestId: string) {
|
||||||
|
if (Resource.App.stage !== "production") return
|
||||||
|
|
||||||
|
let data: Record<string, any> = {}
|
||||||
|
let modelName: string | undefined
|
||||||
|
|
||||||
|
return {
|
||||||
|
provideModel: (model?: string) => (modelName = model),
|
||||||
|
provideRequest: (request: string) => (data.request = request),
|
||||||
|
provideResponse: (response: string) => (data.response = response),
|
||||||
|
provideStream: (chunk: string) => (data.response = (data.response ?? "") + chunk),
|
||||||
|
flush: () => {
|
||||||
|
if (!modelName) return
|
||||||
|
|
||||||
|
const str = new Date().toISOString().replace(/[^0-9]/g, "")
|
||||||
|
const yyyymmdd = str.substring(0, 8)
|
||||||
|
const hh = str.substring(8, 10)
|
||||||
|
|
||||||
|
waitUntil(
|
||||||
|
Resource.ConsoleData.put(`${yyyymmdd}/${hh}/${modelName}/${sessionId}/${requestId}.json`, JSON.stringify(data)),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,11 +13,20 @@ import { ModelTable } from "@opencode-ai/console-core/schema/model.sql.js"
|
|||||||
import { ProviderTable } from "@opencode-ai/console-core/schema/provider.sql.js"
|
import { ProviderTable } from "@opencode-ai/console-core/schema/provider.sql.js"
|
||||||
import { logger } from "./logger"
|
import { logger } from "./logger"
|
||||||
import { AuthError, CreditsError, MonthlyLimitError, UserLimitError, ModelError, RateLimitError } from "./error"
|
import { AuthError, CreditsError, MonthlyLimitError, UserLimitError, ModelError, RateLimitError } from "./error"
|
||||||
import { createBodyConverter, createStreamPartConverter, createResponseConverter } from "./provider/provider"
|
import {
|
||||||
|
createBodyConverter,
|
||||||
|
createStreamPartConverter,
|
||||||
|
createResponseConverter,
|
||||||
|
ProviderHelper,
|
||||||
|
UsageInfo,
|
||||||
|
} from "./provider/provider"
|
||||||
import { anthropicHelper } from "./provider/anthropic"
|
import { anthropicHelper } from "./provider/anthropic"
|
||||||
|
import { googleHelper } from "./provider/google"
|
||||||
import { openaiHelper } from "./provider/openai"
|
import { openaiHelper } from "./provider/openai"
|
||||||
import { oaCompatHelper } from "./provider/openai-compatible"
|
import { oaCompatHelper } from "./provider/openai-compatible"
|
||||||
import { createRateLimiter } from "./rateLimiter"
|
import { createRateLimiter } from "./rateLimiter"
|
||||||
|
import { createDataDumper } from "./dataDumper"
|
||||||
|
import { createTrialLimiter } from "./trialLimiter"
|
||||||
|
|
||||||
type ZenData = Awaited<ReturnType<typeof ZenData.list>>
|
type ZenData = Awaited<ReturnType<typeof ZenData.list>>
|
||||||
type RetryOptions = {
|
type RetryOptions = {
|
||||||
@@ -30,6 +39,8 @@ export async function handler(
|
|||||||
opts: {
|
opts: {
|
||||||
format: ZenData.Format
|
format: ZenData.Format
|
||||||
parseApiKey: (headers: Headers) => string | undefined
|
parseApiKey: (headers: Headers) => string | undefined
|
||||||
|
parseModel: (url: string, body: any) => string
|
||||||
|
parseIsStream: (url: string, body: any) => boolean
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
type AuthInfo = Awaited<ReturnType<typeof authenticate>>
|
type AuthInfo = Awaited<ReturnType<typeof authenticate>>
|
||||||
@@ -43,20 +54,28 @@ export async function handler(
|
|||||||
]
|
]
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const url = input.request.url
|
||||||
const body = await input.request.json()
|
const body = await input.request.json()
|
||||||
|
const model = opts.parseModel(url, body)
|
||||||
|
const isStream = opts.parseIsStream(url, body)
|
||||||
const ip = input.request.headers.get("x-real-ip") ?? ""
|
const ip = input.request.headers.get("x-real-ip") ?? ""
|
||||||
|
const sessionId = input.request.headers.get("x-opencode-session") ?? ""
|
||||||
|
const requestId = input.request.headers.get("x-opencode-request") ?? ""
|
||||||
logger.metric({
|
logger.metric({
|
||||||
is_tream: !!body.stream,
|
is_tream: isStream,
|
||||||
session: input.request.headers.get("x-opencode-session"),
|
session: sessionId,
|
||||||
request: input.request.headers.get("x-opencode-request"),
|
request: requestId,
|
||||||
})
|
})
|
||||||
const zenData = ZenData.list()
|
const zenData = ZenData.list()
|
||||||
const modelInfo = validateModel(zenData, body.model)
|
const modelInfo = validateModel(zenData, model)
|
||||||
|
const dataDumper = createDataDumper(sessionId, requestId)
|
||||||
|
const trialLimiter = createTrialLimiter(modelInfo.trial?.limit, ip)
|
||||||
|
const isTrial = await trialLimiter?.isTrial()
|
||||||
const rateLimiter = createRateLimiter(modelInfo.id, modelInfo.rateLimit, ip)
|
const rateLimiter = createRateLimiter(modelInfo.id, modelInfo.rateLimit, ip)
|
||||||
await rateLimiter?.check()
|
await rateLimiter?.check()
|
||||||
|
|
||||||
const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => {
|
const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => {
|
||||||
const providerInfo = selectProvider(zenData, modelInfo, ip, retry)
|
const providerInfo = selectProvider(zenData, modelInfo, sessionId, isTrial ?? false, retry)
|
||||||
const authInfo = await authenticate(modelInfo, providerInfo)
|
const authInfo = await authenticate(modelInfo, providerInfo)
|
||||||
validateBilling(authInfo, modelInfo)
|
validateBilling(authInfo, modelInfo)
|
||||||
validateModelSettings(authInfo)
|
validateModelSettings(authInfo)
|
||||||
@@ -64,7 +83,7 @@ export async function handler(
|
|||||||
logger.metric({ provider: providerInfo.id })
|
logger.metric({ provider: providerInfo.id })
|
||||||
|
|
||||||
const startTimestamp = Date.now()
|
const startTimestamp = Date.now()
|
||||||
const reqUrl = providerInfo.modifyUrl(providerInfo.api)
|
const reqUrl = providerInfo.modifyUrl(providerInfo.api, providerInfo.model, isStream)
|
||||||
const reqBody = JSON.stringify(
|
const reqBody = JSON.stringify(
|
||||||
providerInfo.modifyBody({
|
providerInfo.modifyBody({
|
||||||
...createBodyConverter(opts.format, providerInfo.format)(body),
|
...createBodyConverter(opts.format, providerInfo.format)(body),
|
||||||
@@ -98,10 +117,14 @@ export async function handler(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return { providerInfo, authInfo, res, startTimestamp }
|
return { providerInfo, authInfo, reqBody, res, startTimestamp }
|
||||||
}
|
}
|
||||||
|
|
||||||
const { providerInfo, authInfo, res, startTimestamp } = await retriableRequest()
|
const { providerInfo, authInfo, reqBody, res, startTimestamp } = await retriableRequest()
|
||||||
|
|
||||||
|
// Store model request
|
||||||
|
dataDumper?.provideModel(providerInfo.storeModel)
|
||||||
|
dataDumper?.provideRequest(reqBody)
|
||||||
|
|
||||||
// Scrub response headers
|
// Scrub response headers
|
||||||
const resHeaders = new Headers()
|
const resHeaders = new Headers()
|
||||||
@@ -114,14 +137,18 @@ export async function handler(
|
|||||||
logger.debug("STATUS: " + res.status + " " + res.statusText)
|
logger.debug("STATUS: " + res.status + " " + res.statusText)
|
||||||
|
|
||||||
// Handle non-streaming response
|
// Handle non-streaming response
|
||||||
if (!body.stream) {
|
if (!isStream) {
|
||||||
const responseConverter = createResponseConverter(providerInfo.format, opts.format)
|
const responseConverter = createResponseConverter(providerInfo.format, opts.format)
|
||||||
const json = await res.json()
|
const json = await res.json()
|
||||||
const body = JSON.stringify(responseConverter(json))
|
const body = JSON.stringify(responseConverter(json))
|
||||||
logger.metric({ response_length: body.length })
|
logger.metric({ response_length: body.length })
|
||||||
logger.debug("RESPONSE: " + body)
|
logger.debug("RESPONSE: " + body)
|
||||||
|
dataDumper?.provideResponse(body)
|
||||||
|
dataDumper?.flush()
|
||||||
|
const tokensInfo = providerInfo.normalizeUsage(json.usage)
|
||||||
|
await trialLimiter?.track(tokensInfo)
|
||||||
await rateLimiter?.track()
|
await rateLimiter?.track()
|
||||||
await trackUsage(authInfo, modelInfo, providerInfo, json.usage)
|
await trackUsage(authInfo, modelInfo, providerInfo, tokensInfo)
|
||||||
await reload(authInfo)
|
await reload(authInfo)
|
||||||
return new Response(body, {
|
return new Response(body, {
|
||||||
status: res.status,
|
status: res.status,
|
||||||
@@ -149,10 +176,13 @@ export async function handler(
|
|||||||
response_length: responseLength,
|
response_length: responseLength,
|
||||||
"timestamp.last_byte": Date.now(),
|
"timestamp.last_byte": Date.now(),
|
||||||
})
|
})
|
||||||
|
dataDumper?.flush()
|
||||||
await rateLimiter?.track()
|
await rateLimiter?.track()
|
||||||
const usage = usageParser.retrieve()
|
const usage = usageParser.retrieve()
|
||||||
if (usage) {
|
if (usage) {
|
||||||
await trackUsage(authInfo, modelInfo, providerInfo, usage)
|
const tokensInfo = providerInfo.normalizeUsage(usage)
|
||||||
|
await trialLimiter?.track(tokensInfo)
|
||||||
|
await trackUsage(authInfo, modelInfo, providerInfo, tokensInfo)
|
||||||
await reload(authInfo)
|
await reload(authInfo)
|
||||||
}
|
}
|
||||||
c.close()
|
c.close()
|
||||||
@@ -168,8 +198,9 @@ export async function handler(
|
|||||||
}
|
}
|
||||||
responseLength += value.length
|
responseLength += value.length
|
||||||
buffer += decoder.decode(value, { stream: true })
|
buffer += decoder.decode(value, { stream: true })
|
||||||
|
dataDumper?.provideStream(buffer)
|
||||||
|
|
||||||
const parts = buffer.split("\n\n")
|
const parts = buffer.split(providerInfo.streamSeparator)
|
||||||
buffer = parts.pop() ?? ""
|
buffer = parts.pop() ?? ""
|
||||||
|
|
||||||
for (let part of parts) {
|
for (let part of parts) {
|
||||||
@@ -257,8 +288,18 @@ export async function handler(
|
|||||||
return { id: modelId, ...modelData }
|
return { id: modelId, ...modelData }
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectProvider(zenData: ZenData, modelInfo: ModelInfo, ip: string, retry: RetryOptions) {
|
function selectProvider(
|
||||||
|
zenData: ZenData,
|
||||||
|
modelInfo: ModelInfo,
|
||||||
|
sessionId: string,
|
||||||
|
isTrial: boolean,
|
||||||
|
retry: RetryOptions,
|
||||||
|
) {
|
||||||
const provider = (() => {
|
const provider = (() => {
|
||||||
|
if (isTrial) {
|
||||||
|
return modelInfo.providers.find((provider) => provider.id === modelInfo.trial!.provider)
|
||||||
|
}
|
||||||
|
|
||||||
if (retry.retryCount === MAX_RETRIES) {
|
if (retry.retryCount === MAX_RETRIES) {
|
||||||
return modelInfo.providers.find((provider) => provider.id === modelInfo.fallbackProvider)
|
return modelInfo.providers.find((provider) => provider.id === modelInfo.fallbackProvider)
|
||||||
}
|
}
|
||||||
@@ -268,9 +309,13 @@ export async function handler(
|
|||||||
.filter((provider) => !retry.excludeProviders.includes(provider.id))
|
.filter((provider) => !retry.excludeProviders.includes(provider.id))
|
||||||
.flatMap((provider) => Array<typeof provider>(provider.weight ?? 1).fill(provider))
|
.flatMap((provider) => Array<typeof provider>(provider.weight ?? 1).fill(provider))
|
||||||
|
|
||||||
// Use the last 2 characters of IP address to select a provider
|
// Use the last 4 characters of session ID to select a provider
|
||||||
const lastChars = ip.slice(-2)
|
let h = 0
|
||||||
const index = parseInt(lastChars, 16) % providers.length
|
const l = sessionId.length
|
||||||
|
for (let i = l - 4; i < l; i++) {
|
||||||
|
h = (h * 31 + sessionId.charCodeAt(i)) | 0 // 32-bit int
|
||||||
|
}
|
||||||
|
const index = (h >>> 0) % providers.length // make unsigned + range 0..length-1
|
||||||
return providers[index || 0]
|
return providers[index || 0]
|
||||||
})()
|
})()
|
||||||
|
|
||||||
@@ -283,6 +328,7 @@ export async function handler(
|
|||||||
...(() => {
|
...(() => {
|
||||||
const format = zenData.providers[provider.id].format
|
const format = zenData.providers[provider.id].format
|
||||||
if (format === "anthropic") return anthropicHelper
|
if (format === "anthropic") return anthropicHelper
|
||||||
|
if (format === "google") return googleHelper
|
||||||
if (format === "openai") return openaiHelper
|
if (format === "openai") return openaiHelper
|
||||||
return oaCompatHelper
|
return oaCompatHelper
|
||||||
})(),
|
})(),
|
||||||
@@ -291,7 +337,7 @@ export async function handler(
|
|||||||
|
|
||||||
async function authenticate(modelInfo: ModelInfo, providerInfo: ProviderInfo) {
|
async function authenticate(modelInfo: ModelInfo, providerInfo: ProviderInfo) {
|
||||||
const apiKey = opts.parseApiKey(input.request.headers)
|
const apiKey = opts.parseApiKey(input.request.headers)
|
||||||
if (!apiKey) {
|
if (!apiKey || apiKey === "public") {
|
||||||
if (modelInfo.allowAnonymous) return
|
if (modelInfo.allowAnonymous) return
|
||||||
throw new AuthError("Missing API key.")
|
throw new AuthError("Missing API key.")
|
||||||
}
|
}
|
||||||
@@ -409,9 +455,14 @@ export async function handler(
|
|||||||
providerInfo.apiKey = authInfo.provider.credentials
|
providerInfo.apiKey = authInfo.provider.credentials
|
||||||
}
|
}
|
||||||
|
|
||||||
async function trackUsage(authInfo: AuthInfo, modelInfo: ModelInfo, providerInfo: ProviderInfo, usage: any) {
|
async function trackUsage(
|
||||||
|
authInfo: AuthInfo,
|
||||||
|
modelInfo: ModelInfo,
|
||||||
|
providerInfo: ProviderInfo,
|
||||||
|
usageInfo: UsageInfo,
|
||||||
|
) {
|
||||||
const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } =
|
const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } =
|
||||||
providerInfo.normalizeUsage(usage)
|
usageInfo
|
||||||
|
|
||||||
const modelCost =
|
const modelCost =
|
||||||
modelInfo.cost200K &&
|
modelInfo.cost200K &&
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export const anthropicHelper = {
|
|||||||
service_tier: "standard_only",
|
service_tier: "standard_only",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
streamSeparator: "\n\n",
|
||||||
createUsageParser: () => {
|
createUsageParser: () => {
|
||||||
let usage: Usage
|
let usage: Usage
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { ProviderHelper } from "./provider"
|
||||||
|
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
promptTokenCount: 11453,
|
||||||
|
candidatesTokenCount: 71,
|
||||||
|
totalTokenCount: 11625,
|
||||||
|
cachedContentTokenCount: 8100,
|
||||||
|
promptTokensDetails: [
|
||||||
|
{modality: "TEXT",tokenCount: 11453}
|
||||||
|
],
|
||||||
|
cacheTokensDetails: [
|
||||||
|
{modality: "TEXT",tokenCount: 8100}
|
||||||
|
],
|
||||||
|
thoughtsTokenCount: 101
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
type Usage = {
|
||||||
|
promptTokenCount?: number
|
||||||
|
candidatesTokenCount?: number
|
||||||
|
totalTokenCount?: number
|
||||||
|
cachedContentTokenCount?: number
|
||||||
|
promptTokensDetails?: { modality: string; tokenCount: number }[]
|
||||||
|
cacheTokensDetails?: { modality: string; tokenCount: number }[]
|
||||||
|
thoughtsTokenCount?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const googleHelper = {
|
||||||
|
format: "google",
|
||||||
|
modifyUrl: (providerApi: string, model?: string, isStream?: boolean) =>
|
||||||
|
`${providerApi}/models/${model}:${isStream ? "streamGenerateContent?alt=sse" : "generateContent"}`,
|
||||||
|
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
|
||||||
|
headers.set("x-goog-api-key", apiKey)
|
||||||
|
},
|
||||||
|
modifyBody: (body: Record<string, any>) => {
|
||||||
|
return body
|
||||||
|
},
|
||||||
|
streamSeparator: "\r\n\r\n",
|
||||||
|
createUsageParser: () => {
|
||||||
|
let usage: Usage
|
||||||
|
|
||||||
|
return {
|
||||||
|
parse: (chunk: string) => {
|
||||||
|
if (!chunk.startsWith("data: ")) return
|
||||||
|
|
||||||
|
let json
|
||||||
|
try {
|
||||||
|
json = JSON.parse(chunk.slice(6)) as { usageMetadata?: Usage }
|
||||||
|
} catch (e) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!json.usageMetadata) return
|
||||||
|
usage = json.usageMetadata
|
||||||
|
},
|
||||||
|
retrieve: () => usage,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
normalizeUsage: (usage: Usage) => {
|
||||||
|
const inputTokens = usage.promptTokenCount ?? 0
|
||||||
|
const outputTokens = usage.candidatesTokenCount ?? 0
|
||||||
|
const reasoningTokens = usage.thoughtsTokenCount ?? 0
|
||||||
|
const cacheReadTokens = usage.cachedContentTokenCount ?? 0
|
||||||
|
return {
|
||||||
|
inputTokens: inputTokens - cacheReadTokens,
|
||||||
|
outputTokens,
|
||||||
|
reasoningTokens,
|
||||||
|
cacheReadTokens,
|
||||||
|
cacheWrite5mTokens: undefined,
|
||||||
|
cacheWrite1hTokens: undefined,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
} satisfies ProviderHelper
|
||||||
@@ -33,6 +33,7 @@ export const oaCompatHelper = {
|
|||||||
...(body.stream ? { stream_options: { include_usage: true } } : {}),
|
...(body.stream ? { stream_options: { include_usage: true } } : {}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
streamSeparator: "\n\n",
|
||||||
createUsageParser: () => {
|
createUsageParser: () => {
|
||||||
let usage: Usage
|
let usage: Usage
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const openaiHelper = {
|
|||||||
modifyBody: (body: Record<string, any>) => {
|
modifyBody: (body: Record<string, any>) => {
|
||||||
return body
|
return body
|
||||||
},
|
},
|
||||||
|
streamSeparator: "\n\n",
|
||||||
createUsageParser: () => {
|
createUsageParser: () => {
|
||||||
let usage: Usage
|
let usage: Usage
|
||||||
|
|
||||||
|
|||||||
@@ -24,23 +24,26 @@ import {
|
|||||||
toOaCompatibleResponse,
|
toOaCompatibleResponse,
|
||||||
} from "./openai-compatible"
|
} from "./openai-compatible"
|
||||||
|
|
||||||
|
export type UsageInfo = {
|
||||||
|
inputTokens: number
|
||||||
|
outputTokens: number
|
||||||
|
reasoningTokens?: number
|
||||||
|
cacheReadTokens?: number
|
||||||
|
cacheWrite5mTokens?: number
|
||||||
|
cacheWrite1hTokens?: number
|
||||||
|
}
|
||||||
|
|
||||||
export type ProviderHelper = {
|
export type ProviderHelper = {
|
||||||
format: ZenData.Format
|
format: ZenData.Format
|
||||||
modifyUrl: (providerApi: string) => string
|
modifyUrl: (providerApi: string, model?: string, isStream?: boolean) => string
|
||||||
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => void
|
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => void
|
||||||
modifyBody: (body: Record<string, any>) => Record<string, any>
|
modifyBody: (body: Record<string, any>) => Record<string, any>
|
||||||
|
streamSeparator: string
|
||||||
createUsageParser: () => {
|
createUsageParser: () => {
|
||||||
parse: (chunk: string) => void
|
parse: (chunk: string) => void
|
||||||
retrieve: () => any
|
retrieve: () => any
|
||||||
}
|
}
|
||||||
normalizeUsage: (usage: any) => {
|
normalizeUsage: (usage: any) => UsageInfo
|
||||||
inputTokens: number
|
|
||||||
outputTokens: number
|
|
||||||
reasoningTokens?: number
|
|
||||||
cacheReadTokens?: number
|
|
||||||
cacheWrite5mTokens?: number
|
|
||||||
cacheWrite1hTokens?: number
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CommonMessage {
|
export interface CommonMessage {
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Database, eq, sql } from "@opencode-ai/console-core/drizzle/index.js"
|
||||||
|
import { IpTable } from "@opencode-ai/console-core/schema/ip.sql.js"
|
||||||
|
import { UsageInfo } from "./provider/provider"
|
||||||
|
|
||||||
|
export function createTrialLimiter(limit: number | undefined, ip: string) {
|
||||||
|
if (!limit) return
|
||||||
|
if (!ip) return
|
||||||
|
|
||||||
|
let trial: boolean
|
||||||
|
|
||||||
|
return {
|
||||||
|
isTrial: async () => {
|
||||||
|
const data = await Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.select({
|
||||||
|
usage: IpTable.usage,
|
||||||
|
})
|
||||||
|
.from(IpTable)
|
||||||
|
.where(eq(IpTable.ip, ip))
|
||||||
|
.then((rows) => rows[0]),
|
||||||
|
)
|
||||||
|
|
||||||
|
trial = (data?.usage ?? 0) < limit
|
||||||
|
return trial
|
||||||
|
},
|
||||||
|
track: async (usageInfo: UsageInfo) => {
|
||||||
|
if (!trial) return
|
||||||
|
const usage =
|
||||||
|
usageInfo.inputTokens +
|
||||||
|
usageInfo.outputTokens +
|
||||||
|
(usageInfo.reasoningTokens ?? 0) +
|
||||||
|
(usageInfo.cacheReadTokens ?? 0) +
|
||||||
|
(usageInfo.cacheWrite5mTokens ?? 0) +
|
||||||
|
(usageInfo.cacheWrite1hTokens ?? 0)
|
||||||
|
await Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.insert(IpTable)
|
||||||
|
.values({ ip, usage })
|
||||||
|
.onDuplicateKeyUpdate({ set: { usage: sql`${IpTable.usage} + ${usage}` } }),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,5 +5,7 @@ export function POST(input: APIEvent) {
|
|||||||
return handler(input, {
|
return handler(input, {
|
||||||
format: "oa-compat",
|
format: "oa-compat",
|
||||||
parseApiKey: (headers: Headers) => headers.get("authorization")?.split(" ")[1],
|
parseApiKey: (headers: Headers) => headers.get("authorization")?.split(" ")[1],
|
||||||
|
parseModel: (url: string, body: any) => body.model,
|
||||||
|
parseIsStream: (url: string, body: any) => !!body.stream,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,5 +5,7 @@ export function POST(input: APIEvent) {
|
|||||||
return handler(input, {
|
return handler(input, {
|
||||||
format: "anthropic",
|
format: "anthropic",
|
||||||
parseApiKey: (headers: Headers) => headers.get("x-api-key") ?? undefined,
|
parseApiKey: (headers: Headers) => headers.get("x-api-key") ?? undefined,
|
||||||
|
parseModel: (url: string, body: any) => body.model,
|
||||||
|
parseIsStream: (url: string, body: any) => !!body.stream,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import type { APIEvent } from "@solidjs/start/server"
|
||||||
|
import { handler } from "~/routes/zen/util/handler"
|
||||||
|
|
||||||
|
export function POST(input: APIEvent) {
|
||||||
|
return handler(input, {
|
||||||
|
format: "google",
|
||||||
|
parseApiKey: (headers: Headers) => headers.get("x-goog-api-key") ?? undefined,
|
||||||
|
parseModel: (url: string, body: any) => url.split("/").pop()?.split(":")?.[0] ?? "",
|
||||||
|
parseIsStream: (url: string, body: any) =>
|
||||||
|
// ie. url: https://opencode.ai/zen/v1/models/gemini-3-pro:streamGenerateContent?alt=sse'
|
||||||
|
url.split("/").pop()?.split(":")?.[1]?.startsWith("streamGenerateContent") ?? false,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -5,5 +5,7 @@ export function POST(input: APIEvent) {
|
|||||||
return handler(input, {
|
return handler(input, {
|
||||||
format: "openai",
|
format: "openai",
|
||||||
parseApiKey: (headers: Headers) => headers.get("authorization")?.split(" ")[1],
|
parseApiKey: (headers: Headers) => headers.get("authorization")?.split(" ")[1],
|
||||||
|
parseModel: (url: string, body: any) => body.model,
|
||||||
|
parseIsStream: (url: string, body: any) => !!body.stream,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"types": ["vinxi/types/client"],
|
"types": ["vite/client"],
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"~/*": ["./src/*"]
|
"~/*": ["./src/*"]
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { defineConfig, PluginOption } from "vite"
|
||||||
|
import { solidStart } from "@solidjs/start/config"
|
||||||
|
import { nitro } from "nitro/vite"
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
solidStart() as PluginOption,
|
||||||
|
nitro({
|
||||||
|
compatibilityDate: "2024-09-19",
|
||||||
|
preset: "cloudflare_module",
|
||||||
|
cloudflare: {
|
||||||
|
nodeCompat: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
server: {
|
||||||
|
allowedHosts: true,
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
external: ["cloudflare:workers"],
|
||||||
|
},
|
||||||
|
minify: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
CREATE TABLE `ip` (
|
||||||
|
`ip` varchar(45) NOT NULL,
|
||||||
|
`time_created` timestamp(3) NOT NULL DEFAULT (now()),
|
||||||
|
`time_updated` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
|
`time_deleted` timestamp(3),
|
||||||
|
`usage` int,
|
||||||
|
CONSTRAINT `ip_ip_pk` PRIMARY KEY(`ip`)
|
||||||
|
);
|
||||||
@@ -0,0 +1,981 @@
|
|||||||
|
{
|
||||||
|
"version": "5",
|
||||||
|
"dialect": "mysql",
|
||||||
|
"id": "9d5d9885-7ec5-45f6-ac53-45a8e25dede7",
|
||||||
|
"prevId": "8b7fa839-a088-408e-84a4-1a07325c0290",
|
||||||
|
"tables": {
|
||||||
|
"account": {
|
||||||
|
"name": "account",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"time_updated": {
|
||||||
|
"name": "time_updated",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"account_id_pk": {
|
||||||
|
"name": "account_id_pk",
|
||||||
|
"columns": ["id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"name": "auth",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"time_updated": {
|
||||||
|
"name": "time_updated",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"provider": {
|
||||||
|
"name": "provider",
|
||||||
|
"type": "enum('email','github','google')",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"subject": {
|
||||||
|
"name": "subject",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"account_id": {
|
||||||
|
"name": "account_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"provider": {
|
||||||
|
"name": "provider",
|
||||||
|
"columns": ["provider", "subject"],
|
||||||
|
"isUnique": true
|
||||||
|
},
|
||||||
|
"account_id": {
|
||||||
|
"name": "account_id",
|
||||||
|
"columns": ["account_id"],
|
||||||
|
"isUnique": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"auth_id_pk": {
|
||||||
|
"name": "auth_id_pk",
|
||||||
|
"columns": ["id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"billing": {
|
||||||
|
"name": "billing",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"time_updated": {
|
||||||
|
"name": "time_updated",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"customer_id": {
|
||||||
|
"name": "customer_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"payment_method_id": {
|
||||||
|
"name": "payment_method_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"payment_method_type": {
|
||||||
|
"name": "payment_method_type",
|
||||||
|
"type": "varchar(32)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"payment_method_last4": {
|
||||||
|
"name": "payment_method_last4",
|
||||||
|
"type": "varchar(4)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"balance": {
|
||||||
|
"name": "balance",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"monthly_limit": {
|
||||||
|
"name": "monthly_limit",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"monthly_usage": {
|
||||||
|
"name": "monthly_usage",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"time_monthly_usage_updated": {
|
||||||
|
"name": "time_monthly_usage_updated",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"reload": {
|
||||||
|
"name": "reload",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"reload_trigger": {
|
||||||
|
"name": "reload_trigger",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"reload_amount": {
|
||||||
|
"name": "reload_amount",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"reload_error": {
|
||||||
|
"name": "reload_error",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"time_reload_error": {
|
||||||
|
"name": "time_reload_error",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"time_reload_locked_till": {
|
||||||
|
"name": "time_reload_locked_till",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"global_customer_id": {
|
||||||
|
"name": "global_customer_id",
|
||||||
|
"columns": ["customer_id"],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"billing_workspace_id_id_pk": {
|
||||||
|
"name": "billing_workspace_id_id_pk",
|
||||||
|
"columns": ["workspace_id", "id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"payment": {
|
||||||
|
"name": "payment",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"time_updated": {
|
||||||
|
"name": "time_updated",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"customer_id": {
|
||||||
|
"name": "customer_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"invoice_id": {
|
||||||
|
"name": "invoice_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"payment_id": {
|
||||||
|
"name": "payment_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"amount": {
|
||||||
|
"name": "amount",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"time_refunded": {
|
||||||
|
"name": "time_refunded",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"payment_workspace_id_id_pk": {
|
||||||
|
"name": "payment_workspace_id_id_pk",
|
||||||
|
"columns": ["workspace_id", "id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"usage": {
|
||||||
|
"name": "usage",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"time_updated": {
|
||||||
|
"name": "time_updated",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"name": "model",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"provider": {
|
||||||
|
"name": "provider",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"input_tokens": {
|
||||||
|
"name": "input_tokens",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"output_tokens": {
|
||||||
|
"name": "output_tokens",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"reasoning_tokens": {
|
||||||
|
"name": "reasoning_tokens",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"cache_read_tokens": {
|
||||||
|
"name": "cache_read_tokens",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"cache_write_5m_tokens": {
|
||||||
|
"name": "cache_write_5m_tokens",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"cache_write_1h_tokens": {
|
||||||
|
"name": "cache_write_1h_tokens",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"cost": {
|
||||||
|
"name": "cost",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"key_id": {
|
||||||
|
"name": "key_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"usage_workspace_id_id_pk": {
|
||||||
|
"name": "usage_workspace_id_id_pk",
|
||||||
|
"columns": ["workspace_id", "id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"ip": {
|
||||||
|
"name": "ip",
|
||||||
|
"columns": {
|
||||||
|
"ip": {
|
||||||
|
"name": "ip",
|
||||||
|
"type": "varchar(45)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"time_updated": {
|
||||||
|
"name": "time_updated",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"usage": {
|
||||||
|
"name": "usage",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"ip_ip_pk": {
|
||||||
|
"name": "ip_ip_pk",
|
||||||
|
"columns": ["ip"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"name": "key",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"time_updated": {
|
||||||
|
"name": "time_updated",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"name": "key",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"time_used": {
|
||||||
|
"name": "time_used",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"global_key": {
|
||||||
|
"name": "global_key",
|
||||||
|
"columns": ["key"],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"key_workspace_id_id_pk": {
|
||||||
|
"name": "key_workspace_id_id_pk",
|
||||||
|
"columns": ["workspace_id", "id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"name": "model",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"time_updated": {
|
||||||
|
"name": "time_updated",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"name": "model",
|
||||||
|
"type": "varchar(64)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"model_workspace_model": {
|
||||||
|
"name": "model_workspace_model",
|
||||||
|
"columns": ["workspace_id", "model"],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"model_workspace_id_id_pk": {
|
||||||
|
"name": "model_workspace_id_id_pk",
|
||||||
|
"columns": ["workspace_id", "id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"provider": {
|
||||||
|
"name": "provider",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"time_updated": {
|
||||||
|
"name": "time_updated",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"provider": {
|
||||||
|
"name": "provider",
|
||||||
|
"type": "varchar(64)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"credentials": {
|
||||||
|
"name": "credentials",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"workspace_provider": {
|
||||||
|
"name": "workspace_provider",
|
||||||
|
"columns": ["workspace_id", "provider"],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"provider_workspace_id_id_pk": {
|
||||||
|
"name": "provider_workspace_id_id_pk",
|
||||||
|
"columns": ["workspace_id", "id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"name": "user",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"time_updated": {
|
||||||
|
"name": "time_updated",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"account_id": {
|
||||||
|
"name": "account_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"time_seen": {
|
||||||
|
"name": "time_seen",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"name": "color",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "enum('admin','member')",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"monthly_limit": {
|
||||||
|
"name": "monthly_limit",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"monthly_usage": {
|
||||||
|
"name": "monthly_usage",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"time_monthly_usage_updated": {
|
||||||
|
"name": "time_monthly_usage_updated",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"user_account_id": {
|
||||||
|
"name": "user_account_id",
|
||||||
|
"columns": ["workspace_id", "account_id"],
|
||||||
|
"isUnique": true
|
||||||
|
},
|
||||||
|
"user_email": {
|
||||||
|
"name": "user_email",
|
||||||
|
"columns": ["workspace_id", "email"],
|
||||||
|
"isUnique": true
|
||||||
|
},
|
||||||
|
"global_account_id": {
|
||||||
|
"name": "global_account_id",
|
||||||
|
"columns": ["account_id"],
|
||||||
|
"isUnique": false
|
||||||
|
},
|
||||||
|
"global_email": {
|
||||||
|
"name": "global_email",
|
||||||
|
"columns": ["email"],
|
||||||
|
"isUnique": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"user_workspace_id_id_pk": {
|
||||||
|
"name": "user_workspace_id_id_pk",
|
||||||
|
"columns": ["workspace_id", "id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"workspace": {
|
||||||
|
"name": "workspace",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"time_updated": {
|
||||||
|
"name": "time_updated",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp(3)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"columns": ["slug"],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"columns": ["id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"tables": {},
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -267,6 +267,13 @@
|
|||||||
"when": 1761928273807,
|
"when": 1761928273807,
|
||||||
"tag": "0037_messy_jackal",
|
"tag": "0037_messy_jackal",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 38,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1764110043942,
|
||||||
|
"tag": "0038_famous_magik",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.0.56",
|
"version": "1.0.120",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -30,6 +30,7 @@
|
|||||||
"update-models": "script/update-models.ts",
|
"update-models": "script/update-models.ts",
|
||||||
"promote-models-to-dev": "script/promote-models.ts dev",
|
"promote-models-to-dev": "script/promote-models.ts dev",
|
||||||
"promote-models-to-prod": "script/promote-models.ts production",
|
"promote-models-to-prod": "script/promote-models.ts production",
|
||||||
|
"pull-models-from-dev": "script/pull-models.ts dev",
|
||||||
"typecheck": "tsgo --noEmit"
|
"typecheck": "tsgo --noEmit"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -11,20 +11,21 @@ const root = path.resolve(process.cwd(), "..", "..", "..")
|
|||||||
|
|
||||||
// read the secret
|
// read the secret
|
||||||
const ret = await $`bun sst secret list`.cwd(root).text()
|
const ret = await $`bun sst secret list`.cwd(root).text()
|
||||||
const value1 = ret
|
const lines = ret.split("\n")
|
||||||
.split("\n")
|
const value1 = lines.find((line) => line.startsWith("ZEN_MODELS1"))?.split("=")[1]
|
||||||
.find((line) => line.startsWith("ZEN_MODELS1"))
|
const value2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
|
||||||
?.split("=")[1]
|
const value3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
|
||||||
const value2 = ret
|
const value4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
|
||||||
.split("\n")
|
|
||||||
.find((line) => line.startsWith("ZEN_MODELS2"))
|
|
||||||
?.split("=")[1]
|
|
||||||
if (!value1) throw new Error("ZEN_MODELS1 not found")
|
if (!value1) throw new Error("ZEN_MODELS1 not found")
|
||||||
if (!value2) throw new Error("ZEN_MODELS2 not found")
|
if (!value2) throw new Error("ZEN_MODELS2 not found")
|
||||||
|
if (!value3) throw new Error("ZEN_MODELS3 not found")
|
||||||
|
if (!value4) throw new Error("ZEN_MODELS4 not found")
|
||||||
|
|
||||||
// validate value
|
// validate value
|
||||||
ZenData.validate(JSON.parse(value1 + value2))
|
ZenData.validate(JSON.parse(value1 + value2 + value3 + value4))
|
||||||
|
|
||||||
// update the secret
|
// update the secret
|
||||||
await $`bun sst secret set ZEN_MODELS1 ${value1} --stage ${stage}`
|
await $`bun sst secret set ZEN_MODELS1 ${value1} --stage ${stage}`
|
||||||
await $`bun sst secret set ZEN_MODELS2 ${value2} --stage ${stage}`
|
await $`bun sst secret set ZEN_MODELS2 ${value2} --stage ${stage}`
|
||||||
|
await $`bun sst secret set ZEN_MODELS3 ${value3} --stage ${stage}`
|
||||||
|
await $`bun sst secret set ZEN_MODELS4 ${value4} --stage ${stage}`
|
||||||
|
|||||||
Executable
+31
@@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
import { $ } from "bun"
|
||||||
|
import path from "path"
|
||||||
|
import { ZenData } from "../src/model"
|
||||||
|
|
||||||
|
const stage = process.argv[2]
|
||||||
|
if (!stage) throw new Error("Stage is required")
|
||||||
|
|
||||||
|
const root = path.resolve(process.cwd(), "..", "..", "..")
|
||||||
|
|
||||||
|
// read the secret
|
||||||
|
const ret = await $`bun sst secret list --stage ${stage}`.cwd(root).text()
|
||||||
|
const lines = ret.split("\n")
|
||||||
|
const value1 = lines.find((line) => line.startsWith("ZEN_MODELS1"))?.split("=")[1]
|
||||||
|
const value2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
|
||||||
|
const value3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
|
||||||
|
const value4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
|
||||||
|
if (!value1) throw new Error("ZEN_MODELS1 not found")
|
||||||
|
if (!value2) throw new Error("ZEN_MODELS2 not found")
|
||||||
|
if (!value3) throw new Error("ZEN_MODELS3 not found")
|
||||||
|
if (!value4) throw new Error("ZEN_MODELS4 not found")
|
||||||
|
|
||||||
|
// validate value
|
||||||
|
ZenData.validate(JSON.parse(value1 + value2 + value3 + value4))
|
||||||
|
|
||||||
|
// update the secret
|
||||||
|
await $`bun sst secret set ZEN_MODELS1 ${value1}`
|
||||||
|
await $`bun sst secret set ZEN_MODELS2 ${value2}`
|
||||||
|
await $`bun sst secret set ZEN_MODELS3 ${value3}`
|
||||||
|
await $`bun sst secret set ZEN_MODELS4 ${value4}`
|
||||||
@@ -9,21 +9,20 @@ const root = path.resolve(process.cwd(), "..", "..", "..")
|
|||||||
const models = await $`bun sst secret list`.cwd(root).text()
|
const models = await $`bun sst secret list`.cwd(root).text()
|
||||||
|
|
||||||
// read the line starting with "ZEN_MODELS"
|
// read the line starting with "ZEN_MODELS"
|
||||||
const oldValue1 = models
|
const lines = models.split("\n")
|
||||||
.split("\n")
|
const oldValue1 = lines.find((line) => line.startsWith("ZEN_MODELS1"))?.split("=")[1]
|
||||||
.find((line) => line.startsWith("ZEN_MODELS1"))
|
const oldValue2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
|
||||||
?.split("=")[1]
|
const oldValue3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
|
||||||
const oldValue2 = models
|
const oldValue4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
|
||||||
.split("\n")
|
|
||||||
.find((line) => line.startsWith("ZEN_MODELS2"))
|
|
||||||
?.split("=")[1]
|
|
||||||
if (!oldValue1) throw new Error("ZEN_MODELS1 not found")
|
if (!oldValue1) throw new Error("ZEN_MODELS1 not found")
|
||||||
if (!oldValue2) throw new Error("ZEN_MODELS2 not found")
|
if (!oldValue2) throw new Error("ZEN_MODELS2 not found")
|
||||||
|
if (!oldValue3) throw new Error("ZEN_MODELS3 not found")
|
||||||
|
if (!oldValue4) throw new Error("ZEN_MODELS4 not found")
|
||||||
|
|
||||||
// store the prettified json to a temp file
|
// store the prettified json to a temp file
|
||||||
const filename = `models-${Date.now()}.json`
|
const filename = `models-${Date.now()}.json`
|
||||||
const tempFile = Bun.file(path.join(os.tmpdir(), filename))
|
const tempFile = Bun.file(path.join(os.tmpdir(), filename))
|
||||||
await tempFile.write(JSON.stringify(JSON.parse(oldValue1 + oldValue2), null, 2))
|
await tempFile.write(JSON.stringify(JSON.parse(oldValue1 + oldValue2 + oldValue3 + oldValue4), null, 2))
|
||||||
console.log("tempFile", tempFile.name)
|
console.log("tempFile", tempFile.name)
|
||||||
|
|
||||||
// open temp file in vim and read the file on close
|
// open temp file in vim and read the file on close
|
||||||
@@ -32,6 +31,12 @@ const newValue = JSON.stringify(JSON.parse(await tempFile.text()))
|
|||||||
ZenData.validate(JSON.parse(newValue))
|
ZenData.validate(JSON.parse(newValue))
|
||||||
|
|
||||||
// update the secret
|
// update the secret
|
||||||
const mid = Math.floor(newValue.length / 2)
|
const chunk = Math.ceil(newValue.length / 4)
|
||||||
await $`bun sst secret set ZEN_MODELS1 ${newValue.slice(0, mid)}`
|
const newValue1 = newValue.slice(0, chunk)
|
||||||
await $`bun sst secret set ZEN_MODELS2 ${newValue.slice(mid)}`
|
const newValue2 = newValue.slice(chunk, chunk * 2)
|
||||||
|
const newValue3 = newValue.slice(chunk * 2, chunk * 3)
|
||||||
|
const newValue4 = newValue.slice(chunk * 3)
|
||||||
|
await $`bun sst secret set ZEN_MODELS1 ${newValue1}`
|
||||||
|
await $`bun sst secret set ZEN_MODELS2 ${newValue2}`
|
||||||
|
await $`bun sst secret set ZEN_MODELS3 ${newValue3}`
|
||||||
|
await $`bun sst secret set ZEN_MODELS4 ${newValue4}`
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export namespace AWS {
|
|||||||
to: z.string(),
|
to: z.string(),
|
||||||
subject: z.string(),
|
subject: z.string(),
|
||||||
body: z.string(),
|
body: z.string(),
|
||||||
|
replyTo: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
async (input) => {
|
async (input) => {
|
||||||
const res = await createClient().fetch("https://email.us-east-1.amazonaws.com/v2/email/outbound-emails", {
|
const res = await createClient().fetch("https://email.us-east-1.amazonaws.com/v2/email/outbound-emails", {
|
||||||
@@ -35,6 +36,7 @@ export namespace AWS {
|
|||||||
Destination: {
|
Destination: {
|
||||||
ToAddresses: [input.to],
|
ToAddresses: [input.to],
|
||||||
},
|
},
|
||||||
|
...(input.replyTo && { ReplyToAddresses: [input.replyTo] }),
|
||||||
Content: {
|
Content: {
|
||||||
Simple: {
|
Simple: {
|
||||||
Subject: {
|
Subject: {
|
||||||
|
|||||||
@@ -57,14 +57,15 @@ export namespace Billing {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const usages = async () => {
|
export const usages = async (page = 0, pageSize = 50) => {
|
||||||
return await Database.use((tx) =>
|
return await Database.use((tx) =>
|
||||||
tx
|
tx
|
||||||
.select()
|
.select()
|
||||||
.from(UsageTable)
|
.from(UsageTable)
|
||||||
.where(eq(UsageTable.workspaceID, Actor.workspace()))
|
.where(eq(UsageTable.workspaceID, Actor.workspace()))
|
||||||
.orderBy(sql`${UsageTable.timeCreated} DESC`)
|
.orderBy(sql`${UsageTable.timeCreated} DESC`)
|
||||||
.limit(100),
|
.limit(pageSize)
|
||||||
|
.offset(page * pageSize),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Actor } from "./actor"
|
|||||||
import { Resource } from "@opencode-ai/console-resource"
|
import { Resource } from "@opencode-ai/console-resource"
|
||||||
|
|
||||||
export namespace ZenData {
|
export namespace ZenData {
|
||||||
const FormatSchema = z.enum(["anthropic", "openai", "oa-compat"])
|
const FormatSchema = z.enum(["anthropic", "google", "openai", "oa-compat"])
|
||||||
export type Format = z.infer<typeof FormatSchema>
|
export type Format = z.infer<typeof FormatSchema>
|
||||||
|
|
||||||
const ModelCostSchema = z.object({
|
const ModelCostSchema = z.object({
|
||||||
@@ -24,6 +24,12 @@ export namespace ZenData {
|
|||||||
cost: ModelCostSchema,
|
cost: ModelCostSchema,
|
||||||
cost200K: ModelCostSchema.optional(),
|
cost200K: ModelCostSchema.optional(),
|
||||||
allowAnonymous: z.boolean().optional(),
|
allowAnonymous: z.boolean().optional(),
|
||||||
|
trial: z
|
||||||
|
.object({
|
||||||
|
limit: z.number(),
|
||||||
|
provider: z.string(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
rateLimit: z.number().optional(),
|
rateLimit: z.number().optional(),
|
||||||
fallbackProvider: z.string().optional(),
|
fallbackProvider: z.string().optional(),
|
||||||
providers: z.array(
|
providers: z.array(
|
||||||
@@ -32,6 +38,7 @@ export namespace ZenData {
|
|||||||
model: z.string(),
|
model: z.string(),
|
||||||
weight: z.number().optional(),
|
weight: z.number().optional(),
|
||||||
disabled: z.boolean().optional(),
|
disabled: z.boolean().optional(),
|
||||||
|
storeModel: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
@@ -53,7 +60,9 @@ export namespace ZenData {
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const list = fn(z.void(), () => {
|
export const list = fn(z.void(), () => {
|
||||||
const json = JSON.parse(Resource.ZEN_MODELS1.value + Resource.ZEN_MODELS2.value)
|
const json = JSON.parse(
|
||||||
|
Resource.ZEN_MODELS1.value + Resource.ZEN_MODELS2.value + Resource.ZEN_MODELS3.value + Resource.ZEN_MODELS4.value,
|
||||||
|
)
|
||||||
return ModelsSchema.parse(json)
|
return ModelsSchema.parse(json)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { mysqlTable, int, primaryKey, varchar } from "drizzle-orm/mysql-core"
|
||||||
|
import { timestamps } from "../drizzle/types"
|
||||||
|
|
||||||
|
export const IpTable = mysqlTable(
|
||||||
|
"ip",
|
||||||
|
{
|
||||||
|
ip: varchar("ip", { length: 45 }).notNull(),
|
||||||
|
...timestamps,
|
||||||
|
usage: int("usage"),
|
||||||
|
},
|
||||||
|
(table) => [primaryKey({ columns: [table.ip] })],
|
||||||
|
)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user