@ranimontagna/agent-toolkit 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +282 -277
  2. package/docs/assets/install-plan.svg +29 -0
  3. package/docs/assets/install-skill-packages.svg +31 -0
  4. package/docs/assets/install-status.svg +32 -0
  5. package/package.json +10 -9
  6. package/setup-agent-toolkit.sh +1 -1
  7. package/skills/backend/fastify-best-practices/LICENSE +21 -0
  8. package/skills/backend/fastify-best-practices/NOTICE.md +11 -0
  9. package/skills/backend/fastify-best-practices/SKILL.md +75 -0
  10. package/skills/backend/fastify-best-practices/rules/authentication.md +521 -0
  11. package/skills/backend/fastify-best-practices/rules/configuration.md +217 -0
  12. package/skills/backend/fastify-best-practices/rules/content-type.md +387 -0
  13. package/skills/backend/fastify-best-practices/rules/cors-security.md +445 -0
  14. package/skills/backend/fastify-best-practices/rules/database.md +320 -0
  15. package/skills/backend/fastify-best-practices/rules/decorators.md +416 -0
  16. package/skills/backend/fastify-best-practices/rules/deployment.md +423 -0
  17. package/skills/backend/fastify-best-practices/rules/error-handling.md +412 -0
  18. package/skills/backend/fastify-best-practices/rules/hooks.md +464 -0
  19. package/skills/backend/fastify-best-practices/rules/http-proxy.md +247 -0
  20. package/skills/backend/fastify-best-practices/rules/logging.md +402 -0
  21. package/skills/backend/fastify-best-practices/rules/performance.md +425 -0
  22. package/skills/backend/fastify-best-practices/rules/plugins.md +320 -0
  23. package/skills/backend/fastify-best-practices/rules/routes.md +467 -0
  24. package/skills/backend/fastify-best-practices/rules/schemas.md +585 -0
  25. package/skills/backend/fastify-best-practices/rules/serialization.md +475 -0
  26. package/skills/backend/fastify-best-practices/rules/testing.md +536 -0
  27. package/skills/backend/fastify-best-practices/rules/typescript.md +458 -0
  28. package/skills/backend/fastify-best-practices/rules/websockets.md +421 -0
  29. package/skills/backend/fastify-best-practices/tile.json +11 -0
  30. package/skills/core/agent-toolkit-maintainer/SKILL.md +16 -14
@@ -0,0 +1,464 @@
1
+ ---
2
+ name: hooks
3
+ description: Hooks and request lifecycle in Fastify
4
+ metadata:
5
+ tags: hooks, lifecycle, middleware, onRequest, preHandler
6
+ ---
7
+
8
+ # Hooks and Request Lifecycle
9
+
10
+ ## Request Lifecycle Overview
11
+
12
+ Fastify executes hooks in a specific order:
13
+
14
+ ```
15
+ Incoming Request
16
+ |
17
+ onRequest
18
+ |
19
+ preParsing
20
+ |
21
+ preValidation
22
+ |
23
+ preHandler
24
+ |
25
+ Handler
26
+ |
27
+ preSerialization
28
+ |
29
+ onSend
30
+ |
31
+ onResponse
32
+ ```
33
+
34
+ ## onRequest Hook
35
+
36
+ First hook to execute, before body parsing. Use for authentication, request ID setup:
37
+
38
+ ```typescript
39
+ import Fastify from 'fastify';
40
+
41
+ const app = Fastify();
42
+
43
+ // Global onRequest hook
44
+ app.addHook('onRequest', async (request, reply) => {
45
+ request.startTime = Date.now();
46
+ request.log.info({ url: request.url, method: request.method }, 'Request started');
47
+ });
48
+
49
+ // Authentication check
50
+ app.addHook('onRequest', async (request, reply) => {
51
+ // Skip auth for public routes
52
+ if (request.url.startsWith('/public')) {
53
+ return;
54
+ }
55
+
56
+ const token = request.headers.authorization?.replace('Bearer ', '');
57
+ if (!token) {
58
+ reply.code(401).send({ error: 'Unauthorized' });
59
+ return; // Stop processing
60
+ }
61
+
62
+ try {
63
+ request.user = await verifyToken(token);
64
+ } catch {
65
+ reply.code(401).send({ error: 'Invalid token' });
66
+ }
67
+ });
68
+ ```
69
+
70
+ ## preParsing Hook
71
+
72
+ Execute before body parsing. Can modify the payload stream:
73
+
74
+ ```typescript
75
+ app.addHook('preParsing', async (request, reply, payload) => {
76
+ // Log raw payload size
77
+ request.log.debug({ contentLength: request.headers['content-length'] }, 'Parsing body');
78
+
79
+ // Return modified payload stream if needed
80
+ return payload;
81
+ });
82
+
83
+ // Decompress incoming data
84
+ app.addHook('preParsing', async (request, reply, payload) => {
85
+ if (request.headers['content-encoding'] === 'gzip') {
86
+ return payload.pipe(zlib.createGunzip());
87
+ }
88
+ return payload;
89
+ });
90
+ ```
91
+
92
+ ## preValidation Hook
93
+
94
+ Execute after parsing, before schema validation:
95
+
96
+ ```typescript
97
+ app.addHook('preValidation', async (request, reply) => {
98
+ // Modify body before validation
99
+ if (request.body && typeof request.body === 'object') {
100
+ // Normalize data
101
+ request.body.email = request.body.email?.toLowerCase().trim();
102
+ }
103
+ });
104
+
105
+ // Rate limiting check
106
+ app.addHook('preValidation', async (request, reply) => {
107
+ const key = request.ip;
108
+ const count = await redis.incr(`ratelimit:${key}`);
109
+
110
+ if (count === 1) {
111
+ await redis.expire(`ratelimit:${key}`, 60);
112
+ }
113
+
114
+ if (count > 100) {
115
+ reply.code(429).send({ error: 'Too many requests' });
116
+ }
117
+ });
118
+ ```
119
+
120
+ ## preHandler Hook
121
+
122
+ Most common hook, execute after validation, before handler:
123
+
124
+ ```typescript
125
+ // Authorization check
126
+ app.addHook('preHandler', async (request, reply) => {
127
+ const { userId } = request.params as { userId: string };
128
+
129
+ if (request.user.id !== userId && !request.user.isAdmin) {
130
+ reply.code(403).send({ error: 'Forbidden' });
131
+ }
132
+ });
133
+
134
+ // Load related data
135
+ app.addHook('preHandler', async (request, reply) => {
136
+ if (request.params?.projectId) {
137
+ request.project = await db.projects.findById(request.params.projectId);
138
+ if (!request.project) {
139
+ reply.code(404).send({ error: 'Project not found' });
140
+ }
141
+ }
142
+ });
143
+
144
+ // Transaction wrapper
145
+ app.addHook('preHandler', async (request) => {
146
+ request.transaction = await db.beginTransaction();
147
+ });
148
+
149
+ app.addHook('onResponse', async (request) => {
150
+ if (request.transaction) {
151
+ await request.transaction.commit();
152
+ }
153
+ });
154
+
155
+ app.addHook('onError', async (request, reply, error) => {
156
+ if (request.transaction) {
157
+ await request.transaction.rollback();
158
+ }
159
+ });
160
+ ```
161
+
162
+ ## preSerialization Hook
163
+
164
+ Modify payload before serialization:
165
+
166
+ ```typescript
167
+ app.addHook('preSerialization', async (request, reply, payload) => {
168
+ // Add metadata to all responses
169
+ if (payload && typeof payload === 'object') {
170
+ return {
171
+ ...payload,
172
+ _meta: {
173
+ requestId: request.id,
174
+ timestamp: new Date().toISOString(),
175
+ },
176
+ };
177
+ }
178
+ return payload;
179
+ });
180
+
181
+ // Remove sensitive fields
182
+ app.addHook('preSerialization', async (request, reply, payload) => {
183
+ if (payload?.user?.password) {
184
+ const { password, ...user } = payload.user;
185
+ return { ...payload, user };
186
+ }
187
+ return payload;
188
+ });
189
+ ```
190
+
191
+ ## onSend Hook
192
+
193
+ Modify response after serialization:
194
+
195
+ ```typescript
196
+ app.addHook('onSend', async (request, reply, payload) => {
197
+ // Add response headers
198
+ reply.header('X-Response-Time', Date.now() - request.startTime);
199
+
200
+ // Compress response
201
+ if (payload && payload.length > 1024) {
202
+ const compressed = await gzip(payload);
203
+ reply.header('Content-Encoding', 'gzip');
204
+ return compressed;
205
+ }
206
+
207
+ return payload;
208
+ });
209
+
210
+ // Transform JSON string response
211
+ app.addHook('onSend', async (request, reply, payload) => {
212
+ if (reply.getHeader('content-type')?.includes('application/json')) {
213
+ // payload is already a string at this point
214
+ return payload;
215
+ }
216
+ return payload;
217
+ });
218
+ ```
219
+
220
+ ## onResponse Hook
221
+
222
+ Execute after response is sent. Cannot modify response:
223
+
224
+ ```typescript
225
+ app.addHook('onResponse', async (request, reply) => {
226
+ // Log response time
227
+ const responseTime = Date.now() - request.startTime;
228
+ request.log.info({
229
+ method: request.method,
230
+ url: request.url,
231
+ statusCode: reply.statusCode,
232
+ responseTime,
233
+ }, 'Request completed');
234
+
235
+ // Track metrics
236
+ metrics.histogram('http_request_duration', responseTime, {
237
+ method: request.method,
238
+ route: request.routeOptions.url,
239
+ status: reply.statusCode,
240
+ });
241
+ });
242
+ ```
243
+
244
+ ## onError Hook
245
+
246
+ Execute when an error is thrown:
247
+
248
+ ```typescript
249
+ app.addHook('onError', async (request, reply, error) => {
250
+ // Log error details
251
+ request.log.error({
252
+ err: error,
253
+ url: request.url,
254
+ method: request.method,
255
+ body: request.body,
256
+ }, 'Request error');
257
+
258
+ // Track error metrics
259
+ metrics.increment('http_errors', {
260
+ error: error.code || 'UNKNOWN',
261
+ route: request.routeOptions.url,
262
+ });
263
+
264
+ // Cleanup resources
265
+ if (request.tempFile) {
266
+ await fs.unlink(request.tempFile).catch(() => {});
267
+ }
268
+ });
269
+ ```
270
+
271
+ ## onTimeout Hook
272
+
273
+ Execute when request times out:
274
+
275
+ ```typescript
276
+ const app = Fastify({
277
+ connectionTimeout: 30000, // 30 seconds
278
+ });
279
+
280
+ app.addHook('onTimeout', async (request, reply) => {
281
+ request.log.warn({
282
+ url: request.url,
283
+ method: request.method,
284
+ }, 'Request timeout');
285
+
286
+ // Cleanup
287
+ if (request.abortController) {
288
+ request.abortController.abort();
289
+ }
290
+ });
291
+ ```
292
+
293
+ ## onRequestAbort Hook
294
+
295
+ Execute when client closes connection:
296
+
297
+ ```typescript
298
+ app.addHook('onRequestAbort', async (request) => {
299
+ request.log.info('Client aborted request');
300
+
301
+ // Cancel ongoing operations
302
+ if (request.abortController) {
303
+ request.abortController.abort();
304
+ }
305
+
306
+ // Cleanup uploaded files
307
+ if (request.uploadedFiles) {
308
+ for (const file of request.uploadedFiles) {
309
+ await fs.unlink(file.path).catch(() => {});
310
+ }
311
+ }
312
+ });
313
+ ```
314
+
315
+ ## Application Lifecycle Hooks
316
+
317
+ Hooks that run at application startup/shutdown:
318
+
319
+ ```typescript
320
+ // After all plugins are loaded
321
+ app.addHook('onReady', async function () {
322
+ this.log.info('Server is ready');
323
+
324
+ // Initialize connections
325
+ await this.db.connect();
326
+ await this.redis.connect();
327
+
328
+ // Warm caches
329
+ await this.cache.warmup();
330
+ });
331
+
332
+ // When server is closing
333
+ app.addHook('onClose', async function () {
334
+ this.log.info('Server is closing');
335
+
336
+ // Cleanup connections
337
+ await this.db.close();
338
+ await this.redis.disconnect();
339
+ });
340
+
341
+ // After routes are registered
342
+ app.addHook('onRoute', (routeOptions) => {
343
+ console.log(`Route registered: ${routeOptions.method} ${routeOptions.url}`);
344
+
345
+ // Track all routes
346
+ routes.push({
347
+ method: routeOptions.method,
348
+ url: routeOptions.url,
349
+ schema: routeOptions.schema,
350
+ });
351
+ });
352
+
353
+ // After plugin is registered
354
+ app.addHook('onRegister', (instance, options) => {
355
+ console.log(`Plugin registered with prefix: ${options.prefix}`);
356
+ });
357
+ ```
358
+
359
+ ## Scoped Hooks
360
+
361
+ Hooks are scoped to their encapsulation context:
362
+
363
+ ```typescript
364
+ app.addHook('onRequest', async (request) => {
365
+ // Runs for ALL routes
366
+ request.log.info('Global hook');
367
+ });
368
+
369
+ app.register(async function adminRoutes(fastify) {
370
+ // Only runs for routes in this plugin
371
+ fastify.addHook('onRequest', async (request, reply) => {
372
+ if (!request.user?.isAdmin) {
373
+ reply.code(403).send({ error: 'Admin only' });
374
+ }
375
+ });
376
+
377
+ fastify.get('/admin/users', async () => {
378
+ return { users: [] };
379
+ });
380
+ }, { prefix: '/admin' });
381
+ ```
382
+
383
+ ## Hook Execution Order
384
+
385
+ Multiple hooks of the same type execute in registration order:
386
+
387
+ ```typescript
388
+ app.addHook('onRequest', async () => {
389
+ console.log('First');
390
+ });
391
+
392
+ app.addHook('onRequest', async () => {
393
+ console.log('Second');
394
+ });
395
+
396
+ app.addHook('onRequest', async () => {
397
+ console.log('Third');
398
+ });
399
+
400
+ // Output: First, Second, Third
401
+ ```
402
+
403
+ ## Stopping Hook Execution
404
+
405
+ Return early from hooks to stop processing:
406
+
407
+ ```typescript
408
+ app.addHook('preHandler', async (request, reply) => {
409
+ if (!request.user) {
410
+ // Send response and return to stop further processing
411
+ reply.code(401).send({ error: 'Unauthorized' });
412
+ return;
413
+ }
414
+ // Continue to next hook and handler
415
+ });
416
+ ```
417
+
418
+ ## Route-Level Hooks
419
+
420
+ Add hooks to specific routes:
421
+
422
+ ```typescript
423
+ const adminOnlyHook = async (request, reply) => {
424
+ if (!request.user?.isAdmin) {
425
+ reply.code(403).send({ error: 'Forbidden' });
426
+ }
427
+ };
428
+
429
+ app.get('/admin/settings', {
430
+ preHandler: [adminOnlyHook],
431
+ handler: async (request) => {
432
+ return { settings: {} };
433
+ },
434
+ });
435
+
436
+ // Multiple hooks
437
+ app.post('/orders', {
438
+ preValidation: [validateApiKey],
439
+ preHandler: [loadUser, checkQuota, logOrder],
440
+ handler: createOrderHandler,
441
+ });
442
+ ```
443
+
444
+ ## Async Hook Patterns
445
+
446
+ Always use async/await in hooks:
447
+
448
+ ```typescript
449
+ // GOOD - async hook
450
+ app.addHook('preHandler', async (request, reply) => {
451
+ const user = await loadUser(request.headers.authorization);
452
+ request.user = user;
453
+ });
454
+
455
+ // AVOID - callback style (deprecated)
456
+ app.addHook('preHandler', (request, reply, done) => {
457
+ loadUser(request.headers.authorization)
458
+ .then((user) => {
459
+ request.user = user;
460
+ done();
461
+ })
462
+ .catch(done);
463
+ });
464
+ ```
@@ -0,0 +1,247 @@
1
+ ---
2
+ name: http-proxy
3
+ description: HTTP proxying and reply.from() in Fastify
4
+ metadata:
5
+ tags: proxy, gateway, reverse-proxy, microservices
6
+ ---
7
+
8
+ # HTTP Proxy and Reply.from()
9
+
10
+ ## @fastify/http-proxy
11
+
12
+ Use `@fastify/http-proxy` for simple reverse proxy scenarios:
13
+
14
+ ```typescript
15
+ import Fastify from 'fastify';
16
+ import httpProxy from '@fastify/http-proxy';
17
+
18
+ const app = Fastify({ logger: true });
19
+
20
+ // Proxy all requests to /api/* to another service
21
+ app.register(httpProxy, {
22
+ upstream: 'http://backend-service:3001',
23
+ prefix: '/api',
24
+ rewritePrefix: '/v1',
25
+ http2: false,
26
+ });
27
+
28
+ // With authentication
29
+ app.register(httpProxy, {
30
+ upstream: 'http://internal-api:3002',
31
+ prefix: '/internal',
32
+ preHandler: async (request, reply) => {
33
+ // Verify authentication before proxying
34
+ if (!request.headers.authorization) {
35
+ reply.code(401).send({ error: 'Unauthorized' });
36
+ }
37
+ },
38
+ });
39
+
40
+ await app.listen({ port: 3000 });
41
+ ```
42
+
43
+ ## @fastify/reply-from
44
+
45
+ For more control over proxying, use `@fastify/reply-from` with `reply.from()`:
46
+
47
+ ```typescript
48
+ import Fastify from 'fastify';
49
+ import replyFrom from '@fastify/reply-from';
50
+
51
+ const app = Fastify({ logger: true });
52
+
53
+ app.register(replyFrom, {
54
+ base: 'http://backend-service:3001',
55
+ http2: false,
56
+ });
57
+
58
+ // Proxy with request/response manipulation
59
+ app.get('/users/:id', async (request, reply) => {
60
+ const { id } = request.params;
61
+
62
+ return reply.from(`/api/users/${id}`, {
63
+ // Modify request before forwarding
64
+ rewriteRequestHeaders: (originalReq, headers) => ({
65
+ ...headers,
66
+ 'x-request-id': request.id,
67
+ 'x-forwarded-for': request.ip,
68
+ }),
69
+ // Modify response before sending
70
+ onResponse: (request, reply, res) => {
71
+ reply.header('x-proxy', 'fastify');
72
+ reply.send(res);
73
+ },
74
+ });
75
+ });
76
+
77
+ // Conditional routing
78
+ app.all('/api/*', async (request, reply) => {
79
+ const upstream = selectUpstream(request);
80
+
81
+ return reply.from(request.url, {
82
+ base: upstream,
83
+ });
84
+ });
85
+
86
+ function selectUpstream(request) {
87
+ // Route to different backends based on request
88
+ if (request.headers['x-beta']) {
89
+ return 'http://beta-backend:3001';
90
+ }
91
+ return 'http://stable-backend:3001';
92
+ }
93
+ ```
94
+
95
+ ## API Gateway Pattern
96
+
97
+ Build an API gateway with multiple backends:
98
+
99
+ ```typescript
100
+ import Fastify from 'fastify';
101
+ import replyFrom from '@fastify/reply-from';
102
+
103
+ const app = Fastify({ logger: true });
104
+
105
+ // Configure multiple upstreams
106
+ const services = {
107
+ users: 'http://users-service:3001',
108
+ orders: 'http://orders-service:3002',
109
+ products: 'http://products-service:3003',
110
+ };
111
+
112
+ app.register(replyFrom);
113
+
114
+ // Route to user service
115
+ app.register(async function (fastify) {
116
+ fastify.all('/*', async (request, reply) => {
117
+ return reply.from(request.url.replace('/users', ''), {
118
+ base: services.users,
119
+ });
120
+ });
121
+ }, { prefix: '/users' });
122
+
123
+ // Route to orders service
124
+ app.register(async function (fastify) {
125
+ fastify.all('/*', async (request, reply) => {
126
+ return reply.from(request.url.replace('/orders', ''), {
127
+ base: services.orders,
128
+ });
129
+ });
130
+ }, { prefix: '/orders' });
131
+
132
+ // Route to products service
133
+ app.register(async function (fastify) {
134
+ fastify.all('/*', async (request, reply) => {
135
+ return reply.from(request.url.replace('/products', ''), {
136
+ base: services.products,
137
+ });
138
+ });
139
+ }, { prefix: '/products' });
140
+ ```
141
+
142
+ ## Request Body Handling
143
+
144
+ Handle request bodies when proxying:
145
+
146
+ ```typescript
147
+ app.post('/api/data', async (request, reply) => {
148
+ return reply.from('/data', {
149
+ body: request.body,
150
+ contentType: request.headers['content-type'],
151
+ });
152
+ });
153
+
154
+ // Stream large bodies
155
+ app.post('/upload', async (request, reply) => {
156
+ return reply.from('/upload', {
157
+ body: request.raw,
158
+ contentType: request.headers['content-type'],
159
+ });
160
+ });
161
+ ```
162
+
163
+ ## Error Handling
164
+
165
+ Handle upstream errors gracefully:
166
+
167
+ ```typescript
168
+ app.register(replyFrom, {
169
+ base: 'http://backend:3001',
170
+ // Called when upstream returns an error
171
+ onError: (reply, error) => {
172
+ reply.log.error({ err: error }, 'Proxy error');
173
+ reply.code(502).send({
174
+ error: 'Bad Gateway',
175
+ message: 'Upstream service unavailable',
176
+ });
177
+ },
178
+ });
179
+
180
+ // Custom error handling per route
181
+ app.get('/data', async (request, reply) => {
182
+ try {
183
+ return await reply.from('/data');
184
+ } catch (error) {
185
+ request.log.error({ err: error }, 'Failed to proxy request');
186
+ return reply.code(503).send({
187
+ error: 'Service Unavailable',
188
+ retryAfter: 30,
189
+ });
190
+ }
191
+ });
192
+ ```
193
+
194
+ ## WebSocket Proxying
195
+
196
+ Proxy WebSocket connections:
197
+
198
+ ```typescript
199
+ import Fastify from 'fastify';
200
+ import httpProxy from '@fastify/http-proxy';
201
+
202
+ const app = Fastify({ logger: true });
203
+
204
+ app.register(httpProxy, {
205
+ upstream: 'http://ws-backend:3001',
206
+ prefix: '/ws',
207
+ websocket: true,
208
+ });
209
+ ```
210
+
211
+ ## Timeout Configuration
212
+
213
+ Configure proxy timeouts:
214
+
215
+ ```typescript
216
+ app.register(replyFrom, {
217
+ base: 'http://backend:3001',
218
+ http: {
219
+ requestOptions: {
220
+ timeout: 30000, // 30 seconds
221
+ },
222
+ },
223
+ });
224
+ ```
225
+
226
+ ## Caching Proxied Responses
227
+
228
+ Add caching to proxied responses:
229
+
230
+ ```typescript
231
+ import { createCache } from 'async-cache-dedupe';
232
+
233
+ const cache = createCache({
234
+ ttl: 60,
235
+ storage: { type: 'memory' },
236
+ });
237
+
238
+ cache.define('proxyGet', async (url: string) => {
239
+ const response = await fetch(`http://backend:3001${url}`);
240
+ return response.json();
241
+ });
242
+
243
+ app.get('/cached/*', async (request, reply) => {
244
+ const data = await cache.proxyGet(request.url);
245
+ return data;
246
+ });
247
+ ```