Julien Simon commited on
Commit
324d099
·
1 Parent(s): 80d8ac1

Initial deployment

Browse files
Files changed (9) hide show
  1. .dockerignore +5 -0
  2. Dockerfile +11 -0
  3. README.md +10 -4
  4. package-lock.json +839 -0
  5. package.json +14 -0
  6. public/app.js +266 -0
  7. public/index.html +56 -0
  8. public/style.css +348 -0
  9. server.js +289 -0
.dockerignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ node_modules/
2
+ .env*
3
+ .git/
4
+ .claude/
5
+ .DS_Store
Dockerfile ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:20-alpine
2
+ WORKDIR /app
3
+ COPY package.json package-lock.json ./
4
+ RUN npm ci --only=production
5
+ COPY server.js ./
6
+ COPY public ./public
7
+ ENV PORT=7860
8
+ EXPOSE 7860
9
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
10
+ CMD wget --no-verbose --tries=1 --spider http://localhost:7860/ || exit 1
11
+ CMD ["node", "server.js"]
README.md CHANGED
@@ -1,10 +1,16 @@
1
  ---
2
  title: Trinity Code Reviewer
3
- emoji: 🌖
4
- colorFrom: pink
5
- colorTo: indigo
6
  sdk: docker
 
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
1
  ---
2
  title: Trinity Code Reviewer
3
+ emoji: 🔍
4
+ colorFrom: purple
5
+ colorTo: blue
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
9
+ license: mit
10
  ---
11
 
12
+ # Trinity Code Reviewer
13
+ AI-powered code review using Trinity Large Preview via OpenRouter.
14
+
15
+ Paste a public GitHub file URL and get streaming analysis across 5 categories:
16
+ Summary, Code Quality, Performance, Security, and Suggestions.
package-lock.json ADDED
@@ -0,0 +1,839 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "trinity-code-reviewer",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "trinity-code-reviewer",
9
+ "version": "1.0.0",
10
+ "dependencies": {
11
+ "dotenv": "^16.4.7",
12
+ "express": "^5.1.0"
13
+ }
14
+ },
15
+ "node_modules/accepts": {
16
+ "version": "2.0.0",
17
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
18
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
19
+ "license": "MIT",
20
+ "dependencies": {
21
+ "mime-types": "^3.0.0",
22
+ "negotiator": "^1.0.0"
23
+ },
24
+ "engines": {
25
+ "node": ">= 0.6"
26
+ }
27
+ },
28
+ "node_modules/body-parser": {
29
+ "version": "2.2.2",
30
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
31
+ "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
32
+ "license": "MIT",
33
+ "dependencies": {
34
+ "bytes": "^3.1.2",
35
+ "content-type": "^1.0.5",
36
+ "debug": "^4.4.3",
37
+ "http-errors": "^2.0.0",
38
+ "iconv-lite": "^0.7.0",
39
+ "on-finished": "^2.4.1",
40
+ "qs": "^6.14.1",
41
+ "raw-body": "^3.0.1",
42
+ "type-is": "^2.0.1"
43
+ },
44
+ "engines": {
45
+ "node": ">=18"
46
+ },
47
+ "funding": {
48
+ "type": "opencollective",
49
+ "url": "https://opencollective.com/express"
50
+ }
51
+ },
52
+ "node_modules/bytes": {
53
+ "version": "3.1.2",
54
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
55
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
56
+ "license": "MIT",
57
+ "engines": {
58
+ "node": ">= 0.8"
59
+ }
60
+ },
61
+ "node_modules/call-bind-apply-helpers": {
62
+ "version": "1.0.2",
63
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
64
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
65
+ "license": "MIT",
66
+ "dependencies": {
67
+ "es-errors": "^1.3.0",
68
+ "function-bind": "^1.1.2"
69
+ },
70
+ "engines": {
71
+ "node": ">= 0.4"
72
+ }
73
+ },
74
+ "node_modules/call-bound": {
75
+ "version": "1.0.4",
76
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
77
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
78
+ "license": "MIT",
79
+ "dependencies": {
80
+ "call-bind-apply-helpers": "^1.0.2",
81
+ "get-intrinsic": "^1.3.0"
82
+ },
83
+ "engines": {
84
+ "node": ">= 0.4"
85
+ },
86
+ "funding": {
87
+ "url": "https://github.com/sponsors/ljharb"
88
+ }
89
+ },
90
+ "node_modules/content-disposition": {
91
+ "version": "1.0.1",
92
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
93
+ "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
94
+ "license": "MIT",
95
+ "engines": {
96
+ "node": ">=18"
97
+ },
98
+ "funding": {
99
+ "type": "opencollective",
100
+ "url": "https://opencollective.com/express"
101
+ }
102
+ },
103
+ "node_modules/content-type": {
104
+ "version": "1.0.5",
105
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
106
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
107
+ "license": "MIT",
108
+ "engines": {
109
+ "node": ">= 0.6"
110
+ }
111
+ },
112
+ "node_modules/cookie": {
113
+ "version": "0.7.2",
114
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
115
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
116
+ "license": "MIT",
117
+ "engines": {
118
+ "node": ">= 0.6"
119
+ }
120
+ },
121
+ "node_modules/cookie-signature": {
122
+ "version": "1.2.2",
123
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
124
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
125
+ "license": "MIT",
126
+ "engines": {
127
+ "node": ">=6.6.0"
128
+ }
129
+ },
130
+ "node_modules/debug": {
131
+ "version": "4.4.3",
132
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
133
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
134
+ "license": "MIT",
135
+ "dependencies": {
136
+ "ms": "^2.1.3"
137
+ },
138
+ "engines": {
139
+ "node": ">=6.0"
140
+ },
141
+ "peerDependenciesMeta": {
142
+ "supports-color": {
143
+ "optional": true
144
+ }
145
+ }
146
+ },
147
+ "node_modules/depd": {
148
+ "version": "2.0.0",
149
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
150
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
151
+ "license": "MIT",
152
+ "engines": {
153
+ "node": ">= 0.8"
154
+ }
155
+ },
156
+ "node_modules/dotenv": {
157
+ "version": "16.6.1",
158
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
159
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
160
+ "license": "BSD-2-Clause",
161
+ "engines": {
162
+ "node": ">=12"
163
+ },
164
+ "funding": {
165
+ "url": "https://dotenvx.com"
166
+ }
167
+ },
168
+ "node_modules/dunder-proto": {
169
+ "version": "1.0.1",
170
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
171
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
172
+ "license": "MIT",
173
+ "dependencies": {
174
+ "call-bind-apply-helpers": "^1.0.1",
175
+ "es-errors": "^1.3.0",
176
+ "gopd": "^1.2.0"
177
+ },
178
+ "engines": {
179
+ "node": ">= 0.4"
180
+ }
181
+ },
182
+ "node_modules/ee-first": {
183
+ "version": "1.1.1",
184
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
185
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
186
+ "license": "MIT"
187
+ },
188
+ "node_modules/encodeurl": {
189
+ "version": "2.0.0",
190
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
191
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
192
+ "license": "MIT",
193
+ "engines": {
194
+ "node": ">= 0.8"
195
+ }
196
+ },
197
+ "node_modules/es-define-property": {
198
+ "version": "1.0.1",
199
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
200
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
201
+ "license": "MIT",
202
+ "engines": {
203
+ "node": ">= 0.4"
204
+ }
205
+ },
206
+ "node_modules/es-errors": {
207
+ "version": "1.3.0",
208
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
209
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
210
+ "license": "MIT",
211
+ "engines": {
212
+ "node": ">= 0.4"
213
+ }
214
+ },
215
+ "node_modules/es-object-atoms": {
216
+ "version": "1.1.1",
217
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
218
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
219
+ "license": "MIT",
220
+ "dependencies": {
221
+ "es-errors": "^1.3.0"
222
+ },
223
+ "engines": {
224
+ "node": ">= 0.4"
225
+ }
226
+ },
227
+ "node_modules/escape-html": {
228
+ "version": "1.0.3",
229
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
230
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
231
+ "license": "MIT"
232
+ },
233
+ "node_modules/etag": {
234
+ "version": "1.8.1",
235
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
236
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
237
+ "license": "MIT",
238
+ "engines": {
239
+ "node": ">= 0.6"
240
+ }
241
+ },
242
+ "node_modules/express": {
243
+ "version": "5.2.1",
244
+ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
245
+ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
246
+ "license": "MIT",
247
+ "dependencies": {
248
+ "accepts": "^2.0.0",
249
+ "body-parser": "^2.2.1",
250
+ "content-disposition": "^1.0.0",
251
+ "content-type": "^1.0.5",
252
+ "cookie": "^0.7.1",
253
+ "cookie-signature": "^1.2.1",
254
+ "debug": "^4.4.0",
255
+ "depd": "^2.0.0",
256
+ "encodeurl": "^2.0.0",
257
+ "escape-html": "^1.0.3",
258
+ "etag": "^1.8.1",
259
+ "finalhandler": "^2.1.0",
260
+ "fresh": "^2.0.0",
261
+ "http-errors": "^2.0.0",
262
+ "merge-descriptors": "^2.0.0",
263
+ "mime-types": "^3.0.0",
264
+ "on-finished": "^2.4.1",
265
+ "once": "^1.4.0",
266
+ "parseurl": "^1.3.3",
267
+ "proxy-addr": "^2.0.7",
268
+ "qs": "^6.14.0",
269
+ "range-parser": "^1.2.1",
270
+ "router": "^2.2.0",
271
+ "send": "^1.1.0",
272
+ "serve-static": "^2.2.0",
273
+ "statuses": "^2.0.1",
274
+ "type-is": "^2.0.1",
275
+ "vary": "^1.1.2"
276
+ },
277
+ "engines": {
278
+ "node": ">= 18"
279
+ },
280
+ "funding": {
281
+ "type": "opencollective",
282
+ "url": "https://opencollective.com/express"
283
+ }
284
+ },
285
+ "node_modules/finalhandler": {
286
+ "version": "2.1.1",
287
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
288
+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
289
+ "license": "MIT",
290
+ "dependencies": {
291
+ "debug": "^4.4.0",
292
+ "encodeurl": "^2.0.0",
293
+ "escape-html": "^1.0.3",
294
+ "on-finished": "^2.4.1",
295
+ "parseurl": "^1.3.3",
296
+ "statuses": "^2.0.1"
297
+ },
298
+ "engines": {
299
+ "node": ">= 18.0.0"
300
+ },
301
+ "funding": {
302
+ "type": "opencollective",
303
+ "url": "https://opencollective.com/express"
304
+ }
305
+ },
306
+ "node_modules/forwarded": {
307
+ "version": "0.2.0",
308
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
309
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
310
+ "license": "MIT",
311
+ "engines": {
312
+ "node": ">= 0.6"
313
+ }
314
+ },
315
+ "node_modules/fresh": {
316
+ "version": "2.0.0",
317
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
318
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
319
+ "license": "MIT",
320
+ "engines": {
321
+ "node": ">= 0.8"
322
+ }
323
+ },
324
+ "node_modules/function-bind": {
325
+ "version": "1.1.2",
326
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
327
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
328
+ "license": "MIT",
329
+ "funding": {
330
+ "url": "https://github.com/sponsors/ljharb"
331
+ }
332
+ },
333
+ "node_modules/get-intrinsic": {
334
+ "version": "1.3.0",
335
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
336
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
337
+ "license": "MIT",
338
+ "dependencies": {
339
+ "call-bind-apply-helpers": "^1.0.2",
340
+ "es-define-property": "^1.0.1",
341
+ "es-errors": "^1.3.0",
342
+ "es-object-atoms": "^1.1.1",
343
+ "function-bind": "^1.1.2",
344
+ "get-proto": "^1.0.1",
345
+ "gopd": "^1.2.0",
346
+ "has-symbols": "^1.1.0",
347
+ "hasown": "^2.0.2",
348
+ "math-intrinsics": "^1.1.0"
349
+ },
350
+ "engines": {
351
+ "node": ">= 0.4"
352
+ },
353
+ "funding": {
354
+ "url": "https://github.com/sponsors/ljharb"
355
+ }
356
+ },
357
+ "node_modules/get-proto": {
358
+ "version": "1.0.1",
359
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
360
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
361
+ "license": "MIT",
362
+ "dependencies": {
363
+ "dunder-proto": "^1.0.1",
364
+ "es-object-atoms": "^1.0.0"
365
+ },
366
+ "engines": {
367
+ "node": ">= 0.4"
368
+ }
369
+ },
370
+ "node_modules/gopd": {
371
+ "version": "1.2.0",
372
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
373
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
374
+ "license": "MIT",
375
+ "engines": {
376
+ "node": ">= 0.4"
377
+ },
378
+ "funding": {
379
+ "url": "https://github.com/sponsors/ljharb"
380
+ }
381
+ },
382
+ "node_modules/has-symbols": {
383
+ "version": "1.1.0",
384
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
385
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
386
+ "license": "MIT",
387
+ "engines": {
388
+ "node": ">= 0.4"
389
+ },
390
+ "funding": {
391
+ "url": "https://github.com/sponsors/ljharb"
392
+ }
393
+ },
394
+ "node_modules/hasown": {
395
+ "version": "2.0.2",
396
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
397
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
398
+ "license": "MIT",
399
+ "dependencies": {
400
+ "function-bind": "^1.1.2"
401
+ },
402
+ "engines": {
403
+ "node": ">= 0.4"
404
+ }
405
+ },
406
+ "node_modules/http-errors": {
407
+ "version": "2.0.1",
408
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
409
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
410
+ "license": "MIT",
411
+ "dependencies": {
412
+ "depd": "~2.0.0",
413
+ "inherits": "~2.0.4",
414
+ "setprototypeof": "~1.2.0",
415
+ "statuses": "~2.0.2",
416
+ "toidentifier": "~1.0.1"
417
+ },
418
+ "engines": {
419
+ "node": ">= 0.8"
420
+ },
421
+ "funding": {
422
+ "type": "opencollective",
423
+ "url": "https://opencollective.com/express"
424
+ }
425
+ },
426
+ "node_modules/iconv-lite": {
427
+ "version": "0.7.2",
428
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
429
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
430
+ "license": "MIT",
431
+ "dependencies": {
432
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
433
+ },
434
+ "engines": {
435
+ "node": ">=0.10.0"
436
+ },
437
+ "funding": {
438
+ "type": "opencollective",
439
+ "url": "https://opencollective.com/express"
440
+ }
441
+ },
442
+ "node_modules/inherits": {
443
+ "version": "2.0.4",
444
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
445
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
446
+ "license": "ISC"
447
+ },
448
+ "node_modules/ipaddr.js": {
449
+ "version": "1.9.1",
450
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
451
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
452
+ "license": "MIT",
453
+ "engines": {
454
+ "node": ">= 0.10"
455
+ }
456
+ },
457
+ "node_modules/is-promise": {
458
+ "version": "4.0.0",
459
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
460
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
461
+ "license": "MIT"
462
+ },
463
+ "node_modules/math-intrinsics": {
464
+ "version": "1.1.0",
465
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
466
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
467
+ "license": "MIT",
468
+ "engines": {
469
+ "node": ">= 0.4"
470
+ }
471
+ },
472
+ "node_modules/media-typer": {
473
+ "version": "1.1.0",
474
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
475
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
476
+ "license": "MIT",
477
+ "engines": {
478
+ "node": ">= 0.8"
479
+ }
480
+ },
481
+ "node_modules/merge-descriptors": {
482
+ "version": "2.0.0",
483
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
484
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
485
+ "license": "MIT",
486
+ "engines": {
487
+ "node": ">=18"
488
+ },
489
+ "funding": {
490
+ "url": "https://github.com/sponsors/sindresorhus"
491
+ }
492
+ },
493
+ "node_modules/mime-db": {
494
+ "version": "1.54.0",
495
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
496
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
497
+ "license": "MIT",
498
+ "engines": {
499
+ "node": ">= 0.6"
500
+ }
501
+ },
502
+ "node_modules/mime-types": {
503
+ "version": "3.0.2",
504
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
505
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
506
+ "license": "MIT",
507
+ "dependencies": {
508
+ "mime-db": "^1.54.0"
509
+ },
510
+ "engines": {
511
+ "node": ">=18"
512
+ },
513
+ "funding": {
514
+ "type": "opencollective",
515
+ "url": "https://opencollective.com/express"
516
+ }
517
+ },
518
+ "node_modules/ms": {
519
+ "version": "2.1.3",
520
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
521
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
522
+ "license": "MIT"
523
+ },
524
+ "node_modules/negotiator": {
525
+ "version": "1.0.0",
526
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
527
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
528
+ "license": "MIT",
529
+ "engines": {
530
+ "node": ">= 0.6"
531
+ }
532
+ },
533
+ "node_modules/object-inspect": {
534
+ "version": "1.13.4",
535
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
536
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
537
+ "license": "MIT",
538
+ "engines": {
539
+ "node": ">= 0.4"
540
+ },
541
+ "funding": {
542
+ "url": "https://github.com/sponsors/ljharb"
543
+ }
544
+ },
545
+ "node_modules/on-finished": {
546
+ "version": "2.4.1",
547
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
548
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
549
+ "license": "MIT",
550
+ "dependencies": {
551
+ "ee-first": "1.1.1"
552
+ },
553
+ "engines": {
554
+ "node": ">= 0.8"
555
+ }
556
+ },
557
+ "node_modules/once": {
558
+ "version": "1.4.0",
559
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
560
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
561
+ "license": "ISC",
562
+ "dependencies": {
563
+ "wrappy": "1"
564
+ }
565
+ },
566
+ "node_modules/parseurl": {
567
+ "version": "1.3.3",
568
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
569
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
570
+ "license": "MIT",
571
+ "engines": {
572
+ "node": ">= 0.8"
573
+ }
574
+ },
575
+ "node_modules/path-to-regexp": {
576
+ "version": "8.3.0",
577
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
578
+ "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
579
+ "license": "MIT",
580
+ "funding": {
581
+ "type": "opencollective",
582
+ "url": "https://opencollective.com/express"
583
+ }
584
+ },
585
+ "node_modules/proxy-addr": {
586
+ "version": "2.0.7",
587
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
588
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
589
+ "license": "MIT",
590
+ "dependencies": {
591
+ "forwarded": "0.2.0",
592
+ "ipaddr.js": "1.9.1"
593
+ },
594
+ "engines": {
595
+ "node": ">= 0.10"
596
+ }
597
+ },
598
+ "node_modules/qs": {
599
+ "version": "6.14.1",
600
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
601
+ "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
602
+ "license": "BSD-3-Clause",
603
+ "dependencies": {
604
+ "side-channel": "^1.1.0"
605
+ },
606
+ "engines": {
607
+ "node": ">=0.6"
608
+ },
609
+ "funding": {
610
+ "url": "https://github.com/sponsors/ljharb"
611
+ }
612
+ },
613
+ "node_modules/range-parser": {
614
+ "version": "1.2.1",
615
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
616
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
617
+ "license": "MIT",
618
+ "engines": {
619
+ "node": ">= 0.6"
620
+ }
621
+ },
622
+ "node_modules/raw-body": {
623
+ "version": "3.0.2",
624
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
625
+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
626
+ "license": "MIT",
627
+ "dependencies": {
628
+ "bytes": "~3.1.2",
629
+ "http-errors": "~2.0.1",
630
+ "iconv-lite": "~0.7.0",
631
+ "unpipe": "~1.0.0"
632
+ },
633
+ "engines": {
634
+ "node": ">= 0.10"
635
+ }
636
+ },
637
+ "node_modules/router": {
638
+ "version": "2.2.0",
639
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
640
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
641
+ "license": "MIT",
642
+ "dependencies": {
643
+ "debug": "^4.4.0",
644
+ "depd": "^2.0.0",
645
+ "is-promise": "^4.0.0",
646
+ "parseurl": "^1.3.3",
647
+ "path-to-regexp": "^8.0.0"
648
+ },
649
+ "engines": {
650
+ "node": ">= 18"
651
+ }
652
+ },
653
+ "node_modules/safer-buffer": {
654
+ "version": "2.1.2",
655
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
656
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
657
+ "license": "MIT"
658
+ },
659
+ "node_modules/send": {
660
+ "version": "1.2.1",
661
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
662
+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
663
+ "license": "MIT",
664
+ "dependencies": {
665
+ "debug": "^4.4.3",
666
+ "encodeurl": "^2.0.0",
667
+ "escape-html": "^1.0.3",
668
+ "etag": "^1.8.1",
669
+ "fresh": "^2.0.0",
670
+ "http-errors": "^2.0.1",
671
+ "mime-types": "^3.0.2",
672
+ "ms": "^2.1.3",
673
+ "on-finished": "^2.4.1",
674
+ "range-parser": "^1.2.1",
675
+ "statuses": "^2.0.2"
676
+ },
677
+ "engines": {
678
+ "node": ">= 18"
679
+ },
680
+ "funding": {
681
+ "type": "opencollective",
682
+ "url": "https://opencollective.com/express"
683
+ }
684
+ },
685
+ "node_modules/serve-static": {
686
+ "version": "2.2.1",
687
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
688
+ "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
689
+ "license": "MIT",
690
+ "dependencies": {
691
+ "encodeurl": "^2.0.0",
692
+ "escape-html": "^1.0.3",
693
+ "parseurl": "^1.3.3",
694
+ "send": "^1.2.0"
695
+ },
696
+ "engines": {
697
+ "node": ">= 18"
698
+ },
699
+ "funding": {
700
+ "type": "opencollective",
701
+ "url": "https://opencollective.com/express"
702
+ }
703
+ },
704
+ "node_modules/setprototypeof": {
705
+ "version": "1.2.0",
706
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
707
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
708
+ "license": "ISC"
709
+ },
710
+ "node_modules/side-channel": {
711
+ "version": "1.1.0",
712
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
713
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
714
+ "license": "MIT",
715
+ "dependencies": {
716
+ "es-errors": "^1.3.0",
717
+ "object-inspect": "^1.13.3",
718
+ "side-channel-list": "^1.0.0",
719
+ "side-channel-map": "^1.0.1",
720
+ "side-channel-weakmap": "^1.0.2"
721
+ },
722
+ "engines": {
723
+ "node": ">= 0.4"
724
+ },
725
+ "funding": {
726
+ "url": "https://github.com/sponsors/ljharb"
727
+ }
728
+ },
729
+ "node_modules/side-channel-list": {
730
+ "version": "1.0.0",
731
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
732
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
733
+ "license": "MIT",
734
+ "dependencies": {
735
+ "es-errors": "^1.3.0",
736
+ "object-inspect": "^1.13.3"
737
+ },
738
+ "engines": {
739
+ "node": ">= 0.4"
740
+ },
741
+ "funding": {
742
+ "url": "https://github.com/sponsors/ljharb"
743
+ }
744
+ },
745
+ "node_modules/side-channel-map": {
746
+ "version": "1.0.1",
747
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
748
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
749
+ "license": "MIT",
750
+ "dependencies": {
751
+ "call-bound": "^1.0.2",
752
+ "es-errors": "^1.3.0",
753
+ "get-intrinsic": "^1.2.5",
754
+ "object-inspect": "^1.13.3"
755
+ },
756
+ "engines": {
757
+ "node": ">= 0.4"
758
+ },
759
+ "funding": {
760
+ "url": "https://github.com/sponsors/ljharb"
761
+ }
762
+ },
763
+ "node_modules/side-channel-weakmap": {
764
+ "version": "1.0.2",
765
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
766
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
767
+ "license": "MIT",
768
+ "dependencies": {
769
+ "call-bound": "^1.0.2",
770
+ "es-errors": "^1.3.0",
771
+ "get-intrinsic": "^1.2.5",
772
+ "object-inspect": "^1.13.3",
773
+ "side-channel-map": "^1.0.1"
774
+ },
775
+ "engines": {
776
+ "node": ">= 0.4"
777
+ },
778
+ "funding": {
779
+ "url": "https://github.com/sponsors/ljharb"
780
+ }
781
+ },
782
+ "node_modules/statuses": {
783
+ "version": "2.0.2",
784
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
785
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
786
+ "license": "MIT",
787
+ "engines": {
788
+ "node": ">= 0.8"
789
+ }
790
+ },
791
+ "node_modules/toidentifier": {
792
+ "version": "1.0.1",
793
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
794
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
795
+ "license": "MIT",
796
+ "engines": {
797
+ "node": ">=0.6"
798
+ }
799
+ },
800
+ "node_modules/type-is": {
801
+ "version": "2.0.1",
802
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
803
+ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
804
+ "license": "MIT",
805
+ "dependencies": {
806
+ "content-type": "^1.0.5",
807
+ "media-typer": "^1.1.0",
808
+ "mime-types": "^3.0.0"
809
+ },
810
+ "engines": {
811
+ "node": ">= 0.6"
812
+ }
813
+ },
814
+ "node_modules/unpipe": {
815
+ "version": "1.0.0",
816
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
817
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
818
+ "license": "MIT",
819
+ "engines": {
820
+ "node": ">= 0.8"
821
+ }
822
+ },
823
+ "node_modules/vary": {
824
+ "version": "1.1.2",
825
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
826
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
827
+ "license": "MIT",
828
+ "engines": {
829
+ "node": ">= 0.8"
830
+ }
831
+ },
832
+ "node_modules/wrappy": {
833
+ "version": "1.0.2",
834
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
835
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
836
+ "license": "ISC"
837
+ }
838
+ }
839
+ }
package.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "trinity-code-reviewer",
3
+ "version": "1.0.0",
4
+ "description": "Paste a GitHub file URL, get a streaming code review powered by Trinity Large Preview",
5
+ "type": "module",
6
+ "main": "server.js",
7
+ "scripts": {
8
+ "start": "node server.js"
9
+ },
10
+ "dependencies": {
11
+ "dotenv": "^16.4.7",
12
+ "express": "^5.1.0"
13
+ }
14
+ }
public/app.js ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* --- DOM refs --- */
2
+ const form = document.getElementById("review-form");
3
+ const urlInput = document.getElementById("url-input");
4
+ const submitBtn = document.getElementById("submit-btn");
5
+ const inputError = document.getElementById("input-error");
6
+ const metaEl = document.getElementById("meta");
7
+ const metaRepo = document.getElementById("meta-repo");
8
+ const metaPath = document.getElementById("meta-path");
9
+ const metaBranch = document.getElementById("meta-branch");
10
+ const tabsEl = document.getElementById("tabs");
11
+ const tabContent = document.getElementById("tab-content");
12
+ const streamError = document.getElementById("stream-error");
13
+ const tabButtons = document.querySelectorAll(".tab");
14
+
15
+ const GITHUB_BLOB_RE = /^https?:\/\/github\.com\/[^/]+\/[^/]+\/blob\/[^/]+\/.+$/;
16
+
17
+ /* --- Section parsing --- */
18
+
19
+ const SECTION_KEYS = ["summary", "quality", "performance", "security", "suggestions"];
20
+
21
+ // Maps heading text (lowercased) to our section key
22
+ const HEADING_MAP = {
23
+ summary: "summary",
24
+ "code quality": "quality",
25
+ performance: "performance",
26
+ security: "security",
27
+ suggestions: "suggestions",
28
+ };
29
+
30
+ function parseSections(markdown) {
31
+ const sections = {};
32
+ let currentKey = null;
33
+
34
+ for (const line of markdown.split("\n")) {
35
+ const m = line.match(/^## (.+)$/);
36
+ if (m) {
37
+ const title = m[1].trim().toLowerCase();
38
+ const matched = Object.entries(HEADING_MAP).find(([kw]) => title.includes(kw));
39
+ if (matched) {
40
+ currentKey = matched[1];
41
+ sections[currentKey] = "";
42
+ continue;
43
+ }
44
+ }
45
+ if (currentKey) {
46
+ sections[currentKey] = (sections[currentKey] || "") + line + "\n";
47
+ }
48
+ }
49
+ return sections;
50
+ }
51
+
52
+ // Detect which section the model is currently writing to (last matched heading)
53
+ function detectCurrentSection(markdown) {
54
+ let last = null;
55
+ for (const line of markdown.split("\n")) {
56
+ const m = line.match(/^## (.+)$/);
57
+ if (m) {
58
+ const title = m[1].trim().toLowerCase();
59
+ const matched = Object.entries(HEADING_MAP).find(([kw]) => title.includes(kw));
60
+ if (matched) last = matched[1];
61
+ }
62
+ }
63
+ return last;
64
+ }
65
+
66
+ /* --- Diff-aware marked renderer --- */
67
+
68
+ function escapeHtml(str) {
69
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
70
+ }
71
+
72
+ function isDiffContent(text) {
73
+ const lines = text.split("\n");
74
+ let diffLines = 0;
75
+ for (const l of lines) {
76
+ if (l.startsWith("+") || l.startsWith("-") || l.startsWith("@@")) diffLines++;
77
+ }
78
+ return diffLines >= 2;
79
+ }
80
+
81
+ function renderDiffBlock(text) {
82
+ const lines = text.split("\n").map((line) => {
83
+ const escaped = escapeHtml(line);
84
+ if (line.startsWith("+++") || line.startsWith("---")) {
85
+ return `<span class="diff-line diff-info">${escaped}</span>`;
86
+ }
87
+ if (line.startsWith("+")) {
88
+ return `<span class="diff-line diff-add">${escaped}</span>`;
89
+ }
90
+ if (line.startsWith("-")) {
91
+ return `<span class="diff-line diff-del">${escaped}</span>`;
92
+ }
93
+ if (line.startsWith("@@")) {
94
+ return `<span class="diff-line diff-info">${escaped}</span>`;
95
+ }
96
+ return `<span class="diff-line diff-neutral">${escaped}</span>`;
97
+ });
98
+ return `<pre class="diff-block"><code>${lines.join("")}</code></pre>`;
99
+ }
100
+
101
+ const renderer = {
102
+ code({ text, lang, language }) {
103
+ const codeLang = (lang || language || "").toLowerCase().trim();
104
+ // Render as diff if tagged as diff OR if content looks like a diff
105
+ if (codeLang === "diff" || (!codeLang && isDiffContent(text))) {
106
+ return renderDiffBlock(text);
107
+ }
108
+ const langClass = codeLang ? ` class="language-${escapeHtml(codeLang)}"` : "";
109
+ return `<pre><code${langClass}>${escapeHtml(text)}</code></pre>`;
110
+ },
111
+ };
112
+
113
+ marked.use({ renderer });
114
+
115
+ function renderMarkdown(md) {
116
+ return DOMPurify.sanitize(marked.parse(md));
117
+ }
118
+
119
+ /* --- Tab management --- */
120
+
121
+ let activeTab = "summary";
122
+ let manualSwitch = false;
123
+ let currentSections = {};
124
+
125
+ tabButtons.forEach((btn) => {
126
+ btn.addEventListener("click", () => {
127
+ manualSwitch = true;
128
+ setActiveTab(btn.dataset.section);
129
+ renderActiveTab();
130
+ });
131
+ });
132
+
133
+ function setActiveTab(section) {
134
+ activeTab = section;
135
+ tabButtons.forEach((btn) => {
136
+ btn.classList.toggle("active", btn.dataset.section === section);
137
+ });
138
+ }
139
+
140
+ function renderActiveTab() {
141
+ const md = currentSections[activeTab] || "";
142
+ if (md.trim()) {
143
+ tabContent.innerHTML = renderMarkdown(md);
144
+ tabContent.classList.remove("tab-content-empty");
145
+ } else {
146
+ tabContent.textContent = "Waiting for content\u2026";
147
+ tabContent.classList.add("tab-content-empty");
148
+ }
149
+ }
150
+
151
+ function updateTabs(sections, currentStreamSection) {
152
+ currentSections = sections;
153
+
154
+ // Mark tabs that have content + which one is streaming
155
+ tabButtons.forEach((btn) => {
156
+ const key = btn.dataset.section;
157
+ btn.classList.toggle("has-content", !!sections[key]?.trim());
158
+ btn.classList.toggle("streaming", key === currentStreamSection);
159
+ });
160
+
161
+ // Auto-switch to the section currently being written (unless user clicked a tab)
162
+ if (!manualSwitch && currentStreamSection) {
163
+ setActiveTab(currentStreamSection);
164
+ }
165
+
166
+ renderActiveTab();
167
+ }
168
+
169
+ /* --- Review flow --- */
170
+
171
+ let currentSource = null;
172
+
173
+ form.addEventListener("submit", (e) => {
174
+ e.preventDefault();
175
+ startReview();
176
+ });
177
+
178
+ function startReview() {
179
+ const url = urlInput.value.trim();
180
+
181
+ // Reset
182
+ inputError.hidden = true;
183
+ streamError.hidden = true;
184
+ metaEl.hidden = true;
185
+ tabsEl.hidden = true;
186
+ tabContent.textContent = "";
187
+ manualSwitch = false;
188
+ setActiveTab("summary");
189
+ tabButtons.forEach((btn) => {
190
+ btn.classList.remove("has-content", "streaming");
191
+ });
192
+
193
+ if (!url) { showInputError("Please enter a GitHub file URL."); return; }
194
+ if (!GITHUB_BLOB_RE.test(url)) {
195
+ showInputError("URL must be a GitHub blob URL (e.g. https://github.com/owner/repo/blob/main/file.js).");
196
+ return;
197
+ }
198
+
199
+ if (currentSource) { currentSource.close(); currentSource = null; }
200
+ setLoading(true);
201
+
202
+ let markdown = "";
203
+ const source = new EventSource(`/api/review?url=${encodeURIComponent(url)}`);
204
+ currentSource = source;
205
+
206
+ source.addEventListener("meta", (e) => {
207
+ const data = JSON.parse(e.data);
208
+ metaRepo.textContent = `${data.owner}/${data.repo}`;
209
+ metaPath.textContent = data.path;
210
+ metaBranch.textContent = data.branch;
211
+ metaEl.hidden = false;
212
+ tabsEl.hidden = false;
213
+ tabContent.classList.add("is-streaming");
214
+ });
215
+
216
+ source.addEventListener("content", (e) => {
217
+ const data = JSON.parse(e.data);
218
+ markdown += data.text;
219
+ const sections = parseSections(markdown);
220
+ const current = detectCurrentSection(markdown);
221
+ updateTabs(sections, current);
222
+ });
223
+
224
+ source.addEventListener("done", () => {
225
+ cleanup();
226
+ // Final render with no streaming section
227
+ const sections = parseSections(markdown);
228
+ updateTabs(sections, null);
229
+ });
230
+
231
+ source.addEventListener("error", (e) => {
232
+ if (e.data) {
233
+ const data = JSON.parse(e.data);
234
+ showStreamError(data.message);
235
+ } else {
236
+ showStreamError("Connection lost. Please try again.");
237
+ }
238
+ cleanup();
239
+ });
240
+
241
+ function cleanup() {
242
+ source.close();
243
+ currentSource = null;
244
+ setLoading(false);
245
+ tabContent.classList.remove("is-streaming");
246
+ tabButtons.forEach((btn) => btn.classList.remove("streaming"));
247
+ }
248
+ }
249
+
250
+ /* --- Helpers --- */
251
+
252
+ function setLoading(on) {
253
+ submitBtn.disabled = on;
254
+ submitBtn.textContent = on ? "Reviewing\u2026" : "Review Code";
255
+ document.body.classList.toggle("loading", on);
256
+ }
257
+
258
+ function showInputError(msg) {
259
+ inputError.textContent = msg;
260
+ inputError.hidden = false;
261
+ }
262
+
263
+ function showStreamError(msg) {
264
+ streamError.textContent = msg;
265
+ streamError.hidden = false;
266
+ }
public/index.html ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Trinity Large Preview — Code Reviewer</title>
7
+ <link rel="stylesheet" href="style.css">
8
+ </head>
9
+ <body>
10
+ <main>
11
+ <h1>Trinity Large Preview</h1>
12
+ <p class="subtitle">Paste a public GitHub file URL for an AI-powered code review.</p>
13
+ <nav class="resources">
14
+ <a href="https://www.arcee.ai/blog/trinity-large" target="_blank" rel="noopener">Arcee Blog</a>
15
+ <a href="https://huggingface.co/arcee-ai/Trinity-Large-Preview" target="_blank" rel="noopener">Model on HuggingFace</a>
16
+ <a href="https://openrouter.ai/arcee-ai/trinity-large-preview:free" target="_blank" rel="noopener">API on OpenRouter</a>
17
+ </nav>
18
+
19
+ <form id="review-form">
20
+ <div class="input-row">
21
+ <input
22
+ type="url"
23
+ id="url-input"
24
+ placeholder="https://github.com/owner/repo/blob/main/path/to/file.js"
25
+ required
26
+ >
27
+ <button type="submit" id="submit-btn">Review Code</button>
28
+ </div>
29
+ <p id="input-error" class="error" hidden></p>
30
+ </form>
31
+
32
+ <div id="meta" hidden>
33
+ <span id="meta-repo"></span>
34
+ <span id="meta-path"></span>
35
+ <span id="meta-branch"></span>
36
+ </div>
37
+
38
+ <div id="tabs" hidden>
39
+ <nav class="tab-bar">
40
+ <button class="tab active" data-section="summary">Summary</button>
41
+ <button class="tab" data-section="quality">Code Quality</button>
42
+ <button class="tab" data-section="performance">Performance</button>
43
+ <button class="tab" data-section="security">Security</button>
44
+ <button class="tab" data-section="suggestions">Suggestions</button>
45
+ </nav>
46
+ <div id="tab-content" class="tab-content"></div>
47
+ </div>
48
+
49
+ <div id="stream-error" class="error" hidden></div>
50
+ </main>
51
+
52
+ <script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
53
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
54
+ <script src="app.js"></script>
55
+ </body>
56
+ </html>
public/style.css ADDED
@@ -0,0 +1,348 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *,
2
+ *::before,
3
+ *::after {
4
+ box-sizing: border-box;
5
+ margin: 0;
6
+ padding: 0;
7
+ }
8
+
9
+ :root {
10
+ --bg: #0e1117;
11
+ --surface: #161b22;
12
+ --border: #30363d;
13
+ --text: #e6edf3;
14
+ --text-muted: #8b949e;
15
+ --accent: #58a6ff;
16
+ --accent-hover: #79c0ff;
17
+ --error: #f85149;
18
+ --diff-add-bg: rgba(46, 160, 67, 0.15);
19
+ --diff-add-fg: #3fb950;
20
+ --diff-del-bg: rgba(248, 81, 73, 0.15);
21
+ --diff-del-fg: #f85149;
22
+ --diff-info-fg: #58a6ff;
23
+ --radius: 8px;
24
+ }
25
+
26
+ body {
27
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
28
+ background: var(--bg);
29
+ color: var(--text);
30
+ line-height: 1.6;
31
+ min-height: 100vh;
32
+ }
33
+
34
+ main {
35
+ max-width: 860px;
36
+ margin: 0 auto;
37
+ padding: 48px 20px;
38
+ }
39
+
40
+ h1 {
41
+ font-size: 1.75rem;
42
+ font-weight: 700;
43
+ margin-bottom: 4px;
44
+ }
45
+
46
+ .subtitle {
47
+ color: var(--text-muted);
48
+ margin-bottom: 8px;
49
+ }
50
+
51
+ .resources {
52
+ display: flex;
53
+ gap: 16px;
54
+ margin-bottom: 28px;
55
+ }
56
+
57
+ .resources a {
58
+ color: var(--accent);
59
+ font-size: 0.85rem;
60
+ text-decoration: none;
61
+ transition: color 0.15s;
62
+ }
63
+
64
+ .resources a:hover {
65
+ color: var(--accent-hover);
66
+ text-decoration: underline;
67
+ }
68
+
69
+ /* ---------- Form ---------- */
70
+
71
+ .input-row {
72
+ display: flex;
73
+ gap: 8px;
74
+ }
75
+
76
+ #url-input {
77
+ flex: 1;
78
+ padding: 10px 14px;
79
+ font-size: 0.95rem;
80
+ background: var(--surface);
81
+ color: var(--text);
82
+ border: 1px solid var(--border);
83
+ border-radius: var(--radius);
84
+ outline: none;
85
+ transition: border-color 0.15s;
86
+ }
87
+
88
+ #url-input:focus {
89
+ border-color: var(--accent);
90
+ }
91
+
92
+ #submit-btn {
93
+ padding: 10px 20px;
94
+ font-size: 0.95rem;
95
+ font-weight: 600;
96
+ color: #fff;
97
+ background: var(--accent);
98
+ border: none;
99
+ border-radius: var(--radius);
100
+ cursor: pointer;
101
+ white-space: nowrap;
102
+ transition: background 0.15s;
103
+ }
104
+
105
+ #submit-btn:hover:not(:disabled) {
106
+ background: var(--accent-hover);
107
+ }
108
+
109
+ #submit-btn:disabled {
110
+ opacity: 0.5;
111
+ cursor: not-allowed;
112
+ }
113
+
114
+ /* ---------- Errors ---------- */
115
+
116
+ .error {
117
+ color: var(--error);
118
+ font-size: 0.9rem;
119
+ margin-top: 10px;
120
+ }
121
+
122
+ /* ---------- Metadata bar ---------- */
123
+
124
+ #meta {
125
+ display: flex;
126
+ gap: 12px;
127
+ flex-wrap: wrap;
128
+ margin-top: 20px;
129
+ padding: 10px 14px;
130
+ background: var(--surface);
131
+ border: 1px solid var(--border);
132
+ border-radius: var(--radius);
133
+ font-size: 0.85rem;
134
+ color: var(--text-muted);
135
+ }
136
+
137
+ #meta span::before {
138
+ font-weight: 600;
139
+ color: var(--text);
140
+ }
141
+
142
+ #meta-repo::before { content: "Repo: "; }
143
+ #meta-path::before { content: "File: "; }
144
+ #meta-branch::before { content: "Branch: "; }
145
+
146
+ /* ---------- Tabs ---------- */
147
+
148
+ #tabs {
149
+ margin-top: 20px;
150
+ }
151
+
152
+ .tab-bar {
153
+ display: flex;
154
+ gap: 2px;
155
+ border-bottom: 1px solid var(--border);
156
+ overflow-x: auto;
157
+ }
158
+
159
+ .tab {
160
+ position: relative;
161
+ padding: 10px 18px;
162
+ font-size: 0.88rem;
163
+ font-weight: 500;
164
+ color: var(--text-muted);
165
+ background: none;
166
+ border: none;
167
+ border-bottom: 2px solid transparent;
168
+ cursor: pointer;
169
+ white-space: nowrap;
170
+ transition: color 0.15s, border-color 0.15s;
171
+ }
172
+
173
+ .tab:hover {
174
+ color: var(--text);
175
+ }
176
+
177
+ .tab.active {
178
+ color: var(--text);
179
+ border-bottom-color: var(--accent);
180
+ }
181
+
182
+ /* Dot: tab has content */
183
+ .tab.has-content::after {
184
+ content: "";
185
+ position: absolute;
186
+ top: 8px;
187
+ right: 6px;
188
+ width: 6px;
189
+ height: 6px;
190
+ border-radius: 50%;
191
+ background: var(--accent);
192
+ }
193
+
194
+ /* Pulsing dot: tab is currently receiving streamed content */
195
+ .tab.streaming::after {
196
+ background: var(--accent);
197
+ animation: pulse 1s ease-in-out infinite;
198
+ }
199
+
200
+ @keyframes pulse {
201
+ 0%, 100% { opacity: 1; }
202
+ 50% { opacity: 0.3; }
203
+ }
204
+
205
+ /* ---------- Tab content ---------- */
206
+
207
+ .tab-content {
208
+ padding: 24px;
209
+ background: var(--surface);
210
+ border: 1px solid var(--border);
211
+ border-top: none;
212
+ border-radius: 0 0 var(--radius) var(--radius);
213
+ min-height: 120px;
214
+ }
215
+
216
+ .tab-content-empty {
217
+ color: var(--text-muted);
218
+ font-style: italic;
219
+ }
220
+
221
+ /* Cursor blink while streaming */
222
+ .tab-content.is-streaming::after {
223
+ content: "▍";
224
+ animation: blink 0.8s step-end infinite;
225
+ }
226
+
227
+ @keyframes blink {
228
+ 50% { opacity: 0; }
229
+ }
230
+
231
+ /* ---------- Rendered markdown ---------- */
232
+
233
+ .tab-content h2 {
234
+ display: none; /* section header is already the tab title */
235
+ }
236
+
237
+ .tab-content h3 {
238
+ font-size: 1rem;
239
+ margin-top: 18px;
240
+ margin-bottom: 6px;
241
+ }
242
+
243
+ .tab-content p {
244
+ margin-bottom: 10px;
245
+ }
246
+
247
+ .tab-content ul,
248
+ .tab-content ol {
249
+ margin-bottom: 10px;
250
+ padding-left: 24px;
251
+ }
252
+
253
+ .tab-content li {
254
+ margin-bottom: 4px;
255
+ }
256
+
257
+ .tab-content strong {
258
+ color: var(--accent);
259
+ }
260
+
261
+ .tab-content code {
262
+ font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
263
+ background: rgba(110, 118, 129, 0.15);
264
+ padding: 2px 6px;
265
+ border-radius: 4px;
266
+ font-size: 0.88em;
267
+ }
268
+
269
+ .tab-content pre {
270
+ background: var(--bg);
271
+ border: 1px solid var(--border);
272
+ border-radius: var(--radius);
273
+ padding: 14px;
274
+ overflow-x: auto;
275
+ margin-bottom: 12px;
276
+ }
277
+
278
+ .tab-content pre code {
279
+ background: none;
280
+ padding: 0;
281
+ }
282
+
283
+ /* ---------- Diff highlighting ---------- */
284
+
285
+ .diff-block {
286
+ background: var(--bg);
287
+ border: 1px solid var(--border);
288
+ border-radius: var(--radius);
289
+ padding: 0;
290
+ overflow-x: auto;
291
+ margin-bottom: 12px;
292
+ font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
293
+ font-size: 0.85rem;
294
+ line-height: 1.55;
295
+ }
296
+
297
+ .diff-block code {
298
+ background: none;
299
+ padding: 0;
300
+ display: block;
301
+ }
302
+
303
+ .diff-line {
304
+ display: block;
305
+ padding: 1px 14px;
306
+ white-space: pre-wrap;
307
+ word-break: break-all;
308
+ }
309
+
310
+ .diff-add {
311
+ background: var(--diff-add-bg);
312
+ color: var(--diff-add-fg);
313
+ }
314
+
315
+ .diff-del {
316
+ background: var(--diff-del-bg);
317
+ color: var(--diff-del-fg);
318
+ }
319
+
320
+ .diff-info {
321
+ color: var(--diff-info-fg);
322
+ font-style: italic;
323
+ padding-top: 4px;
324
+ padding-bottom: 4px;
325
+ }
326
+
327
+ .diff-neutral {
328
+ color: var(--text-muted);
329
+ }
330
+
331
+ /* ---------- Loading spinner ---------- */
332
+
333
+ .loading #submit-btn::after {
334
+ content: "";
335
+ display: inline-block;
336
+ width: 14px;
337
+ height: 14px;
338
+ margin-left: 8px;
339
+ border: 2px solid rgba(255, 255, 255, 0.3);
340
+ border-top-color: #fff;
341
+ border-radius: 50%;
342
+ animation: spin 0.6s linear infinite;
343
+ vertical-align: middle;
344
+ }
345
+
346
+ @keyframes spin {
347
+ to { transform: rotate(360deg); }
348
+ }
server.js ADDED
@@ -0,0 +1,289 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import "dotenv/config";
2
+ import express from "express";
3
+ import { fileURLToPath } from "url";
4
+ import { dirname, join, extname } from "path";
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const app = express();
8
+ const PORT = process.env.PORT || 3000;
9
+ const API_KEY = process.env.OPENROUTER_API_KEY;
10
+
11
+ if (!API_KEY || API_KEY === "your-key-here") {
12
+ console.error("Missing OPENROUTER_API_KEY in .env file");
13
+ process.exit(1);
14
+ }
15
+
16
+ // --- Helpers ---
17
+
18
+ function parseGitHubUrl(url) {
19
+ // Matches: https://github.com/{owner}/{repo}/blob/{branch}/{...path}
20
+ const match = url.match(
21
+ /^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/(.+)$/
22
+ );
23
+ if (!match) return null;
24
+ const [, owner, repo, branch, path] = match;
25
+ const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}`;
26
+ const ext = extname(path).slice(1) || "txt";
27
+ return { owner, repo, branch, path, rawUrl, ext };
28
+ }
29
+
30
+ const SYSTEM_PROMPT = `You are an elite code reviewer channeling the uncompromising standards of Linus Torvalds, the algorithmic rigor of Donald Knuth, and the design discipline of Bjarne Stroustrup. You do not hand out compliments. You find what is wrong and you say it plainly. If the code is sloppy, say so. If a design decision is stupid, explain why. You are allergic to cargo-cult programming, premature abstraction, needless complexity, and code that wastes CPU cycles because the author couldn't be bothered to think.
31
+
32
+ Your tone is direct, blunt, and technically precise. You can be witty but never nice for the sake of being nice. Think LKML-style review: no sugarcoating, no "great job", no filler. Every sentence must earn its place. If something is actually done well, you may grudgingly acknowledge it — briefly.
33
+
34
+ The source file is provided with line numbers (e.g. "42 | code"). Produce a structured review with EXACTLY these five markdown sections:
35
+
36
+ ## Summary
37
+ What this file does and whether it has any business existing in its current form. Be blunt.
38
+
39
+ ## Code Quality
40
+ Tear into readability, naming, structure, anti-patterns, code smells, and idiom violations. For EVERY issue use this exact structure:
41
+
42
+ **Line 42:** Short summary of the problem — what is wrong and why it matters.
43
+
44
+ \`\`\`diff
45
+ - disgraceful_code()
46
+ + what_it_should_have_been()
47
+ \`\`\`
48
+
49
+ **Why:** [1-2 sentences — the concrete technical benefit: what was wrong, what the fix guarantees.]
50
+
51
+ **Why not:** [1-2 sentences — honest tradeoffs, risks, or reasons to push back: added complexity, backward-compat breakage, perf tradeoff in the other direction, migration cost, or "no meaningful downside" if it's a clear win.]
52
+
53
+ ## Performance
54
+ Find the inefficiencies the author was too lazy to notice. Unnecessary allocations, O(n²) where O(n) was trivial, needless copies, hot-path bloat, allocations in loops. For EVERY issue use this exact structure:
55
+
56
+ **Lines X-Y:** Short summary of the inefficiency — name the wasted work.
57
+
58
+ \`\`\`diff
59
+ - slow_version()
60
+ + fast_version()
61
+ \`\`\`
62
+
63
+ **Why:** [what makes the new version faster — name the cost eliminated.]
64
+
65
+ **Why not:** [honest tradeoffs — readability cost, marginal gain on cold paths, memory-vs-speed, or "no meaningful downside".]
66
+
67
+ ## Security
68
+ Input validation gaps, injection vectors (SQL, XSS, command, path traversal), secrets in code, broken auth, race conditions, unchecked boundaries. If there's nothing, say so in one sentence — don't invent problems. For EVERY issue use this exact structure:
69
+
70
+ **Line X:** Short summary of the vulnerability — name the attack surface.
71
+
72
+ \`\`\`diff
73
+ - vulnerable_code()
74
+ + secure_code()
75
+ \`\`\`
76
+
77
+ **Why:** [what attack the old code enabled and how the fix closes that vector.]
78
+
79
+ **Why not:** [usability friction, performance cost, over-defense, or "no meaningful downside".]
80
+
81
+ ## Suggestions
82
+ Numbered list of concrete improvements, ranked ruthlessly by impact. No hand-wavy "consider maybe possibly" language. EVERY suggestion MUST include a \`\`\`diff block — no exceptions. A suggestion without a diff is useless; show the exact code change. Use this exact structure:
83
+
84
+ 1. **Line X:** Short summary of what to improve and why.
85
+
86
+ \`\`\`diff
87
+ - current_code()
88
+ + improved_code()
89
+ \`\`\`
90
+
91
+ **Why:** [the concrete benefit.]
92
+
93
+ **Why not:** [tradeoffs, risks, or reasons a reasonable engineer might skip this one.]
94
+
95
+ 2. **Line Y:** ...
96
+
97
+ (continue numbering)
98
+
99
+ Rules:
100
+ - You MUST use exactly these five ## headings and no others.
101
+ - ALWAYS reference line numbers from the provided source.
102
+ - ALWAYS use \`\`\`diff blocks for code changes (- for removals, + for additions). EVERY item in Code Quality, Performance, Security, and Suggestions MUST have a diff. No exceptions.
103
+ - Do NOT pad the review with praise or pleasantries. Respect the reader's time.
104
+ - If the code is genuinely excellent in some area, one dry sentence of acknowledgment is sufficient.
105
+ - Be merciless but never wrong. Every criticism must be technically defensible.`;
106
+
107
+ function buildUserMessage(meta, code) {
108
+ const numbered = code
109
+ .split("\n")
110
+ .map((line, i) => `${i + 1} | ${line}`)
111
+ .join("\n");
112
+
113
+ return `Review the following file.
114
+
115
+ **Repository:** ${meta.owner}/${meta.repo}
116
+ **Branch:** ${meta.branch}
117
+ **File:** ${meta.path}
118
+
119
+ \`\`\`${meta.ext}
120
+ ${numbered}
121
+ \`\`\``;
122
+ }
123
+
124
+ // --- Routes ---
125
+
126
+ app.use(express.static(join(__dirname, "public")));
127
+
128
+ app.get("/api/review", async (req, res) => {
129
+ const url = req.query.url;
130
+ if (!url) {
131
+ return res.status(400).json({ error: "Missing url parameter" });
132
+ }
133
+
134
+ const meta = parseGitHubUrl(url);
135
+ if (!meta) {
136
+ return res
137
+ .status(400)
138
+ .json({ error: "Invalid GitHub blob URL. Expected format: https://github.com/{owner}/{repo}/blob/{branch}/{path}" });
139
+ }
140
+
141
+ // SSE headers
142
+ res.setHeader("Content-Type", "text/event-stream");
143
+ res.setHeader("Cache-Control", "no-cache");
144
+ res.setHeader("Connection", "keep-alive");
145
+ res.flushHeaders();
146
+
147
+ const send = (event, data) => {
148
+ res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
149
+ };
150
+
151
+ // Keep connection alive with SSE comments every 15s
152
+ const keepalive = setInterval(() => res.write(": keepalive\n\n"), 15_000);
153
+
154
+ // Track client disconnect
155
+ let clientGone = false;
156
+ req.on("close", () => { clientGone = true; });
157
+
158
+ try {
159
+ // Fetch file content from GitHub
160
+ const ghResponse = await fetch(meta.rawUrl);
161
+ if (!ghResponse.ok) {
162
+ const status = ghResponse.status;
163
+ const msg =
164
+ status === 404
165
+ ? "File not found — check the URL or ensure the repo is public."
166
+ : status === 403
167
+ ? "Access denied — this may be a private repository."
168
+ : `GitHub returned HTTP ${status}.`;
169
+ send("error", { message: msg });
170
+ return res.end();
171
+ }
172
+
173
+ const contentLength = ghResponse.headers.get("content-length");
174
+ if (contentLength && Number(contentLength) > 100_000) {
175
+ send("error", { message: "File is too large (>100 KB). Paste a smaller file." });
176
+ return res.end();
177
+ }
178
+
179
+ const code = await ghResponse.text();
180
+ if (!code.trim()) {
181
+ send("error", { message: "File is empty." });
182
+ return res.end();
183
+ }
184
+
185
+ // Send file metadata to the client
186
+ send("meta", { owner: meta.owner, repo: meta.repo, branch: meta.branch, path: meta.path });
187
+
188
+ // Stream from OpenRouter
189
+ console.log(`[review] Requesting review from OpenRouter…`);
190
+ const controller = new AbortController();
191
+ const timeout = setTimeout(() => controller.abort(), 120_000); // 2 min timeout
192
+
193
+ let orResponse;
194
+ try {
195
+ orResponse = await fetch("https://openrouter.ai/api/v1/chat/completions", {
196
+ method: "POST",
197
+ signal: controller.signal,
198
+ headers: {
199
+ Authorization: `Bearer ${API_KEY}`,
200
+ "Content-Type": "application/json",
201
+ "HTTP-Referer": "https://github.com/trinity-code-reviewer",
202
+ "X-Title": "Trinity Code Reviewer",
203
+ },
204
+ body: JSON.stringify({
205
+ model: "arcee-ai/trinity-large-preview:free",
206
+ stream: true,
207
+ messages: [
208
+ { role: "system", content: SYSTEM_PROMPT },
209
+ { role: "user", content: buildUserMessage(meta, code) },
210
+ ],
211
+ }),
212
+ });
213
+ } catch (fetchErr) {
214
+ clearTimeout(timeout);
215
+ const msg = fetchErr.name === "AbortError"
216
+ ? "Request timed out — the model took too long to respond. Try again."
217
+ : `Failed to reach OpenRouter: ${fetchErr.message}`;
218
+ console.error(`[review] Fetch failed:`, fetchErr.message);
219
+ send("error", { message: msg });
220
+ return res.end();
221
+ }
222
+
223
+ if (!orResponse.ok) {
224
+ clearTimeout(timeout);
225
+ const status = orResponse.status;
226
+ const body = await orResponse.text();
227
+ console.error(`[review] OpenRouter HTTP ${status}:`, body.slice(0, 300));
228
+ const msg =
229
+ status === 401
230
+ ? "Invalid OpenRouter API key."
231
+ : status === 429
232
+ ? "Rate limited — please wait and try again."
233
+ : `OpenRouter error (HTTP ${status}): ${body.slice(0, 200)}`;
234
+ send("error", { message: msg });
235
+ return res.end();
236
+ }
237
+
238
+ console.log(`[review] Streaming response…`);
239
+
240
+ // Relay streamed chunks
241
+ const reader = orResponse.body.getReader();
242
+ const decoder = new TextDecoder();
243
+ let buffer = "";
244
+ let chunks = 0;
245
+
246
+ while (true) {
247
+ const { done, value } = await reader.read();
248
+ if (done) break;
249
+
250
+ buffer += decoder.decode(value, { stream: true });
251
+ const lines = buffer.split("\n");
252
+ buffer = lines.pop(); // keep incomplete line in buffer
253
+
254
+ for (const line of lines) {
255
+ const trimmed = line.trim();
256
+ if (!trimmed.startsWith("data: ")) continue;
257
+ const payload = trimmed.slice(6);
258
+ if (payload === "[DONE]") continue;
259
+
260
+ try {
261
+ const parsed = JSON.parse(payload);
262
+ const delta = parsed.choices?.[0]?.delta?.content;
263
+ if (delta) {
264
+ chunks++;
265
+ send("content", { text: delta });
266
+ }
267
+ } catch {
268
+ // skip malformed JSON chunks
269
+ }
270
+ }
271
+ }
272
+
273
+ clearTimeout(timeout);
274
+ console.log(`[review] Done — ${chunks} chunks streamed.`);
275
+ send("done", {});
276
+ } catch (err) {
277
+ console.error("[review] Error:", err);
278
+ if (!clientGone) {
279
+ send("error", { message: "Internal server error. Check the server logs." });
280
+ }
281
+ } finally {
282
+ clearInterval(keepalive);
283
+ res.end();
284
+ }
285
+ });
286
+
287
+ app.listen(PORT, () => {
288
+ console.log(`Trinity Large Preview running at http://localhost:${PORT}`);
289
+ });