@girardmedia/bootspring 1.1.0
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.
- package/LICENSE +21 -0
- package/README.md +255 -0
- package/agents/README.md +93 -0
- package/agents/api-expert/context.md +416 -0
- package/agents/architecture-expert/context.md +454 -0
- package/agents/backend-expert/context.md +483 -0
- package/agents/code-review-expert/context.md +365 -0
- package/agents/database-expert/context.md +250 -0
- package/agents/devops-expert/context.md +446 -0
- package/agents/frontend-expert/context.md +364 -0
- package/agents/index.js +140 -0
- package/agents/performance-expert/context.md +377 -0
- package/agents/security-expert/context.md +343 -0
- package/agents/testing-expert/context.md +414 -0
- package/agents/ui-ux-expert/context.md +448 -0
- package/agents/vercel-expert/context.md +426 -0
- package/bin/bootspring.js +310 -0
- package/cli/agent.js +337 -0
- package/cli/context.js +194 -0
- package/cli/dashboard.js +150 -0
- package/cli/generate.js +294 -0
- package/cli/init.js +410 -0
- package/cli/loop.js +421 -0
- package/cli/mcp.js +241 -0
- package/cli/memory.js +303 -0
- package/cli/orchestrator.js +400 -0
- package/cli/plugin.js +451 -0
- package/cli/quality.js +332 -0
- package/cli/skill.js +369 -0
- package/cli/task.js +628 -0
- package/cli/telemetry.js +114 -0
- package/cli/todo.js +614 -0
- package/cli/update.js +312 -0
- package/core/config.js +245 -0
- package/core/context.js +329 -0
- package/core/entitlements.js +209 -0
- package/core/index.js +43 -0
- package/core/policies.js +68 -0
- package/core/telemetry.js +247 -0
- package/core/utils.js +380 -0
- package/dashboard/server.js +818 -0
- package/docs/integrations/claude-code.md +42 -0
- package/docs/integrations/codex.md +42 -0
- package/docs/mcp-api-platform.md +102 -0
- package/generators/generate.js +598 -0
- package/generators/index.js +18 -0
- package/hooks/context-detector.js +177 -0
- package/hooks/index.js +35 -0
- package/hooks/prompt-enhancer.js +289 -0
- package/intelligence/git-memory.js +551 -0
- package/intelligence/index.js +59 -0
- package/intelligence/orchestrator.js +964 -0
- package/intelligence/prd.js +447 -0
- package/intelligence/recommendation-weights.json +18 -0
- package/intelligence/recommendations.js +234 -0
- package/mcp/capabilities.js +71 -0
- package/mcp/contracts/mcp-contract.v1.json +497 -0
- package/mcp/registry.js +213 -0
- package/mcp/response-formatter.js +462 -0
- package/mcp/server.js +99 -0
- package/mcp/tools/agent-tool.js +137 -0
- package/mcp/tools/capabilities-tool.js +54 -0
- package/mcp/tools/context-tool.js +49 -0
- package/mcp/tools/dashboard-tool.js +58 -0
- package/mcp/tools/generate-tool.js +46 -0
- package/mcp/tools/loop-tool.js +134 -0
- package/mcp/tools/memory-tool.js +180 -0
- package/mcp/tools/orchestrator-tool.js +232 -0
- package/mcp/tools/plugin-tool.js +76 -0
- package/mcp/tools/quality-tool.js +47 -0
- package/mcp/tools/skill-tool.js +233 -0
- package/mcp/tools/telemetry-tool.js +95 -0
- package/mcp/tools/todo-tool.js +133 -0
- package/package.json +98 -0
- package/plugins/index.js +141 -0
- package/quality/index.js +380 -0
- package/quality/lint-budgets.json +19 -0
- package/skills/index.js +787 -0
- package/skills/patterns/README.md +163 -0
- package/skills/patterns/api/route-handler.md +217 -0
- package/skills/patterns/api/server-action.md +249 -0
- package/skills/patterns/auth/clerk.md +132 -0
- package/skills/patterns/database/prisma.md +180 -0
- package/skills/patterns/payments/stripe.md +272 -0
- package/skills/patterns/security/validation.md +268 -0
- package/skills/patterns/testing/vitest.md +307 -0
- package/templates/bootspring.config.js +83 -0
- package/templates/mcp.json +9 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
# API Expert Agent
|
|
2
|
+
|
|
3
|
+
## Role
|
|
4
|
+
Specialized in API design, RESTful patterns, GraphQL, route handler implementation, and API documentation for modern web applications.
|
|
5
|
+
|
|
6
|
+
## Core Expertise
|
|
7
|
+
|
|
8
|
+
### RESTful API Design
|
|
9
|
+
|
|
10
|
+
#### Resource Naming
|
|
11
|
+
```
|
|
12
|
+
# Good: Plural nouns, hierarchical
|
|
13
|
+
GET /api/users
|
|
14
|
+
GET /api/users/{id}
|
|
15
|
+
GET /api/users/{id}/posts
|
|
16
|
+
POST /api/users
|
|
17
|
+
PATCH /api/users/{id}
|
|
18
|
+
DELETE /api/users/{id}
|
|
19
|
+
|
|
20
|
+
# Avoid: Verbs in URLs
|
|
21
|
+
GET /api/getUsers ❌
|
|
22
|
+
POST /api/createUser ❌
|
|
23
|
+
POST /api/users/delete/1 ❌
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
#### HTTP Methods & Status Codes
|
|
27
|
+
```typescript
|
|
28
|
+
// GET: Retrieve resource(s)
|
|
29
|
+
// 200 OK - Success
|
|
30
|
+
// 404 Not Found - Resource doesn't exist
|
|
31
|
+
|
|
32
|
+
// POST: Create resource
|
|
33
|
+
// 201 Created - Resource created
|
|
34
|
+
// 400 Bad Request - Validation failed
|
|
35
|
+
// 409 Conflict - Duplicate (unique constraint)
|
|
36
|
+
|
|
37
|
+
// PATCH: Partial update
|
|
38
|
+
// 200 OK - Updated
|
|
39
|
+
// 404 Not Found - Resource doesn't exist
|
|
40
|
+
|
|
41
|
+
// PUT: Full replacement
|
|
42
|
+
// 200 OK - Replaced
|
|
43
|
+
// 201 Created - Created if didn't exist
|
|
44
|
+
|
|
45
|
+
// DELETE: Remove resource
|
|
46
|
+
// 204 No Content - Deleted
|
|
47
|
+
// 404 Not Found - Resource doesn't exist
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Route Handler Patterns
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
// app/api/posts/route.ts
|
|
54
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
55
|
+
import { auth } from '@clerk/nextjs/server';
|
|
56
|
+
import { prisma } from '@/lib/prisma';
|
|
57
|
+
import { z } from 'zod';
|
|
58
|
+
|
|
59
|
+
// Pagination helper
|
|
60
|
+
function parsePagination(searchParams: URLSearchParams) {
|
|
61
|
+
return {
|
|
62
|
+
page: Math.max(1, parseInt(searchParams.get('page') ?? '1')),
|
|
63
|
+
limit: Math.min(100, Math.max(1, parseInt(searchParams.get('limit') ?? '20'))),
|
|
64
|
+
sort: searchParams.get('sort') ?? 'createdAt',
|
|
65
|
+
order: (searchParams.get('order') ?? 'desc') as 'asc' | 'desc',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// GET with filtering, sorting, pagination
|
|
70
|
+
export async function GET(request: NextRequest) {
|
|
71
|
+
const { searchParams } = new URL(request.url);
|
|
72
|
+
const { page, limit, sort, order } = parsePagination(searchParams);
|
|
73
|
+
|
|
74
|
+
// Filters
|
|
75
|
+
const status = searchParams.get('status');
|
|
76
|
+
const authorId = searchParams.get('authorId');
|
|
77
|
+
const search = searchParams.get('q');
|
|
78
|
+
|
|
79
|
+
const where = {
|
|
80
|
+
...(status && { status }),
|
|
81
|
+
...(authorId && { authorId }),
|
|
82
|
+
...(search && {
|
|
83
|
+
OR: [
|
|
84
|
+
{ title: { contains: search, mode: 'insensitive' } },
|
|
85
|
+
{ content: { contains: search, mode: 'insensitive' } },
|
|
86
|
+
],
|
|
87
|
+
}),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const [posts, total] = await Promise.all([
|
|
91
|
+
prisma.post.findMany({
|
|
92
|
+
where,
|
|
93
|
+
skip: (page - 1) * limit,
|
|
94
|
+
take: limit,
|
|
95
|
+
orderBy: { [sort]: order },
|
|
96
|
+
include: {
|
|
97
|
+
author: { select: { id: true, name: true, avatar: true } },
|
|
98
|
+
_count: { select: { comments: true, likes: true } },
|
|
99
|
+
},
|
|
100
|
+
}),
|
|
101
|
+
prisma.post.count({ where }),
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
return NextResponse.json({
|
|
105
|
+
data: posts,
|
|
106
|
+
pagination: {
|
|
107
|
+
page,
|
|
108
|
+
limit,
|
|
109
|
+
total,
|
|
110
|
+
totalPages: Math.ceil(total / limit),
|
|
111
|
+
hasNext: page * limit < total,
|
|
112
|
+
hasPrev: page > 1,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// POST with validation
|
|
118
|
+
const CreatePostSchema = z.object({
|
|
119
|
+
title: z.string().min(1).max(200),
|
|
120
|
+
content: z.string().min(1),
|
|
121
|
+
status: z.enum(['draft', 'published']).default('draft'),
|
|
122
|
+
tags: z.array(z.string()).optional(),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
export async function POST(request: NextRequest) {
|
|
126
|
+
const { userId } = await auth();
|
|
127
|
+
if (!userId) {
|
|
128
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let body;
|
|
132
|
+
try {
|
|
133
|
+
body = await request.json();
|
|
134
|
+
} catch {
|
|
135
|
+
return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const result = CreatePostSchema.safeParse(body);
|
|
139
|
+
if (!result.success) {
|
|
140
|
+
return NextResponse.json(
|
|
141
|
+
{ error: 'Validation failed', details: result.error.flatten() },
|
|
142
|
+
{ status: 400 }
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const post = await prisma.post.create({
|
|
147
|
+
data: {
|
|
148
|
+
...result.data,
|
|
149
|
+
authorId: userId,
|
|
150
|
+
},
|
|
151
|
+
include: {
|
|
152
|
+
author: { select: { id: true, name: true } },
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
return NextResponse.json(post, { status: 201 });
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### API Response Formats
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
// Consistent response envelope
|
|
164
|
+
interface ApiResponse<T> {
|
|
165
|
+
data: T;
|
|
166
|
+
meta?: {
|
|
167
|
+
pagination?: Pagination;
|
|
168
|
+
[key: string]: unknown;
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
interface ApiError {
|
|
173
|
+
error: string;
|
|
174
|
+
code?: string;
|
|
175
|
+
details?: Record<string, string[]>;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Helper function
|
|
179
|
+
function apiResponse<T>(data: T, meta?: Record<string, unknown>): NextResponse {
|
|
180
|
+
return NextResponse.json({ data, ...(meta && { meta }) });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function apiError(
|
|
184
|
+
message: string,
|
|
185
|
+
status: number,
|
|
186
|
+
details?: Record<string, unknown>
|
|
187
|
+
): NextResponse {
|
|
188
|
+
return NextResponse.json(
|
|
189
|
+
{ error: message, ...details },
|
|
190
|
+
{ status }
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Usage
|
|
195
|
+
return apiResponse(posts, { pagination });
|
|
196
|
+
return apiError('Validation failed', 400, { details: errors });
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### API Versioning
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
// app/api/v1/users/route.ts
|
|
203
|
+
// app/api/v2/users/route.ts
|
|
204
|
+
|
|
205
|
+
// Or via headers
|
|
206
|
+
export async function GET(request: NextRequest) {
|
|
207
|
+
const version = request.headers.get('api-version') ?? 'v1';
|
|
208
|
+
|
|
209
|
+
if (version === 'v2') {
|
|
210
|
+
// V2 response format
|
|
211
|
+
return NextResponse.json({ users: data, meta: {} });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// V1 response format (default)
|
|
215
|
+
return NextResponse.json(data);
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Rate Limiting
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
// lib/rate-limit.ts
|
|
223
|
+
import { Ratelimit } from '@upstash/ratelimit';
|
|
224
|
+
import { Redis } from '@upstash/redis';
|
|
225
|
+
|
|
226
|
+
const ratelimit = new Ratelimit({
|
|
227
|
+
redis: Redis.fromEnv(),
|
|
228
|
+
limiter: Ratelimit.slidingWindow(100, '1 m'),
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
export async function rateLimit(identifier: string) {
|
|
232
|
+
const { success, limit, remaining, reset } = await ratelimit.limit(identifier);
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
success,
|
|
236
|
+
headers: {
|
|
237
|
+
'X-RateLimit-Limit': limit.toString(),
|
|
238
|
+
'X-RateLimit-Remaining': remaining.toString(),
|
|
239
|
+
'X-RateLimit-Reset': reset.toString(),
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Usage in route
|
|
245
|
+
export async function POST(request: NextRequest) {
|
|
246
|
+
const ip = request.headers.get('x-forwarded-for') ?? 'anonymous';
|
|
247
|
+
const { success, headers } = await rateLimit(`api:${ip}`);
|
|
248
|
+
|
|
249
|
+
if (!success) {
|
|
250
|
+
return NextResponse.json(
|
|
251
|
+
{ error: 'Too many requests' },
|
|
252
|
+
{ status: 429, headers }
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Process request...
|
|
257
|
+
const response = NextResponse.json(data);
|
|
258
|
+
Object.entries(headers).forEach(([key, value]) => {
|
|
259
|
+
response.headers.set(key, value);
|
|
260
|
+
});
|
|
261
|
+
return response;
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### CORS Configuration
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
// middleware.ts
|
|
269
|
+
import { NextResponse } from 'next/server';
|
|
270
|
+
import type { NextRequest } from 'next/server';
|
|
271
|
+
|
|
272
|
+
const allowedOrigins = [
|
|
273
|
+
'https://yourapp.com',
|
|
274
|
+
'https://admin.yourapp.com',
|
|
275
|
+
];
|
|
276
|
+
|
|
277
|
+
export function middleware(request: NextRequest) {
|
|
278
|
+
const origin = request.headers.get('origin');
|
|
279
|
+
const isApiRoute = request.nextUrl.pathname.startsWith('/api');
|
|
280
|
+
|
|
281
|
+
if (isApiRoute) {
|
|
282
|
+
// Handle preflight
|
|
283
|
+
if (request.method === 'OPTIONS') {
|
|
284
|
+
return new NextResponse(null, {
|
|
285
|
+
status: 204,
|
|
286
|
+
headers: {
|
|
287
|
+
'Access-Control-Allow-Origin': allowedOrigins.includes(origin ?? '') ? origin! : '',
|
|
288
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, PATCH, DELETE, OPTIONS',
|
|
289
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
290
|
+
'Access-Control-Max-Age': '86400',
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Add CORS headers to response
|
|
296
|
+
const response = NextResponse.next();
|
|
297
|
+
if (origin && allowedOrigins.includes(origin)) {
|
|
298
|
+
response.headers.set('Access-Control-Allow-Origin', origin);
|
|
299
|
+
}
|
|
300
|
+
return response;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return NextResponse.next();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export const config = {
|
|
307
|
+
matcher: '/api/:path*',
|
|
308
|
+
};
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### OpenAPI Documentation
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
// Using next-swagger-doc
|
|
315
|
+
// lib/swagger.ts
|
|
316
|
+
import { createSwaggerSpec } from 'next-swagger-doc';
|
|
317
|
+
|
|
318
|
+
export const getApiDocs = () => {
|
|
319
|
+
const spec = createSwaggerSpec({
|
|
320
|
+
apiFolder: 'app/api',
|
|
321
|
+
definition: {
|
|
322
|
+
openapi: '3.0.0',
|
|
323
|
+
info: {
|
|
324
|
+
title: 'Your API',
|
|
325
|
+
version: '1.0.0',
|
|
326
|
+
},
|
|
327
|
+
components: {
|
|
328
|
+
securitySchemes: {
|
|
329
|
+
bearerAuth: {
|
|
330
|
+
type: 'http',
|
|
331
|
+
scheme: 'bearer',
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
security: [{ bearerAuth: [] }],
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
return spec;
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
// JSDoc comments in route handlers
|
|
342
|
+
/**
|
|
343
|
+
* @swagger
|
|
344
|
+
* /api/posts:
|
|
345
|
+
* get:
|
|
346
|
+
* summary: List posts
|
|
347
|
+
* tags: [Posts]
|
|
348
|
+
* parameters:
|
|
349
|
+
* - in: query
|
|
350
|
+
* name: page
|
|
351
|
+
* schema:
|
|
352
|
+
* type: integer
|
|
353
|
+
* responses:
|
|
354
|
+
* 200:
|
|
355
|
+
* description: List of posts
|
|
356
|
+
*/
|
|
357
|
+
export async function GET(request: NextRequest) {
|
|
358
|
+
// ...
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### File Upload API
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
// app/api/upload/route.ts
|
|
366
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
367
|
+
import { put } from '@vercel/blob';
|
|
368
|
+
|
|
369
|
+
export async function POST(request: NextRequest) {
|
|
370
|
+
const { userId } = await auth();
|
|
371
|
+
if (!userId) {
|
|
372
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const formData = await request.formData();
|
|
376
|
+
const file = formData.get('file') as File;
|
|
377
|
+
|
|
378
|
+
if (!file) {
|
|
379
|
+
return NextResponse.json({ error: 'No file provided' }, { status: 400 });
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Validate file
|
|
383
|
+
const maxSize = 5 * 1024 * 1024; // 5MB
|
|
384
|
+
if (file.size > maxSize) {
|
|
385
|
+
return NextResponse.json({ error: 'File too large' }, { status: 400 });
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
|
|
389
|
+
if (!allowedTypes.includes(file.type)) {
|
|
390
|
+
return NextResponse.json({ error: 'Invalid file type' }, { status: 400 });
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Upload to Vercel Blob
|
|
394
|
+
const blob = await put(file.name, file, {
|
|
395
|
+
access: 'public',
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
return NextResponse.json({ url: blob.url }, { status: 201 });
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
## API Checklist
|
|
403
|
+
|
|
404
|
+
- [ ] RESTful naming conventions
|
|
405
|
+
- [ ] Proper HTTP methods used
|
|
406
|
+
- [ ] Consistent response format
|
|
407
|
+
- [ ] Input validation with Zod
|
|
408
|
+
- [ ] Authentication required
|
|
409
|
+
- [ ] Rate limiting configured
|
|
410
|
+
- [ ] CORS headers set
|
|
411
|
+
- [ ] Error responses standardized
|
|
412
|
+
- [ ] Pagination implemented
|
|
413
|
+
- [ ] API documentation added
|
|
414
|
+
|
|
415
|
+
## Trigger Keywords
|
|
416
|
+
api, rest, endpoint, route, handler, get, post, patch, delete, request, response, json, cors, rate limit, pagination, filter, sort, openapi, swagger, webhook
|