@brainfish-ai/devdoc 0.1.48 → 0.1.49
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/dist/cli/commands/deploy.js +16 -11
- package/package.json +1 -1
- package/renderer/app/[...slug]/client.js +17 -0
- package/renderer/app/[...slug]/page.js +125 -0
- package/renderer/app/api/assets/[...path]/route.js +23 -4
- package/renderer/app/api/chat/route.js +188 -25
- package/renderer/app/api/collections/route.js +95 -2
- package/renderer/app/api/deploy/route.js +4 -0
- package/renderer/app/api/suggestions/route.js +98 -10
- package/renderer/app/globals.css +33 -0
- package/renderer/app/layout.js +83 -8
- package/renderer/components/docs/mdx/cards.js +16 -45
- package/renderer/components/docs/mdx/file-tree.js +102 -0
- package/renderer/components/docs/mdx/index.js +7 -0
- package/renderer/components/docs-viewer/agent/agent-chat.js +75 -11
- package/renderer/components/docs-viewer/agent/messages/assistant-message.js +67 -3
- package/renderer/components/docs-viewer/agent/messages/tool-call-display.js +49 -4
- package/renderer/components/docs-viewer/content/content-router.js +1 -1
- package/renderer/components/docs-viewer/content/doc-page.js +36 -28
- package/renderer/components/docs-viewer/index.js +223 -58
- package/renderer/components/docs-viewer/playground/graphql-playground.js +131 -33
- package/renderer/components/docs-viewer/shared/method-badge.js +11 -2
- package/renderer/components/docs-viewer/sidebar/collection-tree.js +44 -6
- package/renderer/components/docs-viewer/sidebar/index.js +2 -1
- package/renderer/components/docs-viewer/sidebar/right-sidebar.js +3 -1
- package/renderer/components/docs-viewer/sidebar/sidebar-item.js +5 -7
- package/renderer/hooks/use-route-state.js +44 -56
- package/renderer/lib/api-docs/agent/indexer.js +73 -12
- package/renderer/lib/api-docs/agent/use-suggestions.js +26 -16
- package/renderer/lib/api-docs/code-editor/mode-context.js +16 -18
- package/renderer/lib/api-docs/parsers/openapi/transformer.js +8 -1
- package/renderer/lib/cache/purge.js +98 -0
- package/renderer/lib/docs-link-utils.js +146 -0
- package/renderer/lib/docs-navigation-context.js +3 -2
- package/renderer/lib/docs-navigation.js +50 -41
- package/renderer/lib/rate-limit.js +203 -0
|
@@ -4,7 +4,25 @@ import { z } from 'zod';
|
|
|
4
4
|
// Use Node.js runtime for better compatibility
|
|
5
5
|
export const runtime = 'nodejs';
|
|
6
6
|
function buildSystemPrompt(endpoints, docs, changelog, currentEndpointId, apiSummary) {
|
|
7
|
-
|
|
7
|
+
// Separate GraphQL and REST endpoints for better organization
|
|
8
|
+
const graphqlEndpoints = endpoints.filter((ep)=>ep.type === 'graphql');
|
|
9
|
+
const restEndpoints = endpoints.filter((ep)=>ep.type === 'rest');
|
|
10
|
+
// Format REST endpoints
|
|
11
|
+
const restEndpointsList = restEndpoints.map((ep, i)=>{
|
|
12
|
+
const params = ep.parameters.length > 0 ? ` (params: ${ep.parameters.join(', ')})` : '';
|
|
13
|
+
return `${i + 1}. [${ep.method}] ${ep.name} - ${ep.path}${params}${ep.description ? ` - ${ep.description.slice(0, 100)}` : ''} (id: ${ep.id})`;
|
|
14
|
+
}).join('\n');
|
|
15
|
+
// Format GraphQL endpoints with operation type
|
|
16
|
+
const graphqlEndpointsList = graphqlEndpoints.map((ep, i)=>{
|
|
17
|
+
const opType = ep.operationType?.toUpperCase() || 'GRAPHQL';
|
|
18
|
+
const variables = ep.parameters.length > 0 ? ` (variables: ${ep.parameters.join(', ')})` : '';
|
|
19
|
+
return `${i + 1}. [${opType}] ${ep.name}${variables}${ep.description ? ` - ${ep.description.slice(0, 100)}` : ''} (id: ${ep.id})`;
|
|
20
|
+
}).join('\n');
|
|
21
|
+
// Combine endpoint lists
|
|
22
|
+
const endpointsList = [
|
|
23
|
+
restEndpointsList && `### REST Endpoints\n${restEndpointsList}`,
|
|
24
|
+
graphqlEndpointsList && `### GraphQL Operations\n${graphqlEndpointsList}`
|
|
25
|
+
].filter(Boolean).join('\n\n') || restEndpoints.concat(graphqlEndpoints).map((ep, i)=>{
|
|
8
26
|
const params = ep.parameters.length > 0 ? ` (params: ${ep.parameters.join(', ')})` : '';
|
|
9
27
|
return `${i + 1}. [${ep.method}] ${ep.name} - ${ep.path}${params}${ep.description ? ` - ${ep.description.slice(0, 100)}` : ''} (id: ${ep.id})`;
|
|
10
28
|
}).join('\n');
|
|
@@ -166,16 +184,24 @@ Before answering ANY question about this documentation, you MUST use search_docs
|
|
|
166
184
|
- Use when user asks about an endpoint, wants documentation, or asks "what is", "how to use"
|
|
167
185
|
- Call this AFTER navigate_to_endpoint to show the documentation view
|
|
168
186
|
|
|
169
|
-
**prefill_parameters**: Fill in values
|
|
187
|
+
**prefill_parameters**: Fill in values for REST endpoints
|
|
170
188
|
- Use when user gives specific values like "user_id is 123", "set the name to John"
|
|
171
189
|
- Can fill params (query), headers, body, or pathVariables (URL path params)
|
|
172
190
|
- **IMPORTANT**: For URL path parameters like {id}, {document_id}, <<id>>, use pathVariables NOT params
|
|
173
191
|
- Example: If URL is /documents/{id}, use pathVariables: [{key: "id", value: "123"}]
|
|
174
192
|
- **IMPORTANT**: Call switch_to_api_client BEFORE prefilling so user can see the API Client
|
|
193
|
+
- **DO NOT use for GraphQL** - use prefill_graphql_variables instead
|
|
194
|
+
|
|
195
|
+
**prefill_graphql_variables**: Fill in GraphQL variables
|
|
196
|
+
- Use when user provides specific values for GraphQL query variables
|
|
197
|
+
- Pass a JSON object with the variable values, e.g. { "code": "EU" }
|
|
198
|
+
- **IMPORTANT**: Navigate to the GraphQL operation FIRST, then prefill variables
|
|
199
|
+
- Works in Docs mode (GraphQL Playground)
|
|
175
200
|
|
|
176
201
|
**switch_to_api_client**: Switch to API Client tab
|
|
177
|
-
- Use when helping users test endpoints, send requests, or prefill data
|
|
202
|
+
- Use when helping users test REST endpoints, send requests, or prefill data
|
|
178
203
|
- Call this FIRST before prefill_parameters so user can see the changes
|
|
204
|
+
- **DO NOT use for GraphQL endpoints** - GraphQL uses the GraphQL Playground in Docs mode
|
|
179
205
|
|
|
180
206
|
**check_request_validity**: Validate if request is ready to send
|
|
181
207
|
- Use BEFORE sending to check what's missing
|
|
@@ -187,11 +213,17 @@ Before answering ANY question about this documentation, you MUST use search_docs
|
|
|
187
213
|
- ALWAYS check_request_validity first if you haven't already
|
|
188
214
|
- Returns full response with status, body, headers, timing
|
|
189
215
|
|
|
190
|
-
**get_current_request**: Get the user's current request configuration from the API Client
|
|
216
|
+
**get_current_request**: Get the user's current REST request configuration from the API Client
|
|
191
217
|
- Use when user says "use my request", "use current payload", "based on my config", "copy from playground"
|
|
192
218
|
- Returns: method, URL, query params, headers, body - exactly as configured in the playground
|
|
193
219
|
- ALWAYS use this when generating code examples if user has configured a request
|
|
194
220
|
|
|
221
|
+
**get_current_graphql_state**: Get the current GraphQL Playground state
|
|
222
|
+
- Returns: current query, variables, response (if executed), loading state, errors
|
|
223
|
+
- **ALWAYS call this** before answering questions about the GraphQL response or current state
|
|
224
|
+
- Use when user asks "what did the query return", "what's in the response", "show me the result"
|
|
225
|
+
- Use to check if a query has been executed and what data it returned
|
|
226
|
+
|
|
195
227
|
**list_notes**: List existing files in the Notes workspace
|
|
196
228
|
- **ALWAYS call this BEFORE creating a new file** to check for duplicates
|
|
197
229
|
- If a similar file exists (same topic, endpoint, or type), UPDATE it instead of creating new
|
|
@@ -231,7 +263,18 @@ Before answering ANY question about this documentation, you MUST use search_docs
|
|
|
231
263
|
- **NEVER assume navigation**: If user asks "fill the params" or "prefill", do it on the CURRENT endpoint - don't navigate elsewhere.
|
|
232
264
|
- **Use tools to verify**: When answering questions, USE search_docs or search_endpoints to get accurate information - don't make up answers.
|
|
233
265
|
|
|
234
|
-
##
|
|
266
|
+
## GraphQL Endpoints (IMPORTANT)
|
|
267
|
+
For GraphQL endpoints (type: "graphql", method: "QUERY", "MUTATION", "SUBSCRIPTION"):
|
|
268
|
+
- The GraphQL Playground is shown in **Docs mode** (NOT API Client mode)
|
|
269
|
+
- When user wants to test a GraphQL query, use **navigate** to go to the operation
|
|
270
|
+
- **DO NOT call switch_to_api_client** for GraphQL endpoints
|
|
271
|
+
- The GraphQL Playground will automatically show the query when you navigate to the endpoint
|
|
272
|
+
- **To prefill variables**: Use **prefill_graphql_variables** with the variable values
|
|
273
|
+
- Example: User says "get continent EU" → navigate to continent query, then prefill_graphql_variables({ "code": "EU" })
|
|
274
|
+
- **To check current state/response**: Use **get_current_graphql_state** to see what query is displayed, what variables are set, and what response was returned (if any)
|
|
275
|
+
- **ALWAYS call get_current_graphql_state** before answering questions about the GraphQL response or helping with query modifications
|
|
276
|
+
|
|
277
|
+
## API Client Assistance (For REST endpoints)
|
|
235
278
|
|
|
236
279
|
When helping users build and send requests, follow this flow:
|
|
237
280
|
|
|
@@ -308,6 +351,53 @@ User: "send it"
|
|
|
308
351
|
4. **Testable**: Include example test cases or curl commands to verify
|
|
309
352
|
5. **Secure**: Never hardcode secrets, use environment variables
|
|
310
353
|
|
|
354
|
+
### IMPORTANT: Keep Examples Focused (NOT Full Applications)
|
|
355
|
+
The sandbox is for **learning and quick integration** - NOT building complete applications.
|
|
356
|
+
|
|
357
|
+
**DO create:**
|
|
358
|
+
- Single-file examples demonstrating ONE API concept (e.g., authentication, creating a resource)
|
|
359
|
+
- Minimal working code that users can copy and adapt
|
|
360
|
+
- Helper functions/classes they can import into their own projects
|
|
361
|
+
- Quick scripts to test and understand API behavior
|
|
362
|
+
|
|
363
|
+
**DON'T create:**
|
|
364
|
+
- Full application structures (no app.py with Flask/FastAPI, no full React apps)
|
|
365
|
+
- Multi-service architectures or microservices
|
|
366
|
+
- Complete CRUD applications with all endpoints
|
|
367
|
+
- Database integrations, frontend UIs, or deployment configs
|
|
368
|
+
- Anything beyond what's needed to demonstrate the API
|
|
369
|
+
|
|
370
|
+
**Example - GOOD (focused):**
|
|
371
|
+
\`\`\`python
|
|
372
|
+
# create-post.py - Create a Facebook Page post
|
|
373
|
+
import requests
|
|
374
|
+
import os
|
|
375
|
+
|
|
376
|
+
def create_page_post(page_id: str, message: str, access_token: str) -> dict:
|
|
377
|
+
"""Create a post on a Facebook Page."""
|
|
378
|
+
response = requests.post(
|
|
379
|
+
f"https://graph.facebook.com/v21.0/{page_id}/feed",
|
|
380
|
+
data={"message": message, "access_token": access_token}
|
|
381
|
+
)
|
|
382
|
+
response.raise_for_status()
|
|
383
|
+
return response.json()
|
|
384
|
+
|
|
385
|
+
# Usage
|
|
386
|
+
if __name__ == "__main__":
|
|
387
|
+
result = create_page_post("your-page-id", "Hello World!", os.getenv("PAGE_TOKEN"))
|
|
388
|
+
print(f"Post ID: {result['id']}")
|
|
389
|
+
\`\`\`
|
|
390
|
+
|
|
391
|
+
**Example - BAD (too much):**
|
|
392
|
+
\`\`\`python
|
|
393
|
+
# DON'T: Full Flask app with routes, templates, database, etc.
|
|
394
|
+
from flask import Flask, render_template
|
|
395
|
+
from sqlalchemy import create_engine
|
|
396
|
+
# ... 200 lines of app code
|
|
397
|
+
\`\`\`
|
|
398
|
+
|
|
399
|
+
Keep it simple: **One concept, one file, under 100 lines.**
|
|
400
|
+
|
|
311
401
|
### Use Mermaid Diagrams (.mmd) for:
|
|
312
402
|
- Authentication/authorization flows → sequenceDiagram
|
|
313
403
|
- Multi-endpoint workflows (e.g., "create order → add items → checkout")
|
|
@@ -381,35 +471,98 @@ class APIClient:
|
|
|
381
471
|
**For workflows, create:**
|
|
382
472
|
1. A Mermaid diagram showing the flow (.mmd)
|
|
383
473
|
2. Implementation code files for each step
|
|
384
|
-
3.
|
|
474
|
+
3. A README.md tying them together
|
|
475
|
+
|
|
476
|
+
## IMPORTANT: Always Create README.md with Code Examples
|
|
477
|
+
|
|
478
|
+
**When creating ANY code file, ALWAYS create a README.md alongside it** to help users understand how to use the code.
|
|
479
|
+
|
|
480
|
+
**README.md template:**
|
|
481
|
+
\`\`\`markdown
|
|
482
|
+
# {Title - What This Does}
|
|
483
|
+
|
|
484
|
+
{One-line description of what this code does}
|
|
485
|
+
|
|
486
|
+
## Quick Start
|
|
487
|
+
|
|
488
|
+
\`\`\`bash
|
|
489
|
+
# Install dependencies
|
|
490
|
+
{pip install X / npm install X / etc.}
|
|
491
|
+
|
|
492
|
+
# Set your credentials
|
|
493
|
+
export API_KEY="your-api-key"
|
|
494
|
+
export OTHER_VAR="value"
|
|
495
|
+
|
|
496
|
+
# Run it
|
|
497
|
+
{python file.py / node file.js / etc.}
|
|
498
|
+
\`\`\`
|
|
499
|
+
|
|
500
|
+
## What's Included
|
|
501
|
+
|
|
502
|
+
- **{filename}**: {Brief description of what the file does}
|
|
503
|
+
|
|
504
|
+
## Configuration
|
|
505
|
+
|
|
506
|
+
| Environment Variable | Description | Required |
|
|
507
|
+
|---------------------|-------------|----------|
|
|
508
|
+
| \`API_KEY\` | Your API key | Yes |
|
|
509
|
+
| \`OTHER_VAR\` | Description | No |
|
|
510
|
+
|
|
511
|
+
## Usage Examples
|
|
512
|
+
|
|
513
|
+
{Show 2-3 common usage patterns with code snippets}
|
|
514
|
+
|
|
515
|
+
## Error Handling
|
|
516
|
+
|
|
517
|
+
{List common errors and how to resolve them}
|
|
518
|
+
\`\`\`
|
|
519
|
+
|
|
520
|
+
**README requirements:**
|
|
521
|
+
- **Always include Quick Start** with copy-paste ready commands
|
|
522
|
+
- **List ALL dependencies** with exact install commands
|
|
523
|
+
- **Show environment variables** in a table format
|
|
524
|
+
- **Provide usage examples** for the most common scenarios
|
|
525
|
+
- Keep it concise - users should be able to start in under 1 minute
|
|
385
526
|
|
|
386
527
|
## Steps (MUST follow this order):
|
|
387
528
|
1. If user mentions "my request" → use get_current_request FIRST
|
|
388
529
|
2. Use get_endpoint_details to get the full schema
|
|
389
530
|
3. **ALWAYS call list_notes** to check for existing similar files
|
|
390
|
-
4. **If similar file exists**: UPDATE
|
|
391
|
-
5. **If NO similar file**:
|
|
392
|
-
|
|
531
|
+
4. **If similar file exists**: UPDATE files in existing folder
|
|
532
|
+
5. **If NO similar file**:
|
|
533
|
+
- **ALWAYS create a folder first** using create_folder (e.g., \`create-post\`)
|
|
534
|
+
- Then create files inside the folder (e.g., \`create-post/create-post.py\`, \`create-post/README.md\`)
|
|
535
|
+
6. **ALWAYS create/update README.md** alongside code files (see README template above)
|
|
393
536
|
|
|
394
537
|
**IMPORTANT**: Use EXACT values from get_current_request in generated code.
|
|
395
538
|
|
|
396
|
-
**
|
|
397
|
-
|
|
398
|
-
|
|
539
|
+
**Folder structure (ALWAYS organize files in folders):**
|
|
540
|
+
\`\`\`
|
|
541
|
+
{action-name}/
|
|
542
|
+
├── {action-name}.{lang} # Main implementation file
|
|
543
|
+
├── README.md # Quick start guide (REQUIRED)
|
|
544
|
+
└── {flow-diagram}.mmd # Optional: visual flow diagram
|
|
545
|
+
\`\`\`
|
|
546
|
+
|
|
547
|
+
**Examples:**
|
|
548
|
+
- \`create-post/create-post.py\` + \`create-post/README.md\`
|
|
549
|
+
- \`auth-flow/auth-implementation.ts\` + \`auth-flow/auth-flow.mmd\` + \`auth-flow/README.md\`
|
|
550
|
+
- \`webhook-handler/webhook-handler.js\` + \`webhook-handler/README.md\`
|
|
399
551
|
|
|
400
552
|
## Concept Explanations → Implementation Guides
|
|
401
553
|
When users ask to "explain" something, create ACTIONABLE content:
|
|
402
|
-
1. Create a
|
|
403
|
-
2. Create
|
|
404
|
-
3.
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
- "
|
|
409
|
-
- "
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
554
|
+
1. Create a folder for the concept (e.g., \`auth-flow/\`)
|
|
555
|
+
2. Create a .mmd diagram showing the flow visually
|
|
556
|
+
3. Create implementation code showing how to do it
|
|
557
|
+
4. Create README.md with quick start instructions
|
|
558
|
+
|
|
559
|
+
**Always pair explanations with working code + README in a folder:**
|
|
560
|
+
- "Explain authentication" → \`auth-flow/auth-flow.mmd\` + \`auth-flow/auth-implementation.py\` + \`auth-flow/README.md\`
|
|
561
|
+
- "How do webhooks work" → \`webhook-handler/webhook-flow.mmd\` + \`webhook-handler/webhook-handler.ts\` + \`webhook-handler/README.md\`
|
|
562
|
+
- "Show the order process" → \`order-flow/order-flow.mmd\` + \`order-flow/create-order.py\` + \`order-flow/README.md\`
|
|
563
|
+
|
|
564
|
+
PREFER: Folder + Diagram + Working Code + README
|
|
565
|
+
AVOID: Long text explanations without code, loose files at root level
|
|
413
566
|
`;
|
|
414
567
|
}
|
|
415
568
|
// Convert raw UIMessages to model messages for the AI SDK
|
|
@@ -629,7 +782,7 @@ export async function POST(req) {
|
|
|
629
782
|
})
|
|
630
783
|
}),
|
|
631
784
|
prefill_parameters: tool({
|
|
632
|
-
description: 'Prefill request parameters, headers, body, or path variables for the current endpoint. Use this when the user provides values they want to use. For URL path parameters like {id} or <<id>>, use pathVariables.',
|
|
785
|
+
description: 'Prefill request parameters, headers, body, or path variables for the current REST endpoint. Use this when the user provides values they want to use. For URL path parameters like {id} or <<id>>, use pathVariables. DO NOT use for GraphQL - use prefill_graphql_variables instead.',
|
|
633
786
|
inputSchema: z.object({
|
|
634
787
|
params: z.array(z.object({
|
|
635
788
|
key: z.string(),
|
|
@@ -646,6 +799,12 @@ export async function POST(req) {
|
|
|
646
799
|
})).optional().describe('URL path variables to prefill (e.g., {id}, <<document_id>>). These replace placeholders in the URL path.')
|
|
647
800
|
})
|
|
648
801
|
}),
|
|
802
|
+
prefill_graphql_variables: tool({
|
|
803
|
+
description: 'Prefill GraphQL query variables in the GraphQL Playground. Use this for GraphQL endpoints when user provides specific values for query variables. Navigate to the GraphQL operation first, then call this to fill in the variables panel.',
|
|
804
|
+
inputSchema: z.object({
|
|
805
|
+
variables: z.record(z.string(), z.unknown()).describe('GraphQL variables as an object, e.g. { "code": "EU", "filter": { "name": { "eq": "Europe" } } }')
|
|
806
|
+
})
|
|
807
|
+
}),
|
|
649
808
|
get_endpoint_details: tool({
|
|
650
809
|
description: 'Get FULL details about a specific endpoint. Returns: parameters (name, description, required), headers, authentication requirements, request body schema (contentType, required fields, example payload), and response schemas with status codes. Use this when users ask about payload structure, required fields, body format, or how to call an endpoint.',
|
|
651
810
|
inputSchema: z.object({
|
|
@@ -656,13 +815,17 @@ export async function POST(req) {
|
|
|
656
815
|
description: 'Get the current request payload from the API Client playground. Returns the actual values the user has configured: HTTP method, URL, query parameters, headers, and request body. Use this when user asks to "use my request", "use current payload", "copy from playground", "based on my configuration", or wants code examples using their exact request setup.',
|
|
657
816
|
inputSchema: z.object({})
|
|
658
817
|
}),
|
|
818
|
+
get_current_graphql_state: tool({
|
|
819
|
+
description: 'Get the current state of the GraphQL Playground. Returns the current query, variables, and response (if any). Use this to understand what the user is currently working with, check if a query has been executed, and see what data was returned. ALWAYS call this before answering questions about the current GraphQL state or response.',
|
|
820
|
+
inputSchema: z.object({})
|
|
821
|
+
}),
|
|
659
822
|
// === MODE SWITCHING TOOLS ===
|
|
660
823
|
switch_to_docs: tool({
|
|
661
824
|
description: 'Switch to the Docs tab to show endpoint documentation. Use this when user asks about an endpoint, wants to understand how it works, asks "what is", "how to use", "what does this do", or needs to see documentation. This shows the detailed documentation view.',
|
|
662
825
|
inputSchema: z.object({})
|
|
663
826
|
}),
|
|
664
827
|
switch_to_api_client: tool({
|
|
665
|
-
description: 'Switch to the API Client tab to test/send requests. Use this when user wants to test
|
|
828
|
+
description: 'Switch to the API Client tab to test/send REST requests. Use this when user wants to test a REST endpoint, send a request, try it out, or you need to prefill parameters. Call this BEFORE prefilling data so user can see the API Client. DO NOT use for GraphQL endpoints - GraphQL uses the GraphQL Playground in Docs mode instead.',
|
|
666
829
|
inputSchema: z.object({})
|
|
667
830
|
}),
|
|
668
831
|
// === NOTES TOOLS ===
|
|
@@ -4,7 +4,9 @@ import { join, basename, isAbsolute } from 'path';
|
|
|
4
4
|
import matter from 'gray-matter';
|
|
5
5
|
import { CacheUtils } from '@/lib/cache';
|
|
6
6
|
import { importOpenAPISpec } from '@/lib/api-docs/parsers/openapi';
|
|
7
|
+
import { importGraphQLSchema } from '@/lib/api-docs/parsers/graphql';
|
|
7
8
|
import { generateAPISummary, formatAPISummaryForPrompt } from '@/lib/api-docs/agent/spec-summary';
|
|
9
|
+
import { buildEndpointIndex } from '@/lib/api-docs/agent/indexer';
|
|
8
10
|
import { getProjectContent } from '@/lib/storage/blob';
|
|
9
11
|
import { isDevMode, shouldShowItem } from '@/lib/docs/config/environment';
|
|
10
12
|
// Configuration
|
|
@@ -74,6 +76,79 @@ function loadGraphQLSchema(schemaPath) {
|
|
|
74
76
|
return null;
|
|
75
77
|
}
|
|
76
78
|
}
|
|
79
|
+
// Build combined endpoint index from REST collection and GraphQL schemas (local/dev mode)
|
|
80
|
+
async function buildCombinedEndpointIndex(collection, navigationTabs) {
|
|
81
|
+
const allEndpoints = [];
|
|
82
|
+
// Add REST endpoints from collection
|
|
83
|
+
if (collection && (collection.folders.length > 0 || collection.requests.length > 0)) {
|
|
84
|
+
const restEndpoints = buildEndpointIndex(collection);
|
|
85
|
+
allEndpoints.push(...restEndpoints);
|
|
86
|
+
}
|
|
87
|
+
// Add GraphQL endpoints from all GraphQL tabs
|
|
88
|
+
for (const tab of navigationTabs){
|
|
89
|
+
if (tab.type === 'graphql' && tab.graphqlSchemas) {
|
|
90
|
+
for (const schemaConfig of tab.graphqlSchemas){
|
|
91
|
+
try {
|
|
92
|
+
const schemaContent = loadGraphQLSchema(schemaConfig.schema);
|
|
93
|
+
if (!schemaContent) continue;
|
|
94
|
+
const graphqlCollection = await importGraphQLSchema(schemaContent, {
|
|
95
|
+
name: schemaConfig.name || 'GraphQL API',
|
|
96
|
+
endpoint: schemaConfig.endpoint || '/graphql'
|
|
97
|
+
});
|
|
98
|
+
const graphqlEndpoints = buildEndpointIndex(graphqlCollection);
|
|
99
|
+
// Add endpoints, avoiding duplicates by ID
|
|
100
|
+
for (const ep of graphqlEndpoints){
|
|
101
|
+
if (!allEndpoints.find((e)=>e.id === ep.id)) {
|
|
102
|
+
allEndpoints.push(ep);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// Skip schemas that fail to parse
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return allEndpoints;
|
|
112
|
+
}
|
|
113
|
+
// Build combined endpoint index from REST collection and GraphQL schemas (blob/multi-tenant mode)
|
|
114
|
+
async function buildCombinedEndpointIndexFromBlob(collection, navigationTabs, projectContent) {
|
|
115
|
+
const allEndpoints = [];
|
|
116
|
+
// Add REST endpoints from collection
|
|
117
|
+
if (collection && (collection.folders.length > 0 || collection.requests.length > 0)) {
|
|
118
|
+
const restEndpoints = buildEndpointIndex(collection);
|
|
119
|
+
allEndpoints.push(...restEndpoints);
|
|
120
|
+
}
|
|
121
|
+
// Add GraphQL endpoints from all GraphQL tabs
|
|
122
|
+
for (const tab of navigationTabs){
|
|
123
|
+
if (tab.type === 'graphql' && tab.graphqlSchemas) {
|
|
124
|
+
for (const schemaConfig of tab.graphqlSchemas){
|
|
125
|
+
try {
|
|
126
|
+
// Load GraphQL schema from blob storage (check files array)
|
|
127
|
+
const schemaFile = projectContent.files.find((f)=>f.path === schemaConfig.schema || f.path === schemaConfig.schema.replace(/^\//, '') // Remove leading slash
|
|
128
|
+
);
|
|
129
|
+
if (!schemaFile) {
|
|
130
|
+
console.log('[Collections] GraphQL schema not found in blob:', schemaConfig.schema);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const graphqlCollection = await importGraphQLSchema(schemaFile.content, {
|
|
134
|
+
name: schemaConfig.name || 'GraphQL API',
|
|
135
|
+
endpoint: schemaConfig.endpoint || '/graphql'
|
|
136
|
+
});
|
|
137
|
+
const graphqlEndpoints = buildEndpointIndex(graphqlCollection);
|
|
138
|
+
// Add endpoints, avoiding duplicates by ID
|
|
139
|
+
for (const ep of graphqlEndpoints){
|
|
140
|
+
if (!allEndpoints.find((e)=>e.id === ep.id)) {
|
|
141
|
+
allEndpoints.push(ep);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} catch (err) {
|
|
145
|
+
console.error('[Collections] Failed to parse GraphQL schema from blob:', err);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return allEndpoints;
|
|
151
|
+
}
|
|
77
152
|
// Load OpenAPI spec from local file
|
|
78
153
|
function loadLocalSpec(specPath) {
|
|
79
154
|
try {
|
|
@@ -438,6 +513,8 @@ function getOpenApiSpec(specPath) {
|
|
|
438
513
|
console.error('[Collections] Failed to parse OpenAPI spec:', e);
|
|
439
514
|
}
|
|
440
515
|
}
|
|
516
|
+
// Build combined endpoint index (REST + GraphQL) for multi-tenant
|
|
517
|
+
const endpointIndex = await buildCombinedEndpointIndexFromBlob(collection, navigationTabs, projectContent);
|
|
441
518
|
// Build the response
|
|
442
519
|
const response = {
|
|
443
520
|
id: projectSlug,
|
|
@@ -452,6 +529,7 @@ function getOpenApiSpec(specPath) {
|
|
|
452
529
|
headers: collection?.headers || [],
|
|
453
530
|
variables: collection?.variables || [],
|
|
454
531
|
apiSummary: null,
|
|
532
|
+
endpointIndex,
|
|
455
533
|
docGroups,
|
|
456
534
|
navigationTabs,
|
|
457
535
|
changelogReleases: [],
|
|
@@ -472,7 +550,12 @@ function getOpenApiSpec(specPath) {
|
|
|
472
550
|
return NextResponse.json(response, {
|
|
473
551
|
headers: {
|
|
474
552
|
'Content-Type': 'application/json',
|
|
475
|
-
|
|
553
|
+
// CDN caching: cache for 5 min at edge, 1 min in browser, revalidate in background for 1 hour
|
|
554
|
+
'Cache-Control': 'public, max-age=60, s-maxage=300, stale-while-revalidate=3600',
|
|
555
|
+
// Vercel-specific: cache at edge for 5 minutes
|
|
556
|
+
'CDN-Cache-Control': 'public, s-maxage=300, stale-while-revalidate=3600',
|
|
557
|
+
// Vercel Edge: cache tag for invalidation
|
|
558
|
+
'x-vercel-cache-tags': `project-${projectSlug}`
|
|
476
559
|
}
|
|
477
560
|
});
|
|
478
561
|
} catch (error) {
|
|
@@ -649,6 +732,8 @@ export async function GET(request) {
|
|
|
649
732
|
const spec = await getOpenApiSpec(specPath);
|
|
650
733
|
// Handle no spec available
|
|
651
734
|
if (!spec) {
|
|
735
|
+
// Still build endpoint index from GraphQL schemas (if any)
|
|
736
|
+
const endpointIndex = await buildCombinedEndpointIndex(null, navigationTabs);
|
|
652
737
|
return NextResponse.json({
|
|
653
738
|
id: 'empty',
|
|
654
739
|
name: docsConfig?.name || 'Documentation',
|
|
@@ -662,6 +747,7 @@ export async function GET(request) {
|
|
|
662
747
|
headers: [],
|
|
663
748
|
variables: [],
|
|
664
749
|
apiSummary: null,
|
|
750
|
+
endpointIndex,
|
|
665
751
|
docGroups,
|
|
666
752
|
navigationTabs,
|
|
667
753
|
changelogReleases,
|
|
@@ -685,6 +771,8 @@ export async function GET(request) {
|
|
|
685
771
|
}
|
|
686
772
|
// Check if spec has paths
|
|
687
773
|
if (!spec.paths || Object.keys(spec.paths).length === 0) {
|
|
774
|
+
// Still build endpoint index from GraphQL schemas (if any)
|
|
775
|
+
const endpointIndex = await buildCombinedEndpointIndex(null, navigationTabs);
|
|
688
776
|
return NextResponse.json({
|
|
689
777
|
id: 'empty',
|
|
690
778
|
name: spec.info?.title || docsConfig?.name || 'Documentation',
|
|
@@ -698,6 +786,7 @@ export async function GET(request) {
|
|
|
698
786
|
headers: [],
|
|
699
787
|
variables: [],
|
|
700
788
|
apiSummary: null,
|
|
789
|
+
endpointIndex,
|
|
701
790
|
docGroups,
|
|
702
791
|
navigationTabs,
|
|
703
792
|
changelogReleases,
|
|
@@ -733,10 +822,13 @@ export async function GET(request) {
|
|
|
733
822
|
// Generate or get cached API summary
|
|
734
823
|
const specVersion = spec.info?.version || 'unknown';
|
|
735
824
|
const apiSummary = await getOrGenerateSummary(collection, specVersion);
|
|
736
|
-
//
|
|
825
|
+
// Build combined endpoint index (REST + GraphQL)
|
|
826
|
+
const endpointIndex = await buildCombinedEndpointIndex(collection, navigationTabs);
|
|
827
|
+
// Return collection with summary, endpoint index, and doc groups
|
|
737
828
|
return NextResponse.json({
|
|
738
829
|
...collection,
|
|
739
830
|
apiSummary,
|
|
831
|
+
endpointIndex,
|
|
740
832
|
specVersion,
|
|
741
833
|
docGroups,
|
|
742
834
|
navigationTabs,
|
|
@@ -772,6 +864,7 @@ export async function GET(request) {
|
|
|
772
864
|
},
|
|
773
865
|
headers: [],
|
|
774
866
|
variables: [],
|
|
867
|
+
endpointIndex: [],
|
|
775
868
|
docGroups: []
|
|
776
869
|
}, {
|
|
777
870
|
headers: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server';
|
|
2
2
|
import { storeProjectContent, updateProjectContent, projectExists, generateProjectSlug, generateApiKey, storeProjectApiKey, validateApiKey } from '@/lib/storage/blob';
|
|
3
3
|
import { getProjectUrl, isValidSlug } from '@/lib/multi-tenant/context';
|
|
4
|
+
import { purgeProjectCache } from '@/lib/cache/purge';
|
|
4
5
|
/**
|
|
5
6
|
* Deploy API - receives content from CLI and stores in Vercel Blob
|
|
6
7
|
*
|
|
@@ -134,6 +135,9 @@ import { getProjectUrl, isValidSlug } from '@/lib/multi-tenant/context';
|
|
|
134
135
|
await storeProjectApiKey(slug, apiKey);
|
|
135
136
|
}
|
|
136
137
|
}
|
|
138
|
+
// Invalidate CDN cache for this project
|
|
139
|
+
// This ensures users see fresh content after deploy
|
|
140
|
+
await purgeProjectCache(slug);
|
|
137
141
|
// Build response
|
|
138
142
|
const projectUrl = getProjectUrl(slug);
|
|
139
143
|
// For new projects, include the API key in response
|
|
@@ -3,10 +3,22 @@ import { anthropic } from '@ai-sdk/anthropic';
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import crypto from 'crypto';
|
|
5
5
|
import { CacheUtils } from '@/lib/cache';
|
|
6
|
+
import { RateLimiter } from '@/lib/rate-limit';
|
|
6
7
|
export const runtime = 'nodejs';
|
|
7
8
|
// Cache TTL: 1 week in seconds
|
|
8
9
|
const CACHE_TTL_SECONDS = 60 * 60 * 24 * 7 // 7 days
|
|
9
10
|
;
|
|
11
|
+
// Rate limit configuration (can be overridden via env vars)
|
|
12
|
+
// Set SUGGESTIONS_RATE_LIMIT_ENABLED=false to disable rate limiting
|
|
13
|
+
const RATE_LIMIT_ENABLED = process.env.SUGGESTIONS_RATE_LIMIT_ENABLED !== 'false';
|
|
14
|
+
const RATE_LIMIT_MAX = parseInt(process.env.SUGGESTIONS_RATE_LIMIT_MAX || '30', 10);
|
|
15
|
+
const RATE_LIMIT_WINDOW = parseInt(process.env.SUGGESTIONS_RATE_LIMIT_WINDOW || '60', 10);
|
|
16
|
+
// Create rate limiter with configurable values
|
|
17
|
+
const suggestionsRateLimiter = new RateLimiter({
|
|
18
|
+
prefix: 'suggestions',
|
|
19
|
+
limit: RATE_LIMIT_MAX,
|
|
20
|
+
windowSeconds: RATE_LIMIT_WINDOW
|
|
21
|
+
});
|
|
10
22
|
const SuggestionsSchema = z.object({
|
|
11
23
|
suggestions: z.array(z.object({
|
|
12
24
|
title: z.string().describe('Short action phrase (2-4 words) like "Find endpoints" or "How do I"'),
|
|
@@ -22,10 +34,20 @@ const SuggestionsSchema = z.object({
|
|
|
22
34
|
if (currentEndpointId) {
|
|
23
35
|
// For specific endpoint: hash the endpoint details
|
|
24
36
|
const endpoint = endpointIndex.find((e)=>e.id === currentEndpointId);
|
|
25
|
-
|
|
37
|
+
if (endpoint) {
|
|
38
|
+
// Include GraphQL operation type in the cache key for better differentiation
|
|
39
|
+
const opType = endpoint.operationType || endpoint.method;
|
|
40
|
+
content = `endpoint:${endpoint.id}:${endpoint.name}:${opType}:${endpoint.path}`;
|
|
41
|
+
} else {
|
|
42
|
+
content = `endpoint:${currentEndpointId}`;
|
|
43
|
+
}
|
|
26
44
|
} else {
|
|
27
45
|
// For general API: hash based on endpoint count and first few endpoint signatures
|
|
28
|
-
const signature = endpointIndex.slice(0, 10).map((e)
|
|
46
|
+
const signature = endpointIndex.slice(0, 10).map((e)=>{
|
|
47
|
+
// Use operation type for GraphQL, method for REST
|
|
48
|
+
const opType = e.operationType || e.method;
|
|
49
|
+
return `${opType}:${e.name}`;
|
|
50
|
+
}).join('|');
|
|
29
51
|
content = `general:${endpointIndex.length}:${signature}`;
|
|
30
52
|
}
|
|
31
53
|
const hash = crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
@@ -34,6 +56,30 @@ const SuggestionsSchema = z.object({
|
|
|
34
56
|
function buildSuggestionPrompt(endpoints, currentEndpointId) {
|
|
35
57
|
const currentEndpoint = currentEndpointId ? endpoints.find((e)=>e.id === currentEndpointId) : null;
|
|
36
58
|
if (currentEndpoint) {
|
|
59
|
+
// Check if this is a GraphQL operation
|
|
60
|
+
const isGraphQL = currentEndpoint.type === 'graphql' || [
|
|
61
|
+
'query',
|
|
62
|
+
'mutation',
|
|
63
|
+
'subscription'
|
|
64
|
+
].includes(currentEndpoint.operationType || '');
|
|
65
|
+
if (isGraphQL) {
|
|
66
|
+
const opType = currentEndpoint.operationType || 'operation';
|
|
67
|
+
return `You are helping users explore a GraphQL API. Generate 4 helpful question suggestions for the "${currentEndpoint.name}" ${opType}.
|
|
68
|
+
|
|
69
|
+
GraphQL Operation details:
|
|
70
|
+
- Name: ${currentEndpoint.name}
|
|
71
|
+
- Type: ${opType}
|
|
72
|
+
- Description: ${currentEndpoint.description || 'No description'}
|
|
73
|
+
- Variables: ${currentEndpoint.parameters.join(', ') || 'None'}
|
|
74
|
+
|
|
75
|
+
Generate questions that help users:
|
|
76
|
+
1. Understand what this ${opType} does and when to use it
|
|
77
|
+
2. Know what variables are required and their types
|
|
78
|
+
3. See example GraphQL queries and responses
|
|
79
|
+
4. Understand how to handle errors or edge cases
|
|
80
|
+
|
|
81
|
+
Make questions specific to this GraphQL ${opType}, not generic.`;
|
|
82
|
+
}
|
|
37
83
|
return `You are helping users explore an API. Generate 4 helpful question suggestions for the "${currentEndpoint.name}" endpoint.
|
|
38
84
|
|
|
39
85
|
Endpoint details:
|
|
@@ -53,33 +99,75 @@ Generate questions that help users:
|
|
|
53
99
|
Make questions specific to this endpoint, not generic.`;
|
|
54
100
|
}
|
|
55
101
|
// General suggestions based on the API
|
|
56
|
-
|
|
102
|
+
// Separate GraphQL and REST endpoints
|
|
103
|
+
const graphqlEndpoints = endpoints.filter((e)=>e.type === 'graphql' || [
|
|
104
|
+
'query',
|
|
105
|
+
'mutation',
|
|
106
|
+
'subscription'
|
|
107
|
+
].includes(e.operationType || ''));
|
|
108
|
+
const restEndpoints = endpoints.filter((e)=>e.type === 'rest' && ![
|
|
109
|
+
'query',
|
|
110
|
+
'mutation',
|
|
111
|
+
'subscription'
|
|
112
|
+
].includes(e.operationType || ''));
|
|
113
|
+
const methodCounts = restEndpoints.reduce((acc, e)=>{
|
|
57
114
|
acc[e.method] = (acc[e.method] || 0) + 1;
|
|
58
115
|
return acc;
|
|
59
116
|
}, {});
|
|
117
|
+
const graphqlCounts = graphqlEndpoints.reduce((acc, e)=>{
|
|
118
|
+
const opType = e.operationType || 'operation';
|
|
119
|
+
acc[opType] = (acc[opType] || 0) + 1;
|
|
120
|
+
return acc;
|
|
121
|
+
}, {});
|
|
60
122
|
const categories = [
|
|
61
123
|
...new Set(endpoints.flatMap((e)=>e.tags))
|
|
62
124
|
].slice(0, 5);
|
|
63
|
-
|
|
125
|
+
// Build API type description
|
|
126
|
+
const apiTypes = [];
|
|
127
|
+
if (restEndpoints.length > 0) apiTypes.push('REST');
|
|
128
|
+
if (graphqlEndpoints.length > 0) apiTypes.push('GraphQL');
|
|
129
|
+
return `You are helping users explore ${apiTypes.join(' and ')} API documentation. Generate 4 helpful starter question suggestions.
|
|
64
130
|
|
|
65
131
|
API Overview:
|
|
66
|
-
- Total
|
|
67
|
-
|
|
132
|
+
- Total operations: ${endpoints.length}
|
|
133
|
+
${restEndpoints.length > 0 ? `- REST Methods: ${Object.entries(methodCounts).map(([m, c])=>`${m}: ${c}`).join(', ')}` : ''}
|
|
134
|
+
${graphqlEndpoints.length > 0 ? `- GraphQL Operations: ${Object.entries(graphqlCounts).map(([m, c])=>`${m}: ${c}`).join(', ')}` : ''}
|
|
68
135
|
- Categories: ${categories.join(', ') || 'General'}
|
|
69
136
|
|
|
70
|
-
Sample
|
|
71
|
-
${endpoints.slice(0, 8).map((e)
|
|
137
|
+
Sample operations:
|
|
138
|
+
${endpoints.slice(0, 8).map((e)=>{
|
|
139
|
+
if (e.type === 'graphql' || [
|
|
140
|
+
'query',
|
|
141
|
+
'mutation',
|
|
142
|
+
'subscription'
|
|
143
|
+
].includes(e.operationType || '')) {
|
|
144
|
+
return `- [${e.operationType?.toUpperCase() || 'GRAPHQL'}] ${e.name}: ${e.description || 'No description'}`;
|
|
145
|
+
}
|
|
146
|
+
return `- [${e.method}] ${e.name}: ${e.description || 'No description'}`;
|
|
147
|
+
}).join('\n')}
|
|
72
148
|
|
|
73
149
|
Generate questions that help users:
|
|
74
|
-
1. Find the right
|
|
150
|
+
1. Find the right operation for their use case
|
|
75
151
|
2. Understand authentication/authorization
|
|
76
|
-
3. Explore common operations (create, read, update, delete)
|
|
152
|
+
3. Explore common operations (${graphqlEndpoints.length > 0 ? 'queries, mutations' : 'create, read, update, delete'})
|
|
77
153
|
4. Get started quickly with the API
|
|
78
154
|
|
|
79
155
|
Make questions specific to this API's capabilities.`;
|
|
80
156
|
}
|
|
81
157
|
export async function POST(req) {
|
|
82
158
|
try {
|
|
159
|
+
// Rate limiting check (configurable via env vars)
|
|
160
|
+
if (RATE_LIMIT_ENABLED) {
|
|
161
|
+
const rateLimitResult = await suggestionsRateLimiter.check(req);
|
|
162
|
+
if (!rateLimitResult.success) {
|
|
163
|
+
console.log(`[Suggestions API] Rate limit exceeded for IP, count: ${rateLimitResult.count}/${rateLimitResult.limit}`);
|
|
164
|
+
return RateLimiter.tooManyRequestsResponse(rateLimitResult, 'Too many suggestion requests. Please wait before trying again.');
|
|
165
|
+
}
|
|
166
|
+
// Log rate limit status for monitoring (only on cache miss to reduce noise)
|
|
167
|
+
if (rateLimitResult.remaining < 5) {
|
|
168
|
+
console.log(`[Suggestions API] Rate limit warning: ${rateLimitResult.remaining} requests remaining`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
83
171
|
const body = await req.json();
|
|
84
172
|
const { endpointIndex, currentEndpointId } = body;
|
|
85
173
|
// Generate hash-based cache key
|