@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,421 @@
1
+ ---
2
+ name: websockets
3
+ description: WebSocket support in Fastify
4
+ metadata:
5
+ tags: websockets, realtime, ws, socket
6
+ ---
7
+
8
+ # WebSocket Support
9
+
10
+ ## Using @fastify/websocket
11
+
12
+ Add WebSocket support to Fastify:
13
+
14
+ ```typescript
15
+ import Fastify from 'fastify';
16
+ import websocket from '@fastify/websocket';
17
+
18
+ const app = Fastify();
19
+
20
+ app.register(websocket);
21
+
22
+ app.get('/ws', { websocket: true }, (socket, request) => {
23
+ socket.on('message', (message) => {
24
+ const data = message.toString();
25
+ console.log('Received:', data);
26
+
27
+ // Echo back
28
+ socket.send(`Echo: ${data}`);
29
+ });
30
+
31
+ socket.on('close', () => {
32
+ console.log('Client disconnected');
33
+ });
34
+
35
+ socket.on('error', (error) => {
36
+ console.error('WebSocket error:', error);
37
+ });
38
+ });
39
+
40
+ await app.listen({ port: 3000 });
41
+ ```
42
+
43
+ ## WebSocket with Hooks
44
+
45
+ Use Fastify hooks with WebSocket routes:
46
+
47
+ ```typescript
48
+ app.register(async function wsRoutes(fastify) {
49
+ // This hook runs before WebSocket upgrade
50
+ fastify.addHook('preValidation', async (request, reply) => {
51
+ const token = request.headers.authorization;
52
+ if (!token) {
53
+ reply.code(401).send({ error: 'Unauthorized' });
54
+ return;
55
+ }
56
+ request.user = await verifyToken(token);
57
+ });
58
+
59
+ fastify.get('/ws', { websocket: true }, (socket, request) => {
60
+ console.log('Connected user:', request.user.id);
61
+
62
+ socket.on('message', (message) => {
63
+ // Handle authenticated messages
64
+ });
65
+ });
66
+ });
67
+ ```
68
+
69
+ ## Connection Options
70
+
71
+ Configure WebSocket server options:
72
+
73
+ ```typescript
74
+ app.register(websocket, {
75
+ options: {
76
+ maxPayload: 1048576, // 1MB max message size
77
+ clientTracking: true,
78
+ perMessageDeflate: {
79
+ zlibDeflateOptions: {
80
+ chunkSize: 1024,
81
+ memLevel: 7,
82
+ level: 3,
83
+ },
84
+ zlibInflateOptions: {
85
+ chunkSize: 10 * 1024,
86
+ },
87
+ },
88
+ },
89
+ });
90
+ ```
91
+
92
+ ## Broadcast to All Clients
93
+
94
+ Broadcast messages to connected clients:
95
+
96
+ ```typescript
97
+ const clients = new Set<WebSocket>();
98
+
99
+ app.get('/ws', { websocket: true }, (socket, request) => {
100
+ clients.add(socket);
101
+
102
+ socket.on('close', () => {
103
+ clients.delete(socket);
104
+ });
105
+
106
+ socket.on('message', (message) => {
107
+ // Broadcast to all other clients
108
+ for (const client of clients) {
109
+ if (client !== socket && client.readyState === WebSocket.OPEN) {
110
+ client.send(message);
111
+ }
112
+ }
113
+ });
114
+ });
115
+
116
+ // Broadcast from HTTP route
117
+ app.post('/broadcast', async (request) => {
118
+ const { message } = request.body;
119
+
120
+ for (const client of clients) {
121
+ if (client.readyState === WebSocket.OPEN) {
122
+ client.send(JSON.stringify({ type: 'broadcast', message }));
123
+ }
124
+ }
125
+
126
+ return { sent: clients.size };
127
+ });
128
+ ```
129
+
130
+ ## Rooms/Channels Pattern
131
+
132
+ Organize connections into rooms:
133
+
134
+ ```typescript
135
+ const rooms = new Map<string, Set<WebSocket>>();
136
+
137
+ function joinRoom(socket: WebSocket, roomId: string) {
138
+ if (!rooms.has(roomId)) {
139
+ rooms.set(roomId, new Set());
140
+ }
141
+ rooms.get(roomId)!.add(socket);
142
+ }
143
+
144
+ function leaveRoom(socket: WebSocket, roomId: string) {
145
+ rooms.get(roomId)?.delete(socket);
146
+ if (rooms.get(roomId)?.size === 0) {
147
+ rooms.delete(roomId);
148
+ }
149
+ }
150
+
151
+ function broadcastToRoom(roomId: string, message: string, exclude?: WebSocket) {
152
+ const room = rooms.get(roomId);
153
+ if (!room) return;
154
+
155
+ for (const client of room) {
156
+ if (client !== exclude && client.readyState === WebSocket.OPEN) {
157
+ client.send(message);
158
+ }
159
+ }
160
+ }
161
+
162
+ app.get('/ws/:roomId', { websocket: true }, (socket, request) => {
163
+ const { roomId } = request.params as { roomId: string };
164
+
165
+ joinRoom(socket, roomId);
166
+
167
+ socket.on('message', (message) => {
168
+ broadcastToRoom(roomId, message.toString(), socket);
169
+ });
170
+
171
+ socket.on('close', () => {
172
+ leaveRoom(socket, roomId);
173
+ });
174
+ });
175
+ ```
176
+
177
+ ## Structured Message Protocol
178
+
179
+ Use JSON for structured messages:
180
+
181
+ ```typescript
182
+ interface WSMessage {
183
+ type: string;
184
+ payload?: unknown;
185
+ id?: string;
186
+ }
187
+
188
+ app.get('/ws', { websocket: true }, (socket, request) => {
189
+ function send(message: WSMessage) {
190
+ socket.send(JSON.stringify(message));
191
+ }
192
+
193
+ socket.on('message', (raw) => {
194
+ let message: WSMessage;
195
+
196
+ try {
197
+ message = JSON.parse(raw.toString());
198
+ } catch {
199
+ send({ type: 'error', payload: 'Invalid JSON' });
200
+ return;
201
+ }
202
+
203
+ switch (message.type) {
204
+ case 'ping':
205
+ send({ type: 'pong', id: message.id });
206
+ break;
207
+
208
+ case 'subscribe':
209
+ handleSubscribe(socket, message.payload);
210
+ send({ type: 'subscribed', payload: message.payload, id: message.id });
211
+ break;
212
+
213
+ case 'message':
214
+ handleMessage(socket, message.payload);
215
+ break;
216
+
217
+ default:
218
+ send({ type: 'error', payload: 'Unknown message type' });
219
+ }
220
+ });
221
+ });
222
+ ```
223
+
224
+ ## Heartbeat/Ping-Pong
225
+
226
+ Keep connections alive:
227
+
228
+ ```typescript
229
+ const HEARTBEAT_INTERVAL = 30000;
230
+ const clients = new Map<WebSocket, { isAlive: boolean }>();
231
+
232
+ app.get('/ws', { websocket: true }, (socket, request) => {
233
+ clients.set(socket, { isAlive: true });
234
+
235
+ socket.on('pong', () => {
236
+ const client = clients.get(socket);
237
+ if (client) client.isAlive = true;
238
+ });
239
+
240
+ socket.on('close', () => {
241
+ clients.delete(socket);
242
+ });
243
+ });
244
+
245
+ // Heartbeat interval
246
+ setInterval(() => {
247
+ for (const [socket, state] of clients) {
248
+ if (!state.isAlive) {
249
+ socket.terminate();
250
+ clients.delete(socket);
251
+ continue;
252
+ }
253
+
254
+ state.isAlive = false;
255
+ socket.ping();
256
+ }
257
+ }, HEARTBEAT_INTERVAL);
258
+ ```
259
+
260
+ ## Authentication
261
+
262
+ Authenticate WebSocket connections:
263
+
264
+ ```typescript
265
+ app.get('/ws', {
266
+ websocket: true,
267
+ preValidation: async (request, reply) => {
268
+ // Authenticate via query parameter or header
269
+ const token = request.query.token || request.headers.authorization?.replace('Bearer ', '');
270
+
271
+ if (!token) {
272
+ reply.code(401).send({ error: 'Token required' });
273
+ return;
274
+ }
275
+
276
+ try {
277
+ request.user = await verifyToken(token);
278
+ } catch {
279
+ reply.code(401).send({ error: 'Invalid token' });
280
+ }
281
+ },
282
+ }, (socket, request) => {
283
+ console.log('Authenticated user:', request.user);
284
+
285
+ socket.on('message', (message) => {
286
+ // Handle authenticated messages
287
+ });
288
+ });
289
+ ```
290
+
291
+ ## Error Handling
292
+
293
+ Handle WebSocket errors properly:
294
+
295
+ ```typescript
296
+ app.get('/ws', { websocket: true }, (socket, request) => {
297
+ socket.on('error', (error) => {
298
+ request.log.error({ err: error }, 'WebSocket error');
299
+ });
300
+
301
+ socket.on('message', async (raw) => {
302
+ try {
303
+ const message = JSON.parse(raw.toString());
304
+ const result = await processMessage(message);
305
+ socket.send(JSON.stringify({ success: true, result }));
306
+ } catch (error) {
307
+ request.log.error({ err: error }, 'Message processing error');
308
+ socket.send(JSON.stringify({
309
+ success: false,
310
+ error: error.message,
311
+ }));
312
+ }
313
+ });
314
+ });
315
+ ```
316
+
317
+ ## Rate Limiting WebSocket Messages
318
+
319
+ Limit message frequency:
320
+
321
+ ```typescript
322
+ const rateLimits = new Map<WebSocket, { count: number; resetAt: number }>();
323
+
324
+ function checkRateLimit(socket: WebSocket, limit: number, window: number): boolean {
325
+ const now = Date.now();
326
+ let state = rateLimits.get(socket);
327
+
328
+ if (!state || now > state.resetAt) {
329
+ state = { count: 0, resetAt: now + window };
330
+ rateLimits.set(socket, state);
331
+ }
332
+
333
+ state.count++;
334
+
335
+ if (state.count > limit) {
336
+ return false;
337
+ }
338
+
339
+ return true;
340
+ }
341
+
342
+ app.get('/ws', { websocket: true }, (socket, request) => {
343
+ socket.on('message', (message) => {
344
+ if (!checkRateLimit(socket, 100, 60000)) {
345
+ socket.send(JSON.stringify({ error: 'Rate limit exceeded' }));
346
+ return;
347
+ }
348
+
349
+ // Process message
350
+ });
351
+
352
+ socket.on('close', () => {
353
+ rateLimits.delete(socket);
354
+ });
355
+ });
356
+ ```
357
+
358
+ ## Graceful Shutdown
359
+
360
+ Close WebSocket connections on shutdown:
361
+
362
+ ```typescript
363
+ import closeWithGrace from 'close-with-grace';
364
+
365
+ const connections = new Set<WebSocket>();
366
+
367
+ app.get('/ws', { websocket: true }, (socket, request) => {
368
+ connections.add(socket);
369
+
370
+ socket.on('close', () => {
371
+ connections.delete(socket);
372
+ });
373
+ });
374
+
375
+ closeWithGrace({ delay: 5000 }, async ({ signal }) => {
376
+ // Notify clients
377
+ for (const socket of connections) {
378
+ if (socket.readyState === WebSocket.OPEN) {
379
+ socket.send(JSON.stringify({ type: 'shutdown', message: 'Server is shutting down' }));
380
+ socket.close(1001, 'Server shutdown');
381
+ }
382
+ }
383
+
384
+ await app.close();
385
+ });
386
+ ```
387
+
388
+ ## Full-Duplex Stream Pattern
389
+
390
+ Use WebSocket for streaming data:
391
+
392
+ ```typescript
393
+ app.get('/ws/stream', { websocket: true }, async (socket, request) => {
394
+ const stream = createDataStream();
395
+
396
+ stream.on('data', (data) => {
397
+ if (socket.readyState === WebSocket.OPEN) {
398
+ socket.send(JSON.stringify({ type: 'data', payload: data }));
399
+ }
400
+ });
401
+
402
+ stream.on('end', () => {
403
+ socket.send(JSON.stringify({ type: 'end' }));
404
+ socket.close();
405
+ });
406
+
407
+ socket.on('message', (message) => {
408
+ const { type, payload } = JSON.parse(message.toString());
409
+
410
+ if (type === 'pause') {
411
+ stream.pause();
412
+ } else if (type === 'resume') {
413
+ stream.resume();
414
+ }
415
+ });
416
+
417
+ socket.on('close', () => {
418
+ stream.destroy();
419
+ });
420
+ });
421
+ ```
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "mcollina/fastify-best-practices",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "summary": "Guides development of Fastify Node.js backend servers and REST APIs using TypeScript or JavaScript. Use when building, configuring, or debugging a Fastify application — including defining routes, implementing plugins, setting up JSON Schema validation, handling errors, optimising performance, managing authentication, configuring CORS and security headers, integrating databases, working with WebSockets, and deploying to production. Covers the full Fastify request lifecycle (hooks, serialization, logging with Pino) and TypeScript integration via strip types. Trigger terms: Fastify, Node.js server, REST API, API routes, backend framework, fastify.config, server.ts, app.ts.",
6
+ "skills": {
7
+ "fastify-best-practices": {
8
+ "path": "SKILL.md"
9
+ }
10
+ }
11
+ }
@@ -11,8 +11,9 @@ Use this skill when working in the Agent Toolkit repository.
11
11
 
12
12
  - Keep the repository public-safe: no internal company names, private URLs, tokens, or credentials.
13
13
  - Keep the installer focused on Claude Code, Codex CLI, OpenCode and Gemini CLI.
14
- - Keep bundled skills organized by scope under `skills/<scope>/<skill-name>/SKILL.md`.
15
- - Keep third-party skills out of `skills/`; install them through CLI-backed tool installers.
14
+ - Keep bundled skills organized by package under `skills/<package>/<skill-name>/SKILL.md`.
15
+ - Prefer external installers for third-party skills, but third-party skills may be bundled when the user explicitly asks for vendoring and the upstream license allows copying.
16
+ - When bundling third-party skills, preserve the upstream license, add clear attribution, and document source ownership in README.
16
17
  - Install skills flat into runtime skill directories using the skill directory name.
17
18
  - Prefer configurable sources over hard-coded local paths.
18
19
  - Keep external tool defaults pinned in `tools.lock.json`; do not reintroduce mutable defaults like `@latest`.
@@ -39,30 +40,31 @@ When changing `bin/agent-toolkit.ts`, `src/**/*.ts` or `setup-agent-toolkit.sh`:
39
40
 
40
41
  ## Skill Changes
41
42
 
42
- When adding a personal skill:
43
+ When adding a bundled skill:
43
44
 
44
- 1. Create `skills/<scope>/<skill-name>/SKILL.md`.
45
+ 1. Create `skills/<package>/<skill-name>/SKILL.md`.
45
46
  2. Use concise YAML frontmatter with `name` and `description`.
46
47
  3. Keep `SKILL.md` procedural and short.
47
48
  4. Move large examples or detailed references into `references/`.
48
49
  5. Do not add README or changelog files inside a skill directory.
49
50
 
50
- When adding third-party skills, add a dedicated installer or extend an existing
51
- external-skill installer, pin sources in `tools.lock.json`, and document the
52
- license/source in README.
51
+ When adding third-party skills, either add a dedicated external installer or
52
+ bundle the skill only when explicitly requested and allowed by license. Preserve
53
+ the upstream license text, add attribution with source URL and commit, and
54
+ document the license/source in README.
53
55
 
54
56
  ## Verification
55
57
 
56
58
  Run:
57
59
 
58
60
  ```bash
59
- rtk npm run check
60
- rtk npm run security
61
- rtk npm run lint
62
- rtk npm run build
63
- rtk npm run typecheck
64
- rtk npm run test:unit
65
- rtk npm run test:integration
61
+ rtk pnpm run check
62
+ rtk pnpm run security
63
+ rtk pnpm run lint
64
+ rtk pnpm run build
65
+ rtk pnpm run typecheck
66
+ rtk pnpm run test:unit
67
+ rtk pnpm run test:integration
66
68
  rtk rg -n "private-company-or-old-tooling-pattern" .
67
69
  rtk graphify update .
68
70
  ```