@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,217 @@
1
+ ---
2
+ name: configuration
3
+ description: Application configuration in Fastify using env-schema
4
+ metadata:
5
+ tags: configuration, environment, env, settings, env-schema
6
+ ---
7
+
8
+ # Application Configuration
9
+
10
+ ## Use env-schema for Configuration
11
+
12
+ **Always use `env-schema` for configuration validation.** It provides JSON Schema validation for environment variables with sensible defaults.
13
+
14
+ ```typescript
15
+ import Fastify from 'fastify';
16
+ import envSchema from 'env-schema';
17
+ import { Type, type Static } from '@sinclair/typebox';
18
+
19
+ const schema = Type.Object({
20
+ PORT: Type.Number({ default: 3000 }),
21
+ HOST: Type.String({ default: '0.0.0.0' }),
22
+ DATABASE_URL: Type.String(),
23
+ JWT_SECRET: Type.String({ minLength: 32 }),
24
+ LOG_LEVEL: Type.Union([
25
+ Type.Literal('trace'),
26
+ Type.Literal('debug'),
27
+ Type.Literal('info'),
28
+ Type.Literal('warn'),
29
+ Type.Literal('error'),
30
+ Type.Literal('fatal'),
31
+ ], { default: 'info' }),
32
+ });
33
+
34
+ type Config = Static<typeof schema>;
35
+
36
+ const config = envSchema<Config>({
37
+ schema,
38
+ dotenv: true, // Load from .env file
39
+ });
40
+
41
+ const app = Fastify({
42
+ logger: { level: config.LOG_LEVEL },
43
+ });
44
+
45
+ app.decorate('config', config);
46
+
47
+ declare module 'fastify' {
48
+ interface FastifyInstance {
49
+ config: Config;
50
+ }
51
+ }
52
+
53
+ await app.listen({ port: config.PORT, host: config.HOST });
54
+ ```
55
+
56
+ ## Configuration as Plugin
57
+
58
+ Encapsulate configuration in a plugin for reuse:
59
+
60
+ ```typescript
61
+ import fp from 'fastify-plugin';
62
+ import envSchema from 'env-schema';
63
+ import { Type, type Static } from '@sinclair/typebox';
64
+
65
+ const schema = Type.Object({
66
+ PORT: Type.Number({ default: 3000 }),
67
+ HOST: Type.String({ default: '0.0.0.0' }),
68
+ DATABASE_URL: Type.String(),
69
+ JWT_SECRET: Type.String({ minLength: 32 }),
70
+ LOG_LEVEL: Type.String({ default: 'info' }),
71
+ });
72
+
73
+ type Config = Static<typeof schema>;
74
+
75
+ declare module 'fastify' {
76
+ interface FastifyInstance {
77
+ config: Config;
78
+ }
79
+ }
80
+
81
+ export default fp(async function configPlugin(fastify) {
82
+ const config = envSchema<Config>({
83
+ schema,
84
+ dotenv: true,
85
+ });
86
+
87
+ fastify.decorate('config', config);
88
+ }, {
89
+ name: 'config',
90
+ });
91
+ ```
92
+
93
+ ## Secrets Management
94
+
95
+ Handle secrets securely:
96
+
97
+ ```typescript
98
+ // Never log secrets
99
+ const app = Fastify({
100
+ logger: {
101
+ level: config.LOG_LEVEL,
102
+ redact: ['req.headers.authorization', '*.password', '*.secret', '*.apiKey'],
103
+ },
104
+ });
105
+
106
+ // For production, use secret managers (AWS Secrets Manager, Vault, etc.)
107
+ // Pass secrets through environment variables - never commit them
108
+ ```
109
+
110
+ ## Feature Flags
111
+
112
+ Implement feature flags via environment variables:
113
+
114
+ ```typescript
115
+ import { Type, type Static } from '@sinclair/typebox';
116
+
117
+ const schema = Type.Object({
118
+ // ... other config
119
+ FEATURE_NEW_DASHBOARD: Type.Boolean({ default: false }),
120
+ FEATURE_BETA_API: Type.Boolean({ default: false }),
121
+ });
122
+
123
+ type Config = Static<typeof schema>;
124
+
125
+ const config = envSchema<Config>({ schema, dotenv: true });
126
+
127
+ // Use in routes
128
+ app.get('/dashboard', async (request) => {
129
+ if (app.config.FEATURE_NEW_DASHBOARD) {
130
+ return { version: 'v2', data: await getNewDashboardData() };
131
+ }
132
+ return { version: 'v1', data: await getOldDashboardData() };
133
+ });
134
+ ```
135
+
136
+ ## Anti-Patterns to Avoid
137
+
138
+ ### NEVER use configuration files
139
+
140
+ ```typescript
141
+ // ❌ NEVER DO THIS - configuration files are an antipattern
142
+ import config from './config/production.json';
143
+
144
+ // ❌ NEVER DO THIS - per-environment config files
145
+ const env = process.env.NODE_ENV || 'development';
146
+ const config = await import(`./config/${env}.js`);
147
+ ```
148
+
149
+ Configuration files lead to:
150
+ - Security risks (secrets in files)
151
+ - Deployment complexity
152
+ - Environment drift
153
+ - Difficult secret rotation
154
+
155
+ ### NEVER use per-environment configuration
156
+
157
+ ```typescript
158
+ // ❌ NEVER DO THIS
159
+ const configs = {
160
+ development: { logLevel: 'debug' },
161
+ production: { logLevel: 'info' },
162
+ test: { logLevel: 'silent' },
163
+ };
164
+ const config = configs[process.env.NODE_ENV];
165
+ ```
166
+
167
+ Instead, use a single configuration source (environment variables) with sensible defaults. The environment controls the values, not conditional code.
168
+
169
+ ### Use specific environment variables, not NODE_ENV
170
+
171
+ ```typescript
172
+ // ❌ AVOID checking NODE_ENV
173
+ if (process.env.NODE_ENV === 'production') {
174
+ // do something
175
+ }
176
+
177
+ // ✅ BETTER - use explicit feature flags or configuration
178
+ if (app.config.ENABLE_DETAILED_LOGGING) {
179
+ // do something
180
+ }
181
+ ```
182
+
183
+ ## Dynamic Configuration
184
+
185
+ For configuration that needs to change without restart, fetch from an external service:
186
+
187
+ ```typescript
188
+ interface DynamicConfig {
189
+ rateLimit: number;
190
+ maintenanceMode: boolean;
191
+ }
192
+
193
+ let dynamicConfig: DynamicConfig = {
194
+ rateLimit: 100,
195
+ maintenanceMode: false,
196
+ };
197
+
198
+ async function refreshConfig() {
199
+ try {
200
+ const newConfig = await fetchConfigFromService();
201
+ dynamicConfig = newConfig;
202
+ app.log.info('Configuration refreshed');
203
+ } catch (error) {
204
+ app.log.error({ err: error }, 'Failed to refresh configuration');
205
+ }
206
+ }
207
+
208
+ // Refresh periodically
209
+ setInterval(refreshConfig, 60000);
210
+
211
+ // Use in hooks
212
+ app.addHook('onRequest', async (request, reply) => {
213
+ if (dynamicConfig.maintenanceMode && !request.url.startsWith('/health')) {
214
+ reply.code(503).send({ error: 'Service under maintenance' });
215
+ }
216
+ });
217
+ ```
@@ -0,0 +1,387 @@
1
+ ---
2
+ name: content-type
3
+ description: Content type parsing in Fastify
4
+ metadata:
5
+ tags: content-type, parsing, body, multipart, json
6
+ ---
7
+
8
+ # Content Type Parsing
9
+
10
+ ## Default Content Type Parsers
11
+
12
+ Fastify includes parsers for common content types:
13
+
14
+ ```typescript
15
+ import Fastify from 'fastify';
16
+
17
+ const app = Fastify();
18
+
19
+ // Built-in parsers:
20
+ // - application/json
21
+ // - text/plain
22
+
23
+ app.post('/json', async (request) => {
24
+ // request.body is parsed JSON object
25
+ return { received: request.body };
26
+ });
27
+
28
+ app.post('/text', async (request) => {
29
+ // request.body is string for text/plain
30
+ return { text: request.body };
31
+ });
32
+ ```
33
+
34
+ ## Custom Content Type Parsers
35
+
36
+ Add parsers for additional content types:
37
+
38
+ ```typescript
39
+ // Parse application/x-www-form-urlencoded
40
+ app.addContentTypeParser(
41
+ 'application/x-www-form-urlencoded',
42
+ { parseAs: 'string' },
43
+ (request, body, done) => {
44
+ const parsed = new URLSearchParams(body);
45
+ done(null, Object.fromEntries(parsed));
46
+ },
47
+ );
48
+
49
+ // Async parser
50
+ app.addContentTypeParser(
51
+ 'application/x-www-form-urlencoded',
52
+ { parseAs: 'string' },
53
+ async (request, body) => {
54
+ const parsed = new URLSearchParams(body);
55
+ return Object.fromEntries(parsed);
56
+ },
57
+ );
58
+ ```
59
+
60
+ ## XML Parsing
61
+
62
+ Parse XML content:
63
+
64
+ ```typescript
65
+ import { XMLParser } from 'fast-xml-parser';
66
+
67
+ const xmlParser = new XMLParser({
68
+ ignoreAttributes: false,
69
+ attributeNamePrefix: '@_',
70
+ });
71
+
72
+ app.addContentTypeParser(
73
+ 'application/xml',
74
+ { parseAs: 'string' },
75
+ async (request, body) => {
76
+ return xmlParser.parse(body);
77
+ },
78
+ );
79
+
80
+ app.addContentTypeParser(
81
+ 'text/xml',
82
+ { parseAs: 'string' },
83
+ async (request, body) => {
84
+ return xmlParser.parse(body);
85
+ },
86
+ );
87
+
88
+ app.post('/xml', async (request) => {
89
+ // request.body is parsed XML as JavaScript object
90
+ return { data: request.body };
91
+ });
92
+ ```
93
+
94
+ ## Multipart Form Data
95
+
96
+ Use @fastify/multipart for file uploads. **Configure these critical options:**
97
+
98
+ ```typescript
99
+ import fastifyMultipart from '@fastify/multipart';
100
+
101
+ app.register(fastifyMultipart, {
102
+ // CRITICAL: Always set explicit limits
103
+ limits: {
104
+ fieldNameSize: 100, // Max field name size in bytes
105
+ fieldSize: 1024 * 1024, // Max field value size (1MB)
106
+ fields: 10, // Max number of non-file fields
107
+ fileSize: 10 * 1024 * 1024, // Max file size (10MB)
108
+ files: 5, // Max number of files
109
+ headerPairs: 2000, // Max number of header pairs
110
+ parts: 1000, // Max number of parts (fields + files)
111
+ },
112
+ // IMPORTANT: Throw on limit exceeded (default is to truncate silently!)
113
+ throwFileSizeLimit: true,
114
+ // Attach all fields to request.body for easier access
115
+ attachFieldsToBody: true,
116
+ // Only accept specific file types (security!)
117
+ // onFile: async (part) => {
118
+ // if (!['image/jpeg', 'image/png'].includes(part.mimetype)) {
119
+ // throw new Error('Invalid file type');
120
+ // }
121
+ // },
122
+ });
123
+
124
+ // Handle file upload
125
+ app.post('/upload', async (request, reply) => {
126
+ const data = await request.file();
127
+
128
+ if (!data) {
129
+ return reply.code(400).send({ error: 'No file uploaded' });
130
+ }
131
+
132
+ // data.file is a stream
133
+ const buffer = await data.toBuffer();
134
+
135
+ return {
136
+ filename: data.filename,
137
+ mimetype: data.mimetype,
138
+ size: buffer.length,
139
+ };
140
+ });
141
+
142
+ // Handle multiple files
143
+ app.post('/upload-multiple', async (request) => {
144
+ const files = [];
145
+
146
+ for await (const part of request.files()) {
147
+ const buffer = await part.toBuffer();
148
+ files.push({
149
+ filename: part.filename,
150
+ mimetype: part.mimetype,
151
+ size: buffer.length,
152
+ });
153
+ }
154
+
155
+ return { files };
156
+ });
157
+
158
+ // Handle mixed form data
159
+ app.post('/form', async (request) => {
160
+ const parts = request.parts();
161
+ const fields: Record<string, string> = {};
162
+ const files: Array<{ name: string; size: number }> = [];
163
+
164
+ for await (const part of parts) {
165
+ if (part.type === 'file') {
166
+ const buffer = await part.toBuffer();
167
+ files.push({ name: part.filename, size: buffer.length });
168
+ } else {
169
+ fields[part.fieldname] = part.value as string;
170
+ }
171
+ }
172
+
173
+ return { fields, files };
174
+ });
175
+ ```
176
+
177
+ ## Stream Processing
178
+
179
+ Process body as stream for large payloads:
180
+
181
+ ```typescript
182
+ import { pipeline } from 'node:stream/promises';
183
+ import { createWriteStream } from 'node:fs';
184
+
185
+ // Add parser that returns stream
186
+ app.addContentTypeParser(
187
+ 'application/octet-stream',
188
+ async (request, payload) => {
189
+ return payload; // Return stream directly
190
+ },
191
+ );
192
+
193
+ app.post('/upload-stream', async (request, reply) => {
194
+ const destination = createWriteStream('./upload.bin');
195
+
196
+ await pipeline(request.body, destination);
197
+
198
+ return { success: true };
199
+ });
200
+ ```
201
+
202
+ ## Custom JSON Parser
203
+
204
+ Replace the default JSON parser:
205
+
206
+ ```typescript
207
+ // Remove default parser
208
+ app.removeContentTypeParser('application/json');
209
+
210
+ // Add custom parser with error handling
211
+ app.addContentTypeParser(
212
+ 'application/json',
213
+ { parseAs: 'string' },
214
+ async (request, body) => {
215
+ try {
216
+ return JSON.parse(body);
217
+ } catch (error) {
218
+ throw {
219
+ statusCode: 400,
220
+ code: 'INVALID_JSON',
221
+ message: 'Invalid JSON payload',
222
+ };
223
+ }
224
+ },
225
+ );
226
+ ```
227
+
228
+ ## Content Type with Parameters
229
+
230
+ Handle content types with parameters:
231
+
232
+ ```typescript
233
+ // Match content type with any charset
234
+ app.addContentTypeParser(
235
+ 'application/json; charset=utf-8',
236
+ { parseAs: 'string' },
237
+ async (request, body) => {
238
+ return JSON.parse(body);
239
+ },
240
+ );
241
+
242
+ // Use regex for flexible matching
243
+ app.addContentTypeParser(
244
+ /^application\/.*\+json$/,
245
+ { parseAs: 'string' },
246
+ async (request, body) => {
247
+ return JSON.parse(body);
248
+ },
249
+ );
250
+ ```
251
+
252
+ ## Catch-All Parser
253
+
254
+ Handle unknown content types:
255
+
256
+ ```typescript
257
+ app.addContentTypeParser('*', async (request, payload) => {
258
+ const chunks: Buffer[] = [];
259
+
260
+ for await (const chunk of payload) {
261
+ chunks.push(chunk);
262
+ }
263
+
264
+ const buffer = Buffer.concat(chunks);
265
+
266
+ // Try to determine content type
267
+ const contentType = request.headers['content-type'];
268
+
269
+ if (contentType?.includes('json')) {
270
+ return JSON.parse(buffer.toString('utf-8'));
271
+ }
272
+
273
+ if (contentType?.includes('text')) {
274
+ return buffer.toString('utf-8');
275
+ }
276
+
277
+ return buffer;
278
+ });
279
+ ```
280
+
281
+ ## Body Limit Configuration
282
+
283
+ Configure body size limits:
284
+
285
+ ```typescript
286
+ // Global limit
287
+ const app = Fastify({
288
+ bodyLimit: 1048576, // 1MB
289
+ });
290
+
291
+ // Per-route limit
292
+ app.post('/large-upload', {
293
+ bodyLimit: 52428800, // 50MB for this route
294
+ }, async (request) => {
295
+ return { size: JSON.stringify(request.body).length };
296
+ });
297
+
298
+ // Per content type limit
299
+ app.addContentTypeParser('application/json', {
300
+ parseAs: 'string',
301
+ bodyLimit: 2097152, // 2MB for JSON
302
+ }, async (request, body) => {
303
+ return JSON.parse(body);
304
+ });
305
+ ```
306
+
307
+ ## Protocol Buffers
308
+
309
+ Parse protobuf content:
310
+
311
+ ```typescript
312
+ import protobuf from 'protobufjs';
313
+
314
+ const root = await protobuf.load('./schema.proto');
315
+ const MessageType = root.lookupType('package.MessageType');
316
+
317
+ app.addContentTypeParser(
318
+ 'application/x-protobuf',
319
+ { parseAs: 'buffer' },
320
+ async (request, body) => {
321
+ const message = MessageType.decode(body);
322
+ return MessageType.toObject(message);
323
+ },
324
+ );
325
+ ```
326
+
327
+ ## Form Data with @fastify/formbody
328
+
329
+ Simple form parsing:
330
+
331
+ ```typescript
332
+ import formbody from '@fastify/formbody';
333
+
334
+ app.register(formbody);
335
+
336
+ app.post('/form', async (request) => {
337
+ // request.body is parsed form data
338
+ const { name, email } = request.body as { name: string; email: string };
339
+ return { name, email };
340
+ });
341
+ ```
342
+
343
+ ## Content Negotiation
344
+
345
+ Handle different request formats:
346
+
347
+ ```typescript
348
+ app.post('/data', async (request, reply) => {
349
+ const contentType = request.headers['content-type'];
350
+
351
+ // Body is already parsed by the appropriate parser
352
+ const data = request.body;
353
+
354
+ // Respond based on Accept header
355
+ const accept = request.headers.accept;
356
+
357
+ if (accept?.includes('application/xml')) {
358
+ reply.type('application/xml');
359
+ return `<data>${JSON.stringify(data)}</data>`;
360
+ }
361
+
362
+ reply.type('application/json');
363
+ return data;
364
+ });
365
+ ```
366
+
367
+ ## Validation After Parsing
368
+
369
+ Validate parsed content:
370
+
371
+ ```typescript
372
+ app.post('/users', {
373
+ schema: {
374
+ body: {
375
+ type: 'object',
376
+ properties: {
377
+ name: { type: 'string', minLength: 1 },
378
+ email: { type: 'string', format: 'email' },
379
+ },
380
+ required: ['name', 'email'],
381
+ },
382
+ },
383
+ }, async (request) => {
384
+ // Body is parsed AND validated
385
+ return request.body;
386
+ });
387
+ ```