@open-mercato/ai-assistant 0.4.2-canary-802e036384 → 0.4.2-canary-8575e8d61b
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/modules/ai_assistant/lib/api-discovery-tools.js +1 -1
- package/dist/modules/ai_assistant/lib/api-discovery-tools.js.map +1 -1
- package/dist/modules/ai_assistant/lib/api-endpoint-index.js +1 -1
- package/dist/modules/ai_assistant/lib/api-endpoint-index.js.map +1 -1
- package/package.json +4 -4
- package/src/modules/ai_assistant/lib/api-discovery-tools.ts +1 -1
- package/src/modules/ai_assistant/lib/api-endpoint-index.ts +1 -1
|
@@ -111,7 +111,7 @@ PARAMETERS:
|
|
|
111
111
|
// ACL checked at API level
|
|
112
112
|
handler: async (input, ctx) => {
|
|
113
113
|
const { method, path, query, body } = input;
|
|
114
|
-
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || process.env.NEXT_PUBLIC_APP_URL || process.env.APP_URL || "http://localhost:
|
|
114
|
+
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || process.env.NEXT_PUBLIC_APP_URL || process.env.APP_URL || "http://localhost:3000";
|
|
115
115
|
const apiPath = path.startsWith("/api") ? path : `/api${path}`;
|
|
116
116
|
let url = `${baseUrl}${apiPath}`;
|
|
117
117
|
const queryParams = { ...query };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/ai_assistant/lib/api-discovery-tools.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * API Discovery Tools\n *\n * Meta-tools for discovering and executing API endpoints via search.\n * Replaces 400+ individual endpoint tools with 3 flexible tools.\n */\n\nimport { z } from 'zod'\nimport { registerMcpTool } from './tool-registry'\nimport type { McpToolContext } from './types'\nimport {\n getApiEndpoints,\n getEndpointByOperationId,\n searchEndpoints,\n type ApiEndpoint,\n} from './api-endpoint-index'\n\n/**\n * Load API discovery tools into the registry\n */\nexport async function loadApiDiscoveryTools(): Promise<number> {\n // Ensure endpoints are parsed and cached\n const endpoints = await getApiEndpoints()\n console.error(`[API Discovery] ${endpoints.length} endpoints available for discovery`)\n\n // Register the three discovery tools\n registerApiDiscoverTool()\n registerApiExecuteTool()\n registerApiSchemaTool()\n\n return 3\n}\n\n/**\n * api_discover - Find relevant API endpoints based on a query\n */\nfunction registerApiDiscoverTool(): void {\n registerMcpTool(\n {\n name: 'api_discover',\n description: `Find API endpoints in Open Mercato by keyword or action.\n\nCAPABILITIES: This tool searches 400+ endpoints that can CREATE, READ, UPDATE, and DELETE\ndata across all modules (customers, products, orders, shipments, invoices, etc.).\n\nSEARCH: Uses hybrid search (fulltext + vector) for best results. You can filter by HTTP method.\n\nEXAMPLES:\n- \"customer endpoints\" - Find all customer-related APIs\n- \"create order\" - Find endpoint to create new orders\n- \"delete product\" - Find endpoint to delete products (confirm with user before executing!)\n- \"update company name\" - Find endpoint to modify companies\n- \"search customers\" - Find search/list endpoints\n\nReturns: method, path, description, and operationId for each match.\nUse operationId with api_schema to get detailed parameter info before calling api_execute.`,\n inputSchema: z.object({\n query: z\n .string()\n .describe('Natural language query to find relevant endpoints (e.g., \"customer list\")'),\n method: z\n .enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])\n .optional()\n .describe('Filter by HTTP method'),\n limit: z.number().optional().default(10).describe('Max results to return (default: 10)'),\n }),\n requiredFeatures: [], // Available to all authenticated users\n handler: async (input: { query: string; method?: string; limit?: number }, ctx) => {\n const { query, method, limit = 10 } = input\n\n // Search for matching endpoints\n const searchService = ctx.container?.resolve<any>('searchService')\n const matches = await searchEndpoints(searchService, query, { limit, method })\n\n if (matches.length === 0) {\n return {\n success: true,\n message: 'No matching endpoints found. Try different search terms.',\n endpoints: [],\n }\n }\n\n // Format results for LLM consumption\n const results = matches.map((endpoint) => ({\n operationId: endpoint.operationId,\n method: endpoint.method,\n path: endpoint.path,\n description: endpoint.description || endpoint.summary,\n tags: endpoint.tags,\n parameters: endpoint.parameters.map((p) => ({\n name: p.name,\n in: p.in,\n required: p.required,\n type: p.type,\n })),\n hasRequestBody: endpoint.requestBodySchema !== null,\n }))\n\n return {\n success: true,\n message: `Found ${results.length} matching endpoint(s)`,\n endpoints: results,\n hint: 'Use api_schema to get detailed parameter info, or api_execute to call an endpoint',\n }\n },\n },\n { moduleId: 'api' }\n )\n}\n\n/**\n * api_execute - Execute an API endpoint\n */\nfunction registerApiExecuteTool(): void {\n registerMcpTool(\n {\n name: 'api_execute',\n description: `Execute an API call to CREATE, READ, UPDATE, or DELETE data in Open Mercato.\n\nWARNING: This tool can MODIFY and DELETE data. Be careful with mutations!\n\nMETHODS:\n- GET: Read/search data (safe, no confirmation needed)\n- POST: Create new records (confirm data with user first)\n- PUT/PATCH: Update existing records (confirm changes with user)\n- DELETE: Remove records permanently (ALWAYS confirm with user before executing!)\n\nWORKFLOW:\n1. First use api_discover to find the right endpoint\n2. Use api_schema to understand required parameters\n3. For POST/PUT/PATCH/DELETE: Confirm with user what will be changed\n4. Execute the call with proper parameters\n\nPARAMETERS:\n- method: HTTP method (GET, POST, PUT, PATCH, DELETE)\n- path: API path with parameters replaced (e.g., /customers/companies/123)\n- query: Query parameters as key-value object (for GET requests and filtering)\n- body: Request body for POST/PUT/PATCH (object with required fields)`,\n inputSchema: z.object({\n method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']).describe('HTTP method'),\n path: z\n .string()\n .describe('API path with parameters replaced (e.g., /customers/123, /orders)'),\n query: z\n .record(z.string(), z.string())\n .optional()\n .describe('Query parameters as key-value pairs'),\n body: z\n .record(z.string(), z.unknown())\n .optional()\n .describe('Request body for POST/PUT/PATCH requests'),\n }),\n requiredFeatures: [], // ACL checked at API level\n handler: async (\n input: {\n method: string\n path: string\n query?: Record<string, string>\n body?: Record<string, unknown>\n },\n ctx: McpToolContext\n ) => {\n const { method, path, query, body } = input\n\n // Build URL\n const baseUrl =\n process.env.NEXT_PUBLIC_API_BASE_URL ||\n process.env.NEXT_PUBLIC_APP_URL ||\n process.env.APP_URL ||\n 'http://localhost:5050'\n\n // Ensure path starts with /api\n const apiPath = path.startsWith('/api') ? path : `/api${path}`\n let url = `${baseUrl}${apiPath}`\n\n // Add query parameters\n const queryParams = { ...query }\n\n // Add context to query for GET, to body for mutations\n if (method === 'GET') {\n if (ctx.tenantId) queryParams.tenantId = ctx.tenantId\n if (ctx.organizationId) queryParams.organizationId = ctx.organizationId\n }\n\n if (Object.keys(queryParams).length > 0) {\n const separator = url.includes('?') ? '&' : '?'\n url += separator + new URLSearchParams(queryParams).toString()\n }\n\n // Build body with context\n let requestBody: Record<string, unknown> | undefined\n if (['POST', 'PUT', 'PATCH'].includes(method)) {\n requestBody = { ...body }\n if (ctx.tenantId) requestBody.tenantId = ctx.tenantId\n if (ctx.organizationId) requestBody.organizationId = ctx.organizationId\n }\n\n // Build headers\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n }\n if (ctx.apiKeySecret) headers['X-API-Key'] = ctx.apiKeySecret\n if (ctx.tenantId) headers['X-Tenant-Id'] = ctx.tenantId\n if (ctx.organizationId) headers['X-Organization-Id'] = ctx.organizationId\n\n // Execute request\n try {\n const response = await fetch(url, {\n method,\n headers,\n body: requestBody ? JSON.stringify(requestBody) : undefined,\n })\n\n const responseText = await response.text()\n\n if (!response.ok) {\n return {\n success: false,\n statusCode: response.status,\n error: `API error ${response.status}`,\n details: tryParseJson(responseText),\n }\n }\n\n return {\n success: true,\n statusCode: response.status,\n data: tryParseJson(responseText),\n }\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Request failed',\n }\n }\n },\n },\n { moduleId: 'api' }\n )\n}\n\n/**\n * api_schema - Get detailed schema for an endpoint\n */\nfunction registerApiSchemaTool(): void {\n registerMcpTool(\n {\n name: 'api_schema',\n description: `Get detailed schema for an API endpoint before executing it.\n\nIMPORTANT: Always check the schema before calling POST, PUT, PATCH, or DELETE endpoints\nto understand what parameters are required.\n\nUSAGE:\n- Use the operationId from api_discover results\n- Returns: path parameters, query parameters, and request body schema\n- Shows which fields are required vs optional\n- Includes field types and descriptions\n\nThis helps you construct the correct api_execute call with all required data.`,\n inputSchema: z.object({\n operationId: z.string().describe('Operation ID from api_discover results'),\n }),\n requiredFeatures: [],\n handler: async (input: { operationId: string }) => {\n const endpoint = await getEndpointByOperationId(input.operationId)\n\n if (!endpoint) {\n return {\n success: false,\n error: `Endpoint not found: ${input.operationId}`,\n hint: 'Use api_discover to find available endpoints',\n }\n }\n\n return {\n success: true,\n endpoint: {\n operationId: endpoint.operationId,\n method: endpoint.method,\n path: endpoint.path,\n description: endpoint.description,\n tags: endpoint.tags,\n deprecated: endpoint.deprecated,\n requiredFeatures: endpoint.requiredFeatures,\n parameters: endpoint.parameters,\n requestBodySchema: endpoint.requestBodySchema,\n },\n usage: buildUsageExample(endpoint),\n }\n },\n },\n { moduleId: 'api' }\n )\n}\n\n/**\n * Build usage example for an endpoint\n */\nfunction buildUsageExample(endpoint: ApiEndpoint): string {\n const pathParams = endpoint.parameters.filter((p) => p.in === 'path')\n const queryParams = endpoint.parameters.filter((p) => p.in === 'query')\n\n let example = `api_execute with:\\n method: \"${endpoint.method}\"\\n path: \"${endpoint.path}\"`\n\n if (pathParams.length > 0) {\n example += `\\n (replace ${pathParams.map((p) => `{${p.name}}`).join(', ')} in path)`\n }\n\n if (queryParams.length > 0) {\n const queryExample = queryParams\n .slice(0, 3)\n .map((p) => `\"${p.name}\": \"...\"`)\n .join(', ')\n example += `\\n query: { ${queryExample} }`\n }\n\n if (endpoint.requestBodySchema) {\n example += `\\n body: { ... } (see requestBodySchema above)`\n }\n\n return example\n}\n\n/**\n * Try to parse JSON, return original string if fails\n */\nfunction tryParseJson(text: string): unknown {\n try {\n return JSON.parse(text)\n } catch {\n return text\n }\n}\n"],
|
|
4
|
+
"sourcesContent": ["/**\n * API Discovery Tools\n *\n * Meta-tools for discovering and executing API endpoints via search.\n * Replaces 400+ individual endpoint tools with 3 flexible tools.\n */\n\nimport { z } from 'zod'\nimport { registerMcpTool } from './tool-registry'\nimport type { McpToolContext } from './types'\nimport {\n getApiEndpoints,\n getEndpointByOperationId,\n searchEndpoints,\n type ApiEndpoint,\n} from './api-endpoint-index'\n\n/**\n * Load API discovery tools into the registry\n */\nexport async function loadApiDiscoveryTools(): Promise<number> {\n // Ensure endpoints are parsed and cached\n const endpoints = await getApiEndpoints()\n console.error(`[API Discovery] ${endpoints.length} endpoints available for discovery`)\n\n // Register the three discovery tools\n registerApiDiscoverTool()\n registerApiExecuteTool()\n registerApiSchemaTool()\n\n return 3\n}\n\n/**\n * api_discover - Find relevant API endpoints based on a query\n */\nfunction registerApiDiscoverTool(): void {\n registerMcpTool(\n {\n name: 'api_discover',\n description: `Find API endpoints in Open Mercato by keyword or action.\n\nCAPABILITIES: This tool searches 400+ endpoints that can CREATE, READ, UPDATE, and DELETE\ndata across all modules (customers, products, orders, shipments, invoices, etc.).\n\nSEARCH: Uses hybrid search (fulltext + vector) for best results. You can filter by HTTP method.\n\nEXAMPLES:\n- \"customer endpoints\" - Find all customer-related APIs\n- \"create order\" - Find endpoint to create new orders\n- \"delete product\" - Find endpoint to delete products (confirm with user before executing!)\n- \"update company name\" - Find endpoint to modify companies\n- \"search customers\" - Find search/list endpoints\n\nReturns: method, path, description, and operationId for each match.\nUse operationId with api_schema to get detailed parameter info before calling api_execute.`,\n inputSchema: z.object({\n query: z\n .string()\n .describe('Natural language query to find relevant endpoints (e.g., \"customer list\")'),\n method: z\n .enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])\n .optional()\n .describe('Filter by HTTP method'),\n limit: z.number().optional().default(10).describe('Max results to return (default: 10)'),\n }),\n requiredFeatures: [], // Available to all authenticated users\n handler: async (input: { query: string; method?: string; limit?: number }, ctx) => {\n const { query, method, limit = 10 } = input\n\n // Search for matching endpoints\n const searchService = ctx.container?.resolve<any>('searchService')\n const matches = await searchEndpoints(searchService, query, { limit, method })\n\n if (matches.length === 0) {\n return {\n success: true,\n message: 'No matching endpoints found. Try different search terms.',\n endpoints: [],\n }\n }\n\n // Format results for LLM consumption\n const results = matches.map((endpoint) => ({\n operationId: endpoint.operationId,\n method: endpoint.method,\n path: endpoint.path,\n description: endpoint.description || endpoint.summary,\n tags: endpoint.tags,\n parameters: endpoint.parameters.map((p) => ({\n name: p.name,\n in: p.in,\n required: p.required,\n type: p.type,\n })),\n hasRequestBody: endpoint.requestBodySchema !== null,\n }))\n\n return {\n success: true,\n message: `Found ${results.length} matching endpoint(s)`,\n endpoints: results,\n hint: 'Use api_schema to get detailed parameter info, or api_execute to call an endpoint',\n }\n },\n },\n { moduleId: 'api' }\n )\n}\n\n/**\n * api_execute - Execute an API endpoint\n */\nfunction registerApiExecuteTool(): void {\n registerMcpTool(\n {\n name: 'api_execute',\n description: `Execute an API call to CREATE, READ, UPDATE, or DELETE data in Open Mercato.\n\nWARNING: This tool can MODIFY and DELETE data. Be careful with mutations!\n\nMETHODS:\n- GET: Read/search data (safe, no confirmation needed)\n- POST: Create new records (confirm data with user first)\n- PUT/PATCH: Update existing records (confirm changes with user)\n- DELETE: Remove records permanently (ALWAYS confirm with user before executing!)\n\nWORKFLOW:\n1. First use api_discover to find the right endpoint\n2. Use api_schema to understand required parameters\n3. For POST/PUT/PATCH/DELETE: Confirm with user what will be changed\n4. Execute the call with proper parameters\n\nPARAMETERS:\n- method: HTTP method (GET, POST, PUT, PATCH, DELETE)\n- path: API path with parameters replaced (e.g., /customers/companies/123)\n- query: Query parameters as key-value object (for GET requests and filtering)\n- body: Request body for POST/PUT/PATCH (object with required fields)`,\n inputSchema: z.object({\n method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']).describe('HTTP method'),\n path: z\n .string()\n .describe('API path with parameters replaced (e.g., /customers/123, /orders)'),\n query: z\n .record(z.string(), z.string())\n .optional()\n .describe('Query parameters as key-value pairs'),\n body: z\n .record(z.string(), z.unknown())\n .optional()\n .describe('Request body for POST/PUT/PATCH requests'),\n }),\n requiredFeatures: [], // ACL checked at API level\n handler: async (\n input: {\n method: string\n path: string\n query?: Record<string, string>\n body?: Record<string, unknown>\n },\n ctx: McpToolContext\n ) => {\n const { method, path, query, body } = input\n\n // Build URL\n const baseUrl =\n process.env.NEXT_PUBLIC_API_BASE_URL ||\n process.env.NEXT_PUBLIC_APP_URL ||\n process.env.APP_URL ||\n 'http://localhost:3000'\n\n // Ensure path starts with /api\n const apiPath = path.startsWith('/api') ? path : `/api${path}`\n let url = `${baseUrl}${apiPath}`\n\n // Add query parameters\n const queryParams = { ...query }\n\n // Add context to query for GET, to body for mutations\n if (method === 'GET') {\n if (ctx.tenantId) queryParams.tenantId = ctx.tenantId\n if (ctx.organizationId) queryParams.organizationId = ctx.organizationId\n }\n\n if (Object.keys(queryParams).length > 0) {\n const separator = url.includes('?') ? '&' : '?'\n url += separator + new URLSearchParams(queryParams).toString()\n }\n\n // Build body with context\n let requestBody: Record<string, unknown> | undefined\n if (['POST', 'PUT', 'PATCH'].includes(method)) {\n requestBody = { ...body }\n if (ctx.tenantId) requestBody.tenantId = ctx.tenantId\n if (ctx.organizationId) requestBody.organizationId = ctx.organizationId\n }\n\n // Build headers\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n }\n if (ctx.apiKeySecret) headers['X-API-Key'] = ctx.apiKeySecret\n if (ctx.tenantId) headers['X-Tenant-Id'] = ctx.tenantId\n if (ctx.organizationId) headers['X-Organization-Id'] = ctx.organizationId\n\n // Execute request\n try {\n const response = await fetch(url, {\n method,\n headers,\n body: requestBody ? JSON.stringify(requestBody) : undefined,\n })\n\n const responseText = await response.text()\n\n if (!response.ok) {\n return {\n success: false,\n statusCode: response.status,\n error: `API error ${response.status}`,\n details: tryParseJson(responseText),\n }\n }\n\n return {\n success: true,\n statusCode: response.status,\n data: tryParseJson(responseText),\n }\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Request failed',\n }\n }\n },\n },\n { moduleId: 'api' }\n )\n}\n\n/**\n * api_schema - Get detailed schema for an endpoint\n */\nfunction registerApiSchemaTool(): void {\n registerMcpTool(\n {\n name: 'api_schema',\n description: `Get detailed schema for an API endpoint before executing it.\n\nIMPORTANT: Always check the schema before calling POST, PUT, PATCH, or DELETE endpoints\nto understand what parameters are required.\n\nUSAGE:\n- Use the operationId from api_discover results\n- Returns: path parameters, query parameters, and request body schema\n- Shows which fields are required vs optional\n- Includes field types and descriptions\n\nThis helps you construct the correct api_execute call with all required data.`,\n inputSchema: z.object({\n operationId: z.string().describe('Operation ID from api_discover results'),\n }),\n requiredFeatures: [],\n handler: async (input: { operationId: string }) => {\n const endpoint = await getEndpointByOperationId(input.operationId)\n\n if (!endpoint) {\n return {\n success: false,\n error: `Endpoint not found: ${input.operationId}`,\n hint: 'Use api_discover to find available endpoints',\n }\n }\n\n return {\n success: true,\n endpoint: {\n operationId: endpoint.operationId,\n method: endpoint.method,\n path: endpoint.path,\n description: endpoint.description,\n tags: endpoint.tags,\n deprecated: endpoint.deprecated,\n requiredFeatures: endpoint.requiredFeatures,\n parameters: endpoint.parameters,\n requestBodySchema: endpoint.requestBodySchema,\n },\n usage: buildUsageExample(endpoint),\n }\n },\n },\n { moduleId: 'api' }\n )\n}\n\n/**\n * Build usage example for an endpoint\n */\nfunction buildUsageExample(endpoint: ApiEndpoint): string {\n const pathParams = endpoint.parameters.filter((p) => p.in === 'path')\n const queryParams = endpoint.parameters.filter((p) => p.in === 'query')\n\n let example = `api_execute with:\\n method: \"${endpoint.method}\"\\n path: \"${endpoint.path}\"`\n\n if (pathParams.length > 0) {\n example += `\\n (replace ${pathParams.map((p) => `{${p.name}}`).join(', ')} in path)`\n }\n\n if (queryParams.length > 0) {\n const queryExample = queryParams\n .slice(0, 3)\n .map((p) => `\"${p.name}\": \"...\"`)\n .join(', ')\n example += `\\n query: { ${queryExample} }`\n }\n\n if (endpoint.requestBodySchema) {\n example += `\\n body: { ... } (see requestBodySchema above)`\n }\n\n return example\n}\n\n/**\n * Try to parse JSON, return original string if fails\n */\nfunction tryParseJson(text: string): unknown {\n try {\n return JSON.parse(text)\n } catch {\n return text\n }\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -21,7 +21,7 @@ async function getEndpointByOperationId(operationId) {
|
|
|
21
21
|
return endpointsByOperationId?.get(operationId) ?? null;
|
|
22
22
|
}
|
|
23
23
|
async function parseApiEndpoints() {
|
|
24
|
-
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || process.env.NEXT_PUBLIC_APP_URL || process.env.APP_URL || "http://localhost:
|
|
24
|
+
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || process.env.NEXT_PUBLIC_APP_URL || process.env.APP_URL || "http://localhost:3000";
|
|
25
25
|
const openApiUrl = `${baseUrl}/api/docs/openapi`;
|
|
26
26
|
try {
|
|
27
27
|
console.error(`[API Index] Fetching OpenAPI spec from ${openApiUrl}...`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/ai_assistant/lib/api-endpoint-index.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * API Endpoint Index\n *\n * Parses OpenAPI spec and indexes endpoints for discovery via hybrid search.\n */\n\nimport type { OpenApiDocument } from '@open-mercato/shared/lib/openapi'\nimport type { SearchService } from '@open-mercato/search/service'\nimport type { IndexableRecord } from '@open-mercato/search/types'\nimport {\n API_ENDPOINT_ENTITY_ID,\n GLOBAL_TENANT_ID,\n API_ENDPOINT_SEARCH_CONFIG,\n endpointToIndexableRecord,\n computeEndpointsChecksum,\n} from './api-endpoint-index-config'\n\n/**\n * Indexed API endpoint structure\n */\nexport interface ApiEndpoint {\n id: string\n operationId: string\n method: string\n path: string\n summary: string\n description: string\n tags: string[]\n requiredFeatures: string[]\n parameters: ApiParameter[]\n requestBodySchema: Record<string, unknown> | null\n deprecated: boolean\n}\n\nexport interface ApiParameter {\n name: string\n in: 'path' | 'query' | 'header'\n required: boolean\n type: string\n description: string\n}\n\n/**\n * Entity type for API endpoints in search index\n * @deprecated Use API_ENDPOINT_ENTITY_ID from api-endpoint-index-config.ts\n */\nexport const API_ENDPOINT_ENTITY = API_ENDPOINT_ENTITY_ID\n\n/**\n * In-memory cache of parsed endpoints (avoid re-parsing on each request)\n */\nlet endpointsCache: ApiEndpoint[] | null = null\nlet endpointsByOperationId: Map<string, ApiEndpoint> | null = null\n\n/**\n * Get all parsed API endpoints (cached)\n */\nexport async function getApiEndpoints(): Promise<ApiEndpoint[]> {\n if (endpointsCache) {\n return endpointsCache\n }\n\n endpointsCache = await parseApiEndpoints()\n endpointsByOperationId = new Map(endpointsCache.map((e) => [e.operationId, e]))\n\n return endpointsCache\n}\n\n/**\n * Get endpoint by operationId\n */\nexport async function getEndpointByOperationId(operationId: string): Promise<ApiEndpoint | null> {\n await getApiEndpoints() // Ensure cache is populated\n return endpointsByOperationId?.get(operationId) ?? null\n}\n\n/**\n * Parse OpenAPI spec into indexable endpoints\n * Fetches the OpenAPI spec from the running app's /api/docs/openapi endpoint\n */\nasync function parseApiEndpoints(): Promise<ApiEndpoint[]> {\n const baseUrl =\n process.env.NEXT_PUBLIC_API_BASE_URL ||\n process.env.NEXT_PUBLIC_APP_URL ||\n process.env.APP_URL ||\n 'http://localhost:5050'\n\n const openApiUrl = `${baseUrl}/api/docs/openapi`\n\n try {\n console.error(`[API Index] Fetching OpenAPI spec from ${openApiUrl}...`)\n const response = await fetch(openApiUrl)\n\n if (!response.ok) {\n console.error(`[API Index] Failed to fetch OpenAPI spec: ${response.status} ${response.statusText}`)\n return []\n }\n\n const doc = (await response.json()) as OpenApiDocument\n console.error(`[API Index] Successfully fetched OpenAPI spec`)\n return extractEndpoints(doc)\n } catch (error) {\n console.error('[API Index] Could not fetch OpenAPI spec:', error instanceof Error ? error.message : error)\n console.error('[API Index] Make sure the app is running at', baseUrl)\n return []\n }\n}\n\n/**\n * Extract endpoints from OpenAPI document\n */\nfunction extractEndpoints(doc: OpenApiDocument): ApiEndpoint[] {\n const endpoints: ApiEndpoint[] = []\n const validMethods = ['get', 'post', 'put', 'patch', 'delete']\n\n if (!doc.paths) {\n return endpoints\n }\n\n for (const [path, pathItem] of Object.entries(doc.paths)) {\n if (!pathItem || typeof pathItem !== 'object') continue\n\n for (const [method, operation] of Object.entries(pathItem)) {\n if (!validMethods.includes(method.toLowerCase())) continue\n if (!operation || typeof operation !== 'object') continue\n\n const op = operation as any\n\n // Generate operationId if not present\n const operationId = op.operationId || generateOperationId(path, method)\n\n const endpoint: ApiEndpoint = {\n id: operationId,\n operationId,\n method: method.toUpperCase(),\n path,\n summary: op.summary || '',\n description: op.description || op.summary || `${method.toUpperCase()} ${path}`,\n tags: op.tags || [],\n requiredFeatures: op['x-require-features'] || [],\n deprecated: op.deprecated || false,\n parameters: extractParameters(op.parameters || []),\n requestBodySchema: extractRequestBodySchema(op.requestBody, doc.components?.schemas),\n }\n\n endpoints.push(endpoint)\n }\n }\n\n console.error(`[API Index] Parsed ${endpoints.length} endpoints from OpenAPI spec`)\n return endpoints\n}\n\n/**\n * Generate operationId from path and method\n */\nfunction generateOperationId(path: string, method: string): string {\n const pathParts = path\n .replace(/^\\//, '')\n .replace(/\\{([^}]+)\\}/g, 'by_$1')\n .split('/')\n .filter(Boolean)\n .join('_')\n\n return `${method.toLowerCase()}_${pathParts}`\n}\n\n/**\n * Extract parameter info\n */\nfunction extractParameters(params: any[]): ApiParameter[] {\n return params\n .filter((p) => p.in === 'path' || p.in === 'query')\n .map((p) => ({\n name: p.name,\n in: p.in,\n required: p.required ?? false,\n type: p.schema?.type || 'string',\n description: p.description || '',\n }))\n}\n\n/**\n * Extract request body schema (simplified)\n */\nfunction extractRequestBodySchema(\n requestBody: any,\n schemas?: Record<string, any>\n): Record<string, unknown> | null {\n if (!requestBody?.content?.['application/json']?.schema) {\n return null\n }\n\n const schema = requestBody.content['application/json'].schema\n\n // Resolve $ref if present\n if (schema.$ref && schemas) {\n const refPath = schema.$ref.replace('#/components/schemas/', '')\n return schemas[refPath] || schema\n }\n\n return schema\n}\n\n/**\n * Checksum from last indexing operation\n */\nlet lastIndexChecksum: string | null = null\n\n/**\n * Index endpoints for search discovery using hybrid search strategies.\n * Uses checksum-based change detection to avoid unnecessary re-indexing.\n *\n * @param searchService - The search service to use for indexing\n * @param force - Force re-indexing even if checksum hasn't changed\n * @returns Number of endpoints indexed\n */\nexport async function indexApiEndpoints(\n searchService: SearchService,\n force = false\n): Promise<number> {\n const endpoints = await getApiEndpoints()\n\n if (endpoints.length === 0) {\n console.error('[API Index] No endpoints to index')\n return 0\n }\n\n // Compute checksum to detect changes\n const checksum = computeEndpointsChecksum(\n endpoints.map((e) => ({ operationId: e.operationId, method: e.method, path: e.path }))\n )\n\n // Skip if checksum matches and not forced\n if (!force && lastIndexChecksum === checksum) {\n console.error(`[API Index] Skipping indexing - ${endpoints.length} endpoints unchanged`)\n return 0\n }\n\n // Convert to indexable records using the proper format\n const records: IndexableRecord[] = endpoints.map((endpoint) =>\n endpointToIndexableRecord(endpoint)\n )\n\n try {\n console.error(`[API Index] Starting bulk index of ${records.length} endpoints...`)\n // Bulk index using all available strategies (fulltext + vector)\n // Use Promise.race with timeout to prevent hanging\n const timeoutMs = 60000 // 60 second timeout\n const indexPromise = searchService.bulkIndex(records)\n const timeoutPromise = new Promise<never>((_, reject) =>\n setTimeout(() => reject(new Error(`Bulk index timed out after ${timeoutMs}ms`)), timeoutMs)\n )\n\n await Promise.race([indexPromise, timeoutPromise])\n lastIndexChecksum = checksum\n console.error(`[API Index] Indexed ${records.length} API endpoints for hybrid search`)\n return records.length\n } catch (error) {\n console.error('[API Index] Failed to index endpoints:', error)\n // Still return the count - some strategies may have succeeded\n lastIndexChecksum = checksum\n return records.length\n }\n}\n\n/**\n * Build searchable content from endpoint\n */\nfunction buildSearchableContent(endpoint: ApiEndpoint): string {\n const parts = [\n endpoint.operationId,\n endpoint.method,\n endpoint.path,\n endpoint.summary,\n endpoint.description,\n ...endpoint.tags,\n ...endpoint.parameters.map((p) => `${p.name} ${p.description}`),\n ]\n\n return parts.filter(Boolean).join(' ')\n}\n\n/**\n * Search endpoints using hybrid search (fulltext + vector).\n * Falls back to in-memory search if search service is not available.\n */\nexport async function searchEndpoints(\n searchService: SearchService | null,\n query: string,\n options: { limit?: number; method?: string } = {}\n): Promise<ApiEndpoint[]> {\n const { limit = API_ENDPOINT_SEARCH_CONFIG.defaultLimit, method } = options\n\n // Ensure endpoints are loaded\n await getApiEndpoints()\n\n // Try hybrid search first if search service is available\n if (searchService) {\n try {\n // Use hybrid search (fulltext + vector)\n const results = await searchService.search(query, {\n tenantId: GLOBAL_TENANT_ID,\n organizationId: null,\n entityTypes: [API_ENDPOINT_ENTITY_ID],\n limit: limit * 2, // Get extra to account for filtering\n })\n\n // Map search results back to ApiEndpoint objects\n const endpoints: ApiEndpoint[] = []\n for (const result of results) {\n if (endpoints.length >= limit) break\n\n const endpoint = endpointsByOperationId?.get(result.recordId)\n if (endpoint) {\n // Apply method filter if not handled by search\n if (method && endpoint.method !== method.toUpperCase()) continue\n endpoints.push(endpoint)\n }\n }\n\n if (endpoints.length > 0) {\n return endpoints\n }\n\n // Fall through to fallback if no results from hybrid search\n console.error('[API Index] No hybrid search results, falling back to in-memory search')\n } catch (error) {\n console.error('[API Index] Hybrid search failed, falling back to in-memory:', error)\n }\n }\n\n // Fallback: Simple in-memory text matching\n return searchEndpointsFallback(query, { limit, method })\n}\n\n/**\n * Fallback in-memory search when hybrid search is not available.\n */\nfunction searchEndpointsFallback(\n query: string,\n options: { limit?: number; method?: string } = {}\n): ApiEndpoint[] {\n const { limit = API_ENDPOINT_SEARCH_CONFIG.defaultLimit, method } = options\n\n if (!endpointsCache) {\n return []\n }\n\n const queryLower = query.toLowerCase()\n const queryTerms = queryLower.split(/\\s+/).filter(Boolean)\n\n let matches = endpointsCache.filter((endpoint) => {\n const content = buildSearchableContent(endpoint).toLowerCase()\n return queryTerms.some((term) => content.includes(term))\n })\n\n // Filter by method if specified\n if (method) {\n matches = matches.filter((e) => e.method === method.toUpperCase())\n }\n\n // Sort by relevance (number of matching terms)\n matches.sort((a, b) => {\n const aContent = buildSearchableContent(a).toLowerCase()\n const bContent = buildSearchableContent(b).toLowerCase()\n const aScore = queryTerms.filter((t) => aContent.includes(t)).length\n const bScore = queryTerms.filter((t) => bContent.includes(t)).length\n return bScore - aScore\n })\n\n return matches.slice(0, limit)\n}\n\n/**\n * Clear endpoint cache (for testing)\n */\nexport function clearEndpointCache(): void {\n endpointsCache = null\n endpointsByOperationId = null\n}\n"],
|
|
4
|
+
"sourcesContent": ["/**\n * API Endpoint Index\n *\n * Parses OpenAPI spec and indexes endpoints for discovery via hybrid search.\n */\n\nimport type { OpenApiDocument } from '@open-mercato/shared/lib/openapi'\nimport type { SearchService } from '@open-mercato/search/service'\nimport type { IndexableRecord } from '@open-mercato/search/types'\nimport {\n API_ENDPOINT_ENTITY_ID,\n GLOBAL_TENANT_ID,\n API_ENDPOINT_SEARCH_CONFIG,\n endpointToIndexableRecord,\n computeEndpointsChecksum,\n} from './api-endpoint-index-config'\n\n/**\n * Indexed API endpoint structure\n */\nexport interface ApiEndpoint {\n id: string\n operationId: string\n method: string\n path: string\n summary: string\n description: string\n tags: string[]\n requiredFeatures: string[]\n parameters: ApiParameter[]\n requestBodySchema: Record<string, unknown> | null\n deprecated: boolean\n}\n\nexport interface ApiParameter {\n name: string\n in: 'path' | 'query' | 'header'\n required: boolean\n type: string\n description: string\n}\n\n/**\n * Entity type for API endpoints in search index\n * @deprecated Use API_ENDPOINT_ENTITY_ID from api-endpoint-index-config.ts\n */\nexport const API_ENDPOINT_ENTITY = API_ENDPOINT_ENTITY_ID\n\n/**\n * In-memory cache of parsed endpoints (avoid re-parsing on each request)\n */\nlet endpointsCache: ApiEndpoint[] | null = null\nlet endpointsByOperationId: Map<string, ApiEndpoint> | null = null\n\n/**\n * Get all parsed API endpoints (cached)\n */\nexport async function getApiEndpoints(): Promise<ApiEndpoint[]> {\n if (endpointsCache) {\n return endpointsCache\n }\n\n endpointsCache = await parseApiEndpoints()\n endpointsByOperationId = new Map(endpointsCache.map((e) => [e.operationId, e]))\n\n return endpointsCache\n}\n\n/**\n * Get endpoint by operationId\n */\nexport async function getEndpointByOperationId(operationId: string): Promise<ApiEndpoint | null> {\n await getApiEndpoints() // Ensure cache is populated\n return endpointsByOperationId?.get(operationId) ?? null\n}\n\n/**\n * Parse OpenAPI spec into indexable endpoints\n * Fetches the OpenAPI spec from the running app's /api/docs/openapi endpoint\n */\nasync function parseApiEndpoints(): Promise<ApiEndpoint[]> {\n const baseUrl =\n process.env.NEXT_PUBLIC_API_BASE_URL ||\n process.env.NEXT_PUBLIC_APP_URL ||\n process.env.APP_URL ||\n 'http://localhost:3000'\n\n const openApiUrl = `${baseUrl}/api/docs/openapi`\n\n try {\n console.error(`[API Index] Fetching OpenAPI spec from ${openApiUrl}...`)\n const response = await fetch(openApiUrl)\n\n if (!response.ok) {\n console.error(`[API Index] Failed to fetch OpenAPI spec: ${response.status} ${response.statusText}`)\n return []\n }\n\n const doc = (await response.json()) as OpenApiDocument\n console.error(`[API Index] Successfully fetched OpenAPI spec`)\n return extractEndpoints(doc)\n } catch (error) {\n console.error('[API Index] Could not fetch OpenAPI spec:', error instanceof Error ? error.message : error)\n console.error('[API Index] Make sure the app is running at', baseUrl)\n return []\n }\n}\n\n/**\n * Extract endpoints from OpenAPI document\n */\nfunction extractEndpoints(doc: OpenApiDocument): ApiEndpoint[] {\n const endpoints: ApiEndpoint[] = []\n const validMethods = ['get', 'post', 'put', 'patch', 'delete']\n\n if (!doc.paths) {\n return endpoints\n }\n\n for (const [path, pathItem] of Object.entries(doc.paths)) {\n if (!pathItem || typeof pathItem !== 'object') continue\n\n for (const [method, operation] of Object.entries(pathItem)) {\n if (!validMethods.includes(method.toLowerCase())) continue\n if (!operation || typeof operation !== 'object') continue\n\n const op = operation as any\n\n // Generate operationId if not present\n const operationId = op.operationId || generateOperationId(path, method)\n\n const endpoint: ApiEndpoint = {\n id: operationId,\n operationId,\n method: method.toUpperCase(),\n path,\n summary: op.summary || '',\n description: op.description || op.summary || `${method.toUpperCase()} ${path}`,\n tags: op.tags || [],\n requiredFeatures: op['x-require-features'] || [],\n deprecated: op.deprecated || false,\n parameters: extractParameters(op.parameters || []),\n requestBodySchema: extractRequestBodySchema(op.requestBody, doc.components?.schemas),\n }\n\n endpoints.push(endpoint)\n }\n }\n\n console.error(`[API Index] Parsed ${endpoints.length} endpoints from OpenAPI spec`)\n return endpoints\n}\n\n/**\n * Generate operationId from path and method\n */\nfunction generateOperationId(path: string, method: string): string {\n const pathParts = path\n .replace(/^\\//, '')\n .replace(/\\{([^}]+)\\}/g, 'by_$1')\n .split('/')\n .filter(Boolean)\n .join('_')\n\n return `${method.toLowerCase()}_${pathParts}`\n}\n\n/**\n * Extract parameter info\n */\nfunction extractParameters(params: any[]): ApiParameter[] {\n return params\n .filter((p) => p.in === 'path' || p.in === 'query')\n .map((p) => ({\n name: p.name,\n in: p.in,\n required: p.required ?? false,\n type: p.schema?.type || 'string',\n description: p.description || '',\n }))\n}\n\n/**\n * Extract request body schema (simplified)\n */\nfunction extractRequestBodySchema(\n requestBody: any,\n schemas?: Record<string, any>\n): Record<string, unknown> | null {\n if (!requestBody?.content?.['application/json']?.schema) {\n return null\n }\n\n const schema = requestBody.content['application/json'].schema\n\n // Resolve $ref if present\n if (schema.$ref && schemas) {\n const refPath = schema.$ref.replace('#/components/schemas/', '')\n return schemas[refPath] || schema\n }\n\n return schema\n}\n\n/**\n * Checksum from last indexing operation\n */\nlet lastIndexChecksum: string | null = null\n\n/**\n * Index endpoints for search discovery using hybrid search strategies.\n * Uses checksum-based change detection to avoid unnecessary re-indexing.\n *\n * @param searchService - The search service to use for indexing\n * @param force - Force re-indexing even if checksum hasn't changed\n * @returns Number of endpoints indexed\n */\nexport async function indexApiEndpoints(\n searchService: SearchService,\n force = false\n): Promise<number> {\n const endpoints = await getApiEndpoints()\n\n if (endpoints.length === 0) {\n console.error('[API Index] No endpoints to index')\n return 0\n }\n\n // Compute checksum to detect changes\n const checksum = computeEndpointsChecksum(\n endpoints.map((e) => ({ operationId: e.operationId, method: e.method, path: e.path }))\n )\n\n // Skip if checksum matches and not forced\n if (!force && lastIndexChecksum === checksum) {\n console.error(`[API Index] Skipping indexing - ${endpoints.length} endpoints unchanged`)\n return 0\n }\n\n // Convert to indexable records using the proper format\n const records: IndexableRecord[] = endpoints.map((endpoint) =>\n endpointToIndexableRecord(endpoint)\n )\n\n try {\n console.error(`[API Index] Starting bulk index of ${records.length} endpoints...`)\n // Bulk index using all available strategies (fulltext + vector)\n // Use Promise.race with timeout to prevent hanging\n const timeoutMs = 60000 // 60 second timeout\n const indexPromise = searchService.bulkIndex(records)\n const timeoutPromise = new Promise<never>((_, reject) =>\n setTimeout(() => reject(new Error(`Bulk index timed out after ${timeoutMs}ms`)), timeoutMs)\n )\n\n await Promise.race([indexPromise, timeoutPromise])\n lastIndexChecksum = checksum\n console.error(`[API Index] Indexed ${records.length} API endpoints for hybrid search`)\n return records.length\n } catch (error) {\n console.error('[API Index] Failed to index endpoints:', error)\n // Still return the count - some strategies may have succeeded\n lastIndexChecksum = checksum\n return records.length\n }\n}\n\n/**\n * Build searchable content from endpoint\n */\nfunction buildSearchableContent(endpoint: ApiEndpoint): string {\n const parts = [\n endpoint.operationId,\n endpoint.method,\n endpoint.path,\n endpoint.summary,\n endpoint.description,\n ...endpoint.tags,\n ...endpoint.parameters.map((p) => `${p.name} ${p.description}`),\n ]\n\n return parts.filter(Boolean).join(' ')\n}\n\n/**\n * Search endpoints using hybrid search (fulltext + vector).\n * Falls back to in-memory search if search service is not available.\n */\nexport async function searchEndpoints(\n searchService: SearchService | null,\n query: string,\n options: { limit?: number; method?: string } = {}\n): Promise<ApiEndpoint[]> {\n const { limit = API_ENDPOINT_SEARCH_CONFIG.defaultLimit, method } = options\n\n // Ensure endpoints are loaded\n await getApiEndpoints()\n\n // Try hybrid search first if search service is available\n if (searchService) {\n try {\n // Use hybrid search (fulltext + vector)\n const results = await searchService.search(query, {\n tenantId: GLOBAL_TENANT_ID,\n organizationId: null,\n entityTypes: [API_ENDPOINT_ENTITY_ID],\n limit: limit * 2, // Get extra to account for filtering\n })\n\n // Map search results back to ApiEndpoint objects\n const endpoints: ApiEndpoint[] = []\n for (const result of results) {\n if (endpoints.length >= limit) break\n\n const endpoint = endpointsByOperationId?.get(result.recordId)\n if (endpoint) {\n // Apply method filter if not handled by search\n if (method && endpoint.method !== method.toUpperCase()) continue\n endpoints.push(endpoint)\n }\n }\n\n if (endpoints.length > 0) {\n return endpoints\n }\n\n // Fall through to fallback if no results from hybrid search\n console.error('[API Index] No hybrid search results, falling back to in-memory search')\n } catch (error) {\n console.error('[API Index] Hybrid search failed, falling back to in-memory:', error)\n }\n }\n\n // Fallback: Simple in-memory text matching\n return searchEndpointsFallback(query, { limit, method })\n}\n\n/**\n * Fallback in-memory search when hybrid search is not available.\n */\nfunction searchEndpointsFallback(\n query: string,\n options: { limit?: number; method?: string } = {}\n): ApiEndpoint[] {\n const { limit = API_ENDPOINT_SEARCH_CONFIG.defaultLimit, method } = options\n\n if (!endpointsCache) {\n return []\n }\n\n const queryLower = query.toLowerCase()\n const queryTerms = queryLower.split(/\\s+/).filter(Boolean)\n\n let matches = endpointsCache.filter((endpoint) => {\n const content = buildSearchableContent(endpoint).toLowerCase()\n return queryTerms.some((term) => content.includes(term))\n })\n\n // Filter by method if specified\n if (method) {\n matches = matches.filter((e) => e.method === method.toUpperCase())\n }\n\n // Sort by relevance (number of matching terms)\n matches.sort((a, b) => {\n const aContent = buildSearchableContent(a).toLowerCase()\n const bContent = buildSearchableContent(b).toLowerCase()\n const aScore = queryTerms.filter((t) => aContent.includes(t)).length\n const bScore = queryTerms.filter((t) => bContent.includes(t)).length\n return bScore - aScore\n })\n\n return matches.slice(0, limit)\n}\n\n/**\n * Clear endpoint cache (for testing)\n */\nexport function clearEndpointCache(): void {\n endpointsCache = null\n endpointsByOperationId = null\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ai-assistant",
|
|
3
|
-
"version": "0.4.2-canary-
|
|
3
|
+
"version": "0.4.2-canary-8575e8d61b",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -93,12 +93,12 @@
|
|
|
93
93
|
"zod-to-json-schema": "^3.25.1"
|
|
94
94
|
},
|
|
95
95
|
"peerDependencies": {
|
|
96
|
-
"@open-mercato/shared": "0.4.2-canary-
|
|
97
|
-
"@open-mercato/ui": "0.4.2-canary-
|
|
96
|
+
"@open-mercato/shared": "0.4.2-canary-8575e8d61b",
|
|
97
|
+
"@open-mercato/ui": "0.4.2-canary-8575e8d61b",
|
|
98
98
|
"zod": ">=3.23.0"
|
|
99
99
|
},
|
|
100
100
|
"devDependencies": {
|
|
101
|
-
"@open-mercato/cli": "0.4.2-canary-
|
|
101
|
+
"@open-mercato/cli": "0.4.2-canary-8575e8d61b",
|
|
102
102
|
"tsx": "^4.21.0"
|
|
103
103
|
},
|
|
104
104
|
"publishConfig": {
|
|
@@ -167,7 +167,7 @@ PARAMETERS:
|
|
|
167
167
|
process.env.NEXT_PUBLIC_API_BASE_URL ||
|
|
168
168
|
process.env.NEXT_PUBLIC_APP_URL ||
|
|
169
169
|
process.env.APP_URL ||
|
|
170
|
-
'http://localhost:
|
|
170
|
+
'http://localhost:3000'
|
|
171
171
|
|
|
172
172
|
// Ensure path starts with /api
|
|
173
173
|
const apiPath = path.startsWith('/api') ? path : `/api${path}`
|
|
@@ -83,7 +83,7 @@ async function parseApiEndpoints(): Promise<ApiEndpoint[]> {
|
|
|
83
83
|
process.env.NEXT_PUBLIC_API_BASE_URL ||
|
|
84
84
|
process.env.NEXT_PUBLIC_APP_URL ||
|
|
85
85
|
process.env.APP_URL ||
|
|
86
|
-
'http://localhost:
|
|
86
|
+
'http://localhost:3000'
|
|
87
87
|
|
|
88
88
|
const openApiUrl = `${baseUrl}/api/docs/openapi`
|
|
89
89
|
|