@open-mercato/ai-assistant 0.4.2-canary-c02407ff85
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/AGENTS.md +1090 -0
- package/README.md +607 -0
- package/build.mjs +92 -0
- package/dist/di.js +8 -0
- package/dist/di.js.map +7 -0
- package/dist/frontend/components/CommandPalette/CommandFooter.js +80 -0
- package/dist/frontend/components/CommandPalette/CommandFooter.js.map +7 -0
- package/dist/frontend/components/CommandPalette/CommandHeader.js +53 -0
- package/dist/frontend/components/CommandPalette/CommandHeader.js.map +7 -0
- package/dist/frontend/components/CommandPalette/CommandInput.js +29 -0
- package/dist/frontend/components/CommandPalette/CommandInput.js.map +7 -0
- package/dist/frontend/components/CommandPalette/CommandItem.js +92 -0
- package/dist/frontend/components/CommandPalette/CommandItem.js.map +7 -0
- package/dist/frontend/components/CommandPalette/CommandPalette.js +244 -0
- package/dist/frontend/components/CommandPalette/CommandPalette.js.map +7 -0
- package/dist/frontend/components/CommandPalette/CommandPaletteProvider.js +42 -0
- package/dist/frontend/components/CommandPalette/CommandPaletteProvider.js.map +7 -0
- package/dist/frontend/components/CommandPalette/CommandPaletteWrapper.js +18 -0
- package/dist/frontend/components/CommandPalette/CommandPaletteWrapper.js.map +7 -0
- package/dist/frontend/components/CommandPalette/DebugPanel.js +215 -0
- package/dist/frontend/components/CommandPalette/DebugPanel.js.map +7 -0
- package/dist/frontend/components/CommandPalette/MessageBubble.js +64 -0
- package/dist/frontend/components/CommandPalette/MessageBubble.js.map +7 -0
- package/dist/frontend/components/CommandPalette/ToolCallConfirmation.js +91 -0
- package/dist/frontend/components/CommandPalette/ToolCallConfirmation.js.map +7 -0
- package/dist/frontend/components/CommandPalette/ToolCallDisplay.js +47 -0
- package/dist/frontend/components/CommandPalette/ToolCallDisplay.js.map +7 -0
- package/dist/frontend/components/CommandPalette/ToolChatPage.js +74 -0
- package/dist/frontend/components/CommandPalette/ToolChatPage.js.map +7 -0
- package/dist/frontend/components/CommandPalette/index.js +28 -0
- package/dist/frontend/components/CommandPalette/index.js.map +7 -0
- package/dist/frontend/constants.js +41 -0
- package/dist/frontend/constants.js.map +7 -0
- package/dist/frontend/hooks/index.js +13 -0
- package/dist/frontend/hooks/index.js.map +7 -0
- package/dist/frontend/hooks/useCommandPalette.js +1094 -0
- package/dist/frontend/hooks/useCommandPalette.js.map +7 -0
- package/dist/frontend/hooks/useMcpTools.js +66 -0
- package/dist/frontend/hooks/useMcpTools.js.map +7 -0
- package/dist/frontend/hooks/usePageContext.js +48 -0
- package/dist/frontend/hooks/usePageContext.js.map +7 -0
- package/dist/frontend/hooks/useRecentActions.js +56 -0
- package/dist/frontend/hooks/useRecentActions.js.map +7 -0
- package/dist/frontend/hooks/useRecentTools.js +55 -0
- package/dist/frontend/hooks/useRecentTools.js.map +7 -0
- package/dist/frontend/index.js +35 -0
- package/dist/frontend/index.js.map +7 -0
- package/dist/frontend/types.js +1 -0
- package/dist/frontend/types.js.map +7 -0
- package/dist/frontend/utils/index.js +7 -0
- package/dist/frontend/utils/index.js.map +7 -0
- package/dist/frontend/utils/toolMatcher.js +95 -0
- package/dist/frontend/utils/toolMatcher.js.map +7 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +7 -0
- package/dist/modules/ai_assistant/acl.js +14 -0
- package/dist/modules/ai_assistant/acl.js.map +7 -0
- package/dist/modules/ai_assistant/api/chat/route.js +152 -0
- package/dist/modules/ai_assistant/api/chat/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/health/route.js +27 -0
- package/dist/modules/ai_assistant/api/health/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/route/route.js +123 -0
- package/dist/modules/ai_assistant/api/route/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/settings/route.js +60 -0
- package/dist/modules/ai_assistant/api/settings/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/tools/execute/route.js +58 -0
- package/dist/modules/ai_assistant/api/tools/execute/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/tools/route.js +48 -0
- package/dist/modules/ai_assistant/api/tools/route.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/page.js +10 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/page.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/page.meta.js +28 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/page.meta.js.map +7 -0
- package/dist/modules/ai_assistant/cli.js +192 -0
- package/dist/modules/ai_assistant/cli.js.map +7 -0
- package/dist/modules/ai_assistant/di.js +11 -0
- package/dist/modules/ai_assistant/di.js.map +7 -0
- package/dist/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.js +257 -0
- package/dist/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.js.map +7 -0
- package/dist/modules/ai_assistant/index.js +13 -0
- package/dist/modules/ai_assistant/index.js.map +7 -0
- package/dist/modules/ai_assistant/lib/ai-sdk.js +13 -0
- package/dist/modules/ai_assistant/lib/ai-sdk.js.map +7 -0
- package/dist/modules/ai_assistant/lib/api-discovery-tools.js +249 -0
- package/dist/modules/ai_assistant/lib/api-discovery-tools.js.map +7 -0
- package/dist/modules/ai_assistant/lib/api-endpoint-index-config.js +177 -0
- package/dist/modules/ai_assistant/lib/api-endpoint-index-config.js.map +7 -0
- package/dist/modules/ai_assistant/lib/api-endpoint-index.js +210 -0
- package/dist/modules/ai_assistant/lib/api-endpoint-index.js.map +7 -0
- package/dist/modules/ai_assistant/lib/auth.js +87 -0
- package/dist/modules/ai_assistant/lib/auth.js.map +7 -0
- package/dist/modules/ai_assistant/lib/chat-config.js +117 -0
- package/dist/modules/ai_assistant/lib/chat-config.js.map +7 -0
- package/dist/modules/ai_assistant/lib/client-factory.js +60 -0
- package/dist/modules/ai_assistant/lib/client-factory.js.map +7 -0
- package/dist/modules/ai_assistant/lib/http-server.js +367 -0
- package/dist/modules/ai_assistant/lib/http-server.js.map +7 -0
- package/dist/modules/ai_assistant/lib/in-process-client.js +126 -0
- package/dist/modules/ai_assistant/lib/in-process-client.js.map +7 -0
- package/dist/modules/ai_assistant/lib/mcp-client.js +146 -0
- package/dist/modules/ai_assistant/lib/mcp-client.js.map +7 -0
- package/dist/modules/ai_assistant/lib/mcp-dev-server.js +283 -0
- package/dist/modules/ai_assistant/lib/mcp-dev-server.js.map +7 -0
- package/dist/modules/ai_assistant/lib/mcp-server-config.js +160 -0
- package/dist/modules/ai_assistant/lib/mcp-server-config.js.map +7 -0
- package/dist/modules/ai_assistant/lib/mcp-server.js +156 -0
- package/dist/modules/ai_assistant/lib/mcp-server.js.map +7 -0
- package/dist/modules/ai_assistant/lib/mcp-tool-adapter.js +44 -0
- package/dist/modules/ai_assistant/lib/mcp-tool-adapter.js.map +7 -0
- package/dist/modules/ai_assistant/lib/opencode-client.js +247 -0
- package/dist/modules/ai_assistant/lib/opencode-client.js.map +7 -0
- package/dist/modules/ai_assistant/lib/opencode-handlers.js +398 -0
- package/dist/modules/ai_assistant/lib/opencode-handlers.js.map +7 -0
- package/dist/modules/ai_assistant/lib/schema-utils.js +94 -0
- package/dist/modules/ai_assistant/lib/schema-utils.js.map +7 -0
- package/dist/modules/ai_assistant/lib/tool-executor.js +55 -0
- package/dist/modules/ai_assistant/lib/tool-executor.js.map +7 -0
- package/dist/modules/ai_assistant/lib/tool-index-config.js +125 -0
- package/dist/modules/ai_assistant/lib/tool-index-config.js.map +7 -0
- package/dist/modules/ai_assistant/lib/tool-loader.js +88 -0
- package/dist/modules/ai_assistant/lib/tool-loader.js.map +7 -0
- package/dist/modules/ai_assistant/lib/tool-registry.js +65 -0
- package/dist/modules/ai_assistant/lib/tool-registry.js.map +7 -0
- package/dist/modules/ai_assistant/lib/tool-search.js +192 -0
- package/dist/modules/ai_assistant/lib/tool-search.js.map +7 -0
- package/dist/modules/ai_assistant/lib/types.js +1 -0
- package/dist/modules/ai_assistant/lib/types.js.map +7 -0
- package/package.json +108 -0
- package/src/di.ts +11 -0
- package/src/frontend/components/CommandPalette/CommandFooter.tsx +113 -0
- package/src/frontend/components/CommandPalette/CommandHeader.tsx +76 -0
- package/src/frontend/components/CommandPalette/CommandInput.tsx +50 -0
- package/src/frontend/components/CommandPalette/CommandItem.tsx +111 -0
- package/src/frontend/components/CommandPalette/CommandPalette.tsx +276 -0
- package/src/frontend/components/CommandPalette/CommandPaletteProvider.tsx +60 -0
- package/src/frontend/components/CommandPalette/CommandPaletteWrapper.tsx +21 -0
- package/src/frontend/components/CommandPalette/DebugPanel.tsx +257 -0
- package/src/frontend/components/CommandPalette/MessageBubble.tsx +73 -0
- package/src/frontend/components/CommandPalette/ToolCallConfirmation.tsx +130 -0
- package/src/frontend/components/CommandPalette/ToolCallDisplay.tsx +57 -0
- package/src/frontend/components/CommandPalette/ToolChatPage.tsx +125 -0
- package/src/frontend/components/CommandPalette/index.ts +14 -0
- package/src/frontend/constants.ts +35 -0
- package/src/frontend/hooks/index.ts +5 -0
- package/src/frontend/hooks/useCommandPalette.ts +1389 -0
- package/src/frontend/hooks/useMcpTools.ts +73 -0
- package/src/frontend/hooks/usePageContext.ts +61 -0
- package/src/frontend/hooks/useRecentActions.ts +64 -0
- package/src/frontend/hooks/useRecentTools.ts +69 -0
- package/src/frontend/index.ts +39 -0
- package/src/frontend/types.ts +260 -0
- package/src/frontend/utils/index.ts +1 -0
- package/src/frontend/utils/toolMatcher.ts +127 -0
- package/src/index.ts +92 -0
- package/src/modules/ai_assistant/acl.ts +10 -0
- package/src/modules/ai_assistant/api/chat/route.ts +213 -0
- package/src/modules/ai_assistant/api/health/route.ts +30 -0
- package/src/modules/ai_assistant/api/route/route.ts +149 -0
- package/src/modules/ai_assistant/api/settings/route.ts +73 -0
- package/src/modules/ai_assistant/api/tools/execute/route.ts +71 -0
- package/src/modules/ai_assistant/api/tools/route.ts +57 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/page.meta.ts +26 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/page.tsx +12 -0
- package/src/modules/ai_assistant/cli.ts +233 -0
- package/src/modules/ai_assistant/di.ts +9 -0
- package/src/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.tsx +418 -0
- package/src/modules/ai_assistant/index.ts +11 -0
- package/src/modules/ai_assistant/lib/ai-sdk.ts +5 -0
- package/src/modules/ai_assistant/lib/api-discovery-tools.ts +334 -0
- package/src/modules/ai_assistant/lib/api-endpoint-index-config.ts +243 -0
- package/src/modules/ai_assistant/lib/api-endpoint-index.ts +381 -0
- package/src/modules/ai_assistant/lib/auth.ts +185 -0
- package/src/modules/ai_assistant/lib/chat-config.ts +152 -0
- package/src/modules/ai_assistant/lib/client-factory.ts +130 -0
- package/src/modules/ai_assistant/lib/http-server.ts +498 -0
- package/src/modules/ai_assistant/lib/in-process-client.ts +205 -0
- package/src/modules/ai_assistant/lib/mcp-client.ts +221 -0
- package/src/modules/ai_assistant/lib/mcp-dev-server.ts +373 -0
- package/src/modules/ai_assistant/lib/mcp-server-config.ts +287 -0
- package/src/modules/ai_assistant/lib/mcp-server.ts +214 -0
- package/src/modules/ai_assistant/lib/mcp-tool-adapter.ts +76 -0
- package/src/modules/ai_assistant/lib/opencode-client.ts +426 -0
- package/src/modules/ai_assistant/lib/opencode-handlers.ts +676 -0
- package/src/modules/ai_assistant/lib/schema-utils.ts +142 -0
- package/src/modules/ai_assistant/lib/tool-executor.ts +71 -0
- package/src/modules/ai_assistant/lib/tool-index-config.ts +178 -0
- package/src/modules/ai_assistant/lib/tool-loader.ts +149 -0
- package/src/modules/ai_assistant/lib/tool-registry.ts +114 -0
- package/src/modules/ai_assistant/lib/tool-search.ts +308 -0
- package/src/modules/ai_assistant/lib/types.ts +147 -0
- package/test-schema.ts +37 -0
- package/tsconfig.json +10 -0
- package/watch.mjs +6 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import {
|
|
2
|
+
API_ENDPOINT_ENTITY_ID,
|
|
3
|
+
GLOBAL_TENANT_ID,
|
|
4
|
+
API_ENDPOINT_SEARCH_CONFIG,
|
|
5
|
+
endpointToIndexableRecord,
|
|
6
|
+
computeEndpointsChecksum
|
|
7
|
+
} from "./api-endpoint-index-config.js";
|
|
8
|
+
const API_ENDPOINT_ENTITY = API_ENDPOINT_ENTITY_ID;
|
|
9
|
+
let endpointsCache = null;
|
|
10
|
+
let endpointsByOperationId = null;
|
|
11
|
+
async function getApiEndpoints() {
|
|
12
|
+
if (endpointsCache) {
|
|
13
|
+
return endpointsCache;
|
|
14
|
+
}
|
|
15
|
+
endpointsCache = await parseApiEndpoints();
|
|
16
|
+
endpointsByOperationId = new Map(endpointsCache.map((e) => [e.operationId, e]));
|
|
17
|
+
return endpointsCache;
|
|
18
|
+
}
|
|
19
|
+
async function getEndpointByOperationId(operationId) {
|
|
20
|
+
await getApiEndpoints();
|
|
21
|
+
return endpointsByOperationId?.get(operationId) ?? null;
|
|
22
|
+
}
|
|
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:3000";
|
|
25
|
+
const openApiUrl = `${baseUrl}/api/docs/openapi`;
|
|
26
|
+
try {
|
|
27
|
+
console.error(`[API Index] Fetching OpenAPI spec from ${openApiUrl}...`);
|
|
28
|
+
const response = await fetch(openApiUrl);
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
console.error(`[API Index] Failed to fetch OpenAPI spec: ${response.status} ${response.statusText}`);
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
const doc = await response.json();
|
|
34
|
+
console.error(`[API Index] Successfully fetched OpenAPI spec`);
|
|
35
|
+
return extractEndpoints(doc);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error("[API Index] Could not fetch OpenAPI spec:", error instanceof Error ? error.message : error);
|
|
38
|
+
console.error("[API Index] Make sure the app is running at", baseUrl);
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function extractEndpoints(doc) {
|
|
43
|
+
const endpoints = [];
|
|
44
|
+
const validMethods = ["get", "post", "put", "patch", "delete"];
|
|
45
|
+
if (!doc.paths) {
|
|
46
|
+
return endpoints;
|
|
47
|
+
}
|
|
48
|
+
for (const [path, pathItem] of Object.entries(doc.paths)) {
|
|
49
|
+
if (!pathItem || typeof pathItem !== "object") continue;
|
|
50
|
+
for (const [method, operation] of Object.entries(pathItem)) {
|
|
51
|
+
if (!validMethods.includes(method.toLowerCase())) continue;
|
|
52
|
+
if (!operation || typeof operation !== "object") continue;
|
|
53
|
+
const op = operation;
|
|
54
|
+
const operationId = op.operationId || generateOperationId(path, method);
|
|
55
|
+
const endpoint = {
|
|
56
|
+
id: operationId,
|
|
57
|
+
operationId,
|
|
58
|
+
method: method.toUpperCase(),
|
|
59
|
+
path,
|
|
60
|
+
summary: op.summary || "",
|
|
61
|
+
description: op.description || op.summary || `${method.toUpperCase()} ${path}`,
|
|
62
|
+
tags: op.tags || [],
|
|
63
|
+
requiredFeatures: op["x-require-features"] || [],
|
|
64
|
+
deprecated: op.deprecated || false,
|
|
65
|
+
parameters: extractParameters(op.parameters || []),
|
|
66
|
+
requestBodySchema: extractRequestBodySchema(op.requestBody, doc.components?.schemas)
|
|
67
|
+
};
|
|
68
|
+
endpoints.push(endpoint);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
console.error(`[API Index] Parsed ${endpoints.length} endpoints from OpenAPI spec`);
|
|
72
|
+
return endpoints;
|
|
73
|
+
}
|
|
74
|
+
function generateOperationId(path, method) {
|
|
75
|
+
const pathParts = path.replace(/^\//, "").replace(/\{([^}]+)\}/g, "by_$1").split("/").filter(Boolean).join("_");
|
|
76
|
+
return `${method.toLowerCase()}_${pathParts}`;
|
|
77
|
+
}
|
|
78
|
+
function extractParameters(params) {
|
|
79
|
+
return params.filter((p) => p.in === "path" || p.in === "query").map((p) => ({
|
|
80
|
+
name: p.name,
|
|
81
|
+
in: p.in,
|
|
82
|
+
required: p.required ?? false,
|
|
83
|
+
type: p.schema?.type || "string",
|
|
84
|
+
description: p.description || ""
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
function extractRequestBodySchema(requestBody, schemas) {
|
|
88
|
+
if (!requestBody?.content?.["application/json"]?.schema) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const schema = requestBody.content["application/json"].schema;
|
|
92
|
+
if (schema.$ref && schemas) {
|
|
93
|
+
const refPath = schema.$ref.replace("#/components/schemas/", "");
|
|
94
|
+
return schemas[refPath] || schema;
|
|
95
|
+
}
|
|
96
|
+
return schema;
|
|
97
|
+
}
|
|
98
|
+
let lastIndexChecksum = null;
|
|
99
|
+
async function indexApiEndpoints(searchService, force = false) {
|
|
100
|
+
const endpoints = await getApiEndpoints();
|
|
101
|
+
if (endpoints.length === 0) {
|
|
102
|
+
console.error("[API Index] No endpoints to index");
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
const checksum = computeEndpointsChecksum(
|
|
106
|
+
endpoints.map((e) => ({ operationId: e.operationId, method: e.method, path: e.path }))
|
|
107
|
+
);
|
|
108
|
+
if (!force && lastIndexChecksum === checksum) {
|
|
109
|
+
console.error(`[API Index] Skipping indexing - ${endpoints.length} endpoints unchanged`);
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
const records = endpoints.map(
|
|
113
|
+
(endpoint) => endpointToIndexableRecord(endpoint)
|
|
114
|
+
);
|
|
115
|
+
try {
|
|
116
|
+
console.error(`[API Index] Starting bulk index of ${records.length} endpoints...`);
|
|
117
|
+
const timeoutMs = 6e4;
|
|
118
|
+
const indexPromise = searchService.bulkIndex(records);
|
|
119
|
+
const timeoutPromise = new Promise(
|
|
120
|
+
(_, reject) => setTimeout(() => reject(new Error(`Bulk index timed out after ${timeoutMs}ms`)), timeoutMs)
|
|
121
|
+
);
|
|
122
|
+
await Promise.race([indexPromise, timeoutPromise]);
|
|
123
|
+
lastIndexChecksum = checksum;
|
|
124
|
+
console.error(`[API Index] Indexed ${records.length} API endpoints for hybrid search`);
|
|
125
|
+
return records.length;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error("[API Index] Failed to index endpoints:", error);
|
|
128
|
+
lastIndexChecksum = checksum;
|
|
129
|
+
return records.length;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function buildSearchableContent(endpoint) {
|
|
133
|
+
const parts = [
|
|
134
|
+
endpoint.operationId,
|
|
135
|
+
endpoint.method,
|
|
136
|
+
endpoint.path,
|
|
137
|
+
endpoint.summary,
|
|
138
|
+
endpoint.description,
|
|
139
|
+
...endpoint.tags,
|
|
140
|
+
...endpoint.parameters.map((p) => `${p.name} ${p.description}`)
|
|
141
|
+
];
|
|
142
|
+
return parts.filter(Boolean).join(" ");
|
|
143
|
+
}
|
|
144
|
+
async function searchEndpoints(searchService, query, options = {}) {
|
|
145
|
+
const { limit = API_ENDPOINT_SEARCH_CONFIG.defaultLimit, method } = options;
|
|
146
|
+
await getApiEndpoints();
|
|
147
|
+
if (searchService) {
|
|
148
|
+
try {
|
|
149
|
+
const results = await searchService.search(query, {
|
|
150
|
+
tenantId: GLOBAL_TENANT_ID,
|
|
151
|
+
organizationId: null,
|
|
152
|
+
entityTypes: [API_ENDPOINT_ENTITY_ID],
|
|
153
|
+
limit: limit * 2
|
|
154
|
+
// Get extra to account for filtering
|
|
155
|
+
});
|
|
156
|
+
const endpoints = [];
|
|
157
|
+
for (const result of results) {
|
|
158
|
+
if (endpoints.length >= limit) break;
|
|
159
|
+
const endpoint = endpointsByOperationId?.get(result.recordId);
|
|
160
|
+
if (endpoint) {
|
|
161
|
+
if (method && endpoint.method !== method.toUpperCase()) continue;
|
|
162
|
+
endpoints.push(endpoint);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (endpoints.length > 0) {
|
|
166
|
+
return endpoints;
|
|
167
|
+
}
|
|
168
|
+
console.error("[API Index] No hybrid search results, falling back to in-memory search");
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.error("[API Index] Hybrid search failed, falling back to in-memory:", error);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return searchEndpointsFallback(query, { limit, method });
|
|
174
|
+
}
|
|
175
|
+
function searchEndpointsFallback(query, options = {}) {
|
|
176
|
+
const { limit = API_ENDPOINT_SEARCH_CONFIG.defaultLimit, method } = options;
|
|
177
|
+
if (!endpointsCache) {
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
180
|
+
const queryLower = query.toLowerCase();
|
|
181
|
+
const queryTerms = queryLower.split(/\s+/).filter(Boolean);
|
|
182
|
+
let matches = endpointsCache.filter((endpoint) => {
|
|
183
|
+
const content = buildSearchableContent(endpoint).toLowerCase();
|
|
184
|
+
return queryTerms.some((term) => content.includes(term));
|
|
185
|
+
});
|
|
186
|
+
if (method) {
|
|
187
|
+
matches = matches.filter((e) => e.method === method.toUpperCase());
|
|
188
|
+
}
|
|
189
|
+
matches.sort((a, b) => {
|
|
190
|
+
const aContent = buildSearchableContent(a).toLowerCase();
|
|
191
|
+
const bContent = buildSearchableContent(b).toLowerCase();
|
|
192
|
+
const aScore = queryTerms.filter((t) => aContent.includes(t)).length;
|
|
193
|
+
const bScore = queryTerms.filter((t) => bContent.includes(t)).length;
|
|
194
|
+
return bScore - aScore;
|
|
195
|
+
});
|
|
196
|
+
return matches.slice(0, limit);
|
|
197
|
+
}
|
|
198
|
+
function clearEndpointCache() {
|
|
199
|
+
endpointsCache = null;
|
|
200
|
+
endpointsByOperationId = null;
|
|
201
|
+
}
|
|
202
|
+
export {
|
|
203
|
+
API_ENDPOINT_ENTITY,
|
|
204
|
+
clearEndpointCache,
|
|
205
|
+
getApiEndpoints,
|
|
206
|
+
getEndpointByOperationId,
|
|
207
|
+
indexApiEndpoints,
|
|
208
|
+
searchEndpoints
|
|
209
|
+
};
|
|
210
|
+
//# sourceMappingURL=api-endpoint-index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 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: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
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
async function authenticateMcpRequest(apiKeySecret, container) {
|
|
2
|
+
if (!apiKeySecret || typeof apiKeySecret !== "string") {
|
|
3
|
+
return { success: false, error: "API key is required" };
|
|
4
|
+
}
|
|
5
|
+
const trimmedSecret = apiKeySecret.trim();
|
|
6
|
+
if (!trimmedSecret) {
|
|
7
|
+
return { success: false, error: "API key is required" };
|
|
8
|
+
}
|
|
9
|
+
if (!trimmedSecret.startsWith("omk_")) {
|
|
10
|
+
return { success: false, error: "Invalid API key format" };
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const em = container.resolve("em");
|
|
14
|
+
const { findApiKeyBySecret } = await import("@open-mercato/core/modules/api_keys/services/apiKeyService");
|
|
15
|
+
const apiKey = await findApiKeyBySecret(em, trimmedSecret);
|
|
16
|
+
if (!apiKey) {
|
|
17
|
+
return { success: false, error: "Invalid or expired API key" };
|
|
18
|
+
}
|
|
19
|
+
const userId = `api_key:${apiKey.id}`;
|
|
20
|
+
const rbacService = container.resolve("rbacService");
|
|
21
|
+
const acl = await rbacService.loadAcl(userId, {
|
|
22
|
+
tenantId: apiKey.tenantId ?? null,
|
|
23
|
+
organizationId: apiKey.organizationId ?? null
|
|
24
|
+
});
|
|
25
|
+
try {
|
|
26
|
+
apiKey.lastUsedAt = /* @__PURE__ */ new Date();
|
|
27
|
+
await em.persistAndFlush(apiKey);
|
|
28
|
+
} catch {
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
success: true,
|
|
32
|
+
keyId: apiKey.id,
|
|
33
|
+
keyName: apiKey.name,
|
|
34
|
+
tenantId: apiKey.tenantId ?? null,
|
|
35
|
+
organizationId: apiKey.organizationId ?? null,
|
|
36
|
+
userId,
|
|
37
|
+
features: acl.features,
|
|
38
|
+
isSuperAdmin: acl.isSuperAdmin
|
|
39
|
+
};
|
|
40
|
+
} catch (error) {
|
|
41
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
42
|
+
console.error("[MCP Auth] Authentication failed:", message);
|
|
43
|
+
return { success: false, error: "Authentication failed" };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function hasRequiredFeatures(requiredFeatures, userFeatures, isSuperAdmin) {
|
|
47
|
+
if (isSuperAdmin) return true;
|
|
48
|
+
if (!requiredFeatures?.length) return true;
|
|
49
|
+
return requiredFeatures.every((required) => {
|
|
50
|
+
if (userFeatures.includes(required)) return true;
|
|
51
|
+
if (userFeatures.includes("*")) return true;
|
|
52
|
+
return userFeatures.some((feature) => {
|
|
53
|
+
if (feature.endsWith(".*")) {
|
|
54
|
+
const prefix = feature.slice(0, -2);
|
|
55
|
+
return required.startsWith(prefix + ".");
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function extractApiKeyFromHeaders(headers) {
|
|
62
|
+
const getHeader = (name) => {
|
|
63
|
+
if (headers instanceof Headers) {
|
|
64
|
+
return headers.get(name);
|
|
65
|
+
}
|
|
66
|
+
if (headers instanceof Map) {
|
|
67
|
+
return headers.get(name) ?? null;
|
|
68
|
+
}
|
|
69
|
+
const value = headers[name] ?? headers[name.toLowerCase()];
|
|
70
|
+
return typeof value === "string" ? value : null;
|
|
71
|
+
};
|
|
72
|
+
const xApiKey = getHeader("x-api-key")?.trim();
|
|
73
|
+
if (xApiKey) {
|
|
74
|
+
return xApiKey;
|
|
75
|
+
}
|
|
76
|
+
const authHeader = getHeader("authorization")?.trim();
|
|
77
|
+
if (authHeader && authHeader.toLowerCase().startsWith("apikey ")) {
|
|
78
|
+
return authHeader.slice(7).trim();
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
export {
|
|
83
|
+
authenticateMcpRequest,
|
|
84
|
+
extractApiKeyFromHeaders,
|
|
85
|
+
hasRequiredFeatures
|
|
86
|
+
};
|
|
87
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/ai_assistant/lib/auth.ts"],
|
|
4
|
+
"sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\n\n/**\n * Successful authentication result.\n */\nexport type McpAuthSuccess = {\n success: true\n keyId: string\n keyName: string\n tenantId: string | null\n organizationId: string | null\n userId: string\n features: string[]\n isSuperAdmin: boolean\n}\n\n/**\n * Failed authentication result.\n */\nexport type McpAuthFailure = {\n success: false\n error: string\n}\n\n/**\n * Result from MCP authentication.\n */\nexport type McpAuthResult = McpAuthSuccess | McpAuthFailure\n\n/**\n * Authenticate an MCP request using an API key.\n *\n * This function validates the API key secret and loads the associated\n * ACL (features, organizations, super admin status) from the key's roles.\n *\n * @param apiKeySecret - The full API key secret (e.g., 'omk_xxxx.yyyy...')\n * @param container - Awilix DI container with 'em' and 'rbacService'\n * @returns Authentication result with user context or error\n */\nexport async function authenticateMcpRequest(\n apiKeySecret: string,\n container: AwilixContainer\n): Promise<McpAuthResult> {\n if (!apiKeySecret || typeof apiKeySecret !== 'string') {\n return { success: false, error: 'API key is required' }\n }\n\n const trimmedSecret = apiKeySecret.trim()\n if (!trimmedSecret) {\n return { success: false, error: 'API key is required' }\n }\n\n if (!trimmedSecret.startsWith('omk_')) {\n return { success: false, error: 'Invalid API key format' }\n }\n\n try {\n const em = container.resolve('em') as EntityManager\n\n const { findApiKeyBySecret } = await import(\n '@open-mercato/core/modules/api_keys/services/apiKeyService'\n )\n\n const apiKey = await findApiKeyBySecret(em, trimmedSecret)\n\n if (!apiKey) {\n return { success: false, error: 'Invalid or expired API key' }\n }\n\n const userId = `api_key:${apiKey.id}`\n\n const rbacService = container.resolve('rbacService') as {\n loadAcl: (\n userId: string,\n scope: { tenantId: string | null; organizationId: string | null }\n ) => Promise<{\n isSuperAdmin: boolean\n features: string[]\n organizations: string[] | null\n }>\n }\n\n const acl = await rbacService.loadAcl(userId, {\n tenantId: apiKey.tenantId ?? null,\n organizationId: apiKey.organizationId ?? null,\n })\n\n try {\n apiKey.lastUsedAt = new Date()\n await em.persistAndFlush(apiKey)\n } catch {\n // Best-effort update; ignore write failures\n }\n\n return {\n success: true,\n keyId: apiKey.id,\n keyName: apiKey.name,\n tenantId: apiKey.tenantId ?? null,\n organizationId: apiKey.organizationId ?? null,\n userId,\n features: acl.features,\n isSuperAdmin: acl.isSuperAdmin,\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n console.error('[MCP Auth] Authentication failed:', message)\n return { success: false, error: 'Authentication failed' }\n }\n}\n\n/**\n * Check if user has the required features for a resource.\n *\n * Supports:\n * - Super admin bypass (always returns true)\n * - Direct feature match (e.g., 'customers.view')\n * - Global wildcard ('*' grants all features)\n * - Prefix wildcard (e.g., 'customers.*' grants 'customers.people.view')\n *\n * @param requiredFeatures - List of features required for access\n * @param userFeatures - List of features the user has\n * @param isSuperAdmin - Whether the user is a super admin\n * @returns True if user has access\n */\nexport function hasRequiredFeatures(\n requiredFeatures: string[] | undefined,\n userFeatures: string[],\n isSuperAdmin: boolean\n): boolean {\n if (isSuperAdmin) return true\n if (!requiredFeatures?.length) return true\n\n return requiredFeatures.every((required) => {\n if (userFeatures.includes(required)) return true\n if (userFeatures.includes('*')) return true\n\n // Check wildcard patterns (e.g., 'customers.*' grants 'customers.people.view')\n return userFeatures.some((feature) => {\n if (feature.endsWith('.*')) {\n const prefix = feature.slice(0, -2)\n return required.startsWith(prefix + '.')\n }\n return false\n })\n })\n}\n\n/**\n * Extract API key from HTTP request headers.\n *\n * Supports two header formats:\n * - x-api-key: <secret>\n * - Authorization: ApiKey <secret>\n *\n * @param headers - Request headers (Map, Headers, or plain object)\n * @returns The API key secret or null if not found\n */\nexport function extractApiKeyFromHeaders(\n headers: Headers | Map<string, string> | Record<string, string | undefined>\n): string | null {\n const getHeader = (name: string): string | null => {\n if (headers instanceof Headers) {\n return headers.get(name)\n }\n if (headers instanceof Map) {\n return headers.get(name) ?? null\n }\n const value = headers[name] ?? headers[name.toLowerCase()]\n return typeof value === 'string' ? value : null\n }\n\n const xApiKey = getHeader('x-api-key')?.trim()\n if (xApiKey) {\n return xApiKey\n }\n\n const authHeader = getHeader('authorization')?.trim()\n if (authHeader && authHeader.toLowerCase().startsWith('apikey ')) {\n return authHeader.slice(7).trim()\n }\n\n return null\n}\n"],
|
|
5
|
+
"mappings": "AAwCA,eAAsB,uBACpB,cACA,WACwB;AACxB,MAAI,CAAC,gBAAgB,OAAO,iBAAiB,UAAU;AACrD,WAAO,EAAE,SAAS,OAAO,OAAO,sBAAsB;AAAA,EACxD;AAEA,QAAM,gBAAgB,aAAa,KAAK;AACxC,MAAI,CAAC,eAAe;AAClB,WAAO,EAAE,SAAS,OAAO,OAAO,sBAAsB;AAAA,EACxD;AAEA,MAAI,CAAC,cAAc,WAAW,MAAM,GAAG;AACrC,WAAO,EAAE,SAAS,OAAO,OAAO,yBAAyB;AAAA,EAC3D;AAEA,MAAI;AACF,UAAM,KAAK,UAAU,QAAQ,IAAI;AAEjC,UAAM,EAAE,mBAAmB,IAAI,MAAM,OACnC,4DACF;AAEA,UAAM,SAAS,MAAM,mBAAmB,IAAI,aAAa;AAEzD,QAAI,CAAC,QAAQ;AACX,aAAO,EAAE,SAAS,OAAO,OAAO,6BAA6B;AAAA,IAC/D;AAEA,UAAM,SAAS,WAAW,OAAO,EAAE;AAEnC,UAAM,cAAc,UAAU,QAAQ,aAAa;AAWnD,UAAM,MAAM,MAAM,YAAY,QAAQ,QAAQ;AAAA,MAC5C,UAAU,OAAO,YAAY;AAAA,MAC7B,gBAAgB,OAAO,kBAAkB;AAAA,IAC3C,CAAC;AAED,QAAI;AACF,aAAO,aAAa,oBAAI,KAAK;AAC7B,YAAM,GAAG,gBAAgB,MAAM;AAAA,IACjC,QAAQ;AAAA,IAER;AAEA,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO,OAAO;AAAA,MACd,SAAS,OAAO;AAAA,MAChB,UAAU,OAAO,YAAY;AAAA,MAC7B,gBAAgB,OAAO,kBAAkB;AAAA,MACzC;AAAA,MACA,UAAU,IAAI;AAAA,MACd,cAAc,IAAI;AAAA,IACpB;AAAA,EACF,SAAS,OAAO;AACd,UAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,YAAQ,MAAM,qCAAqC,OAAO;AAC1D,WAAO,EAAE,SAAS,OAAO,OAAO,wBAAwB;AAAA,EAC1D;AACF;AAgBO,SAAS,oBACd,kBACA,cACA,cACS;AACT,MAAI,aAAc,QAAO;AACzB,MAAI,CAAC,kBAAkB,OAAQ,QAAO;AAEtC,SAAO,iBAAiB,MAAM,CAAC,aAAa;AAC1C,QAAI,aAAa,SAAS,QAAQ,EAAG,QAAO;AAC5C,QAAI,aAAa,SAAS,GAAG,EAAG,QAAO;AAGvC,WAAO,aAAa,KAAK,CAAC,YAAY;AACpC,UAAI,QAAQ,SAAS,IAAI,GAAG;AAC1B,cAAM,SAAS,QAAQ,MAAM,GAAG,EAAE;AAClC,eAAO,SAAS,WAAW,SAAS,GAAG;AAAA,MACzC;AACA,aAAO;AAAA,IACT,CAAC;AAAA,EACH,CAAC;AACH;AAYO,SAAS,yBACd,SACe;AACf,QAAM,YAAY,CAAC,SAAgC;AACjD,QAAI,mBAAmB,SAAS;AAC9B,aAAO,QAAQ,IAAI,IAAI;AAAA,IACzB;AACA,QAAI,mBAAmB,KAAK;AAC1B,aAAO,QAAQ,IAAI,IAAI,KAAK;AAAA,IAC9B;AACA,UAAM,QAAQ,QAAQ,IAAI,KAAK,QAAQ,KAAK,YAAY,CAAC;AACzD,WAAO,OAAO,UAAU,WAAW,QAAQ;AAAA,EAC7C;AAEA,QAAM,UAAU,UAAU,WAAW,GAAG,KAAK;AAC7C,MAAI,SAAS;AACX,WAAO;AAAA,EACT;AAEA,QAAM,aAAa,UAAU,eAAe,GAAG,KAAK;AACpD,MAAI,cAAc,WAAW,YAAY,EAAE,WAAW,SAAS,GAAG;AAChE,WAAO,WAAW,MAAM,CAAC,EAAE,KAAK;AAAA,EAClC;AAEA,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const CHAT_CONFIG_KEY = "chat_provider";
|
|
2
|
+
const CHAT_PROVIDERS = {
|
|
3
|
+
openai: {
|
|
4
|
+
name: "OpenAI",
|
|
5
|
+
envKeyRequired: "OPENAI_API_KEY",
|
|
6
|
+
defaultModel: "gpt-4o",
|
|
7
|
+
models: [
|
|
8
|
+
{ id: "gpt-4o", name: "GPT-4o", contextWindow: 128e3 },
|
|
9
|
+
{ id: "gpt-4o-mini", name: "GPT-4o Mini", contextWindow: 128e3 },
|
|
10
|
+
{ id: "gpt-4-turbo", name: "GPT-4 Turbo", contextWindow: 128e3 },
|
|
11
|
+
{ id: "gpt-3.5-turbo", name: "GPT-3.5 Turbo", contextWindow: 16385 }
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
anthropic: {
|
|
15
|
+
name: "Anthropic",
|
|
16
|
+
envKeyRequired: "ANTHROPIC_API_KEY",
|
|
17
|
+
defaultModel: "claude-sonnet-4-5-20250929",
|
|
18
|
+
models: [
|
|
19
|
+
{ id: "claude-sonnet-4-5-20250929", name: "Claude Sonnet 4.5", contextWindow: 2e5 },
|
|
20
|
+
{ id: "claude-sonnet-4-20250514", name: "Claude Sonnet 4", contextWindow: 2e5 },
|
|
21
|
+
{ id: "claude-3-5-sonnet-20241022", name: "Claude 3.5 Sonnet", contextWindow: 2e5 },
|
|
22
|
+
{ id: "claude-3-5-haiku-20241022", name: "Claude 3.5 Haiku", contextWindow: 2e5 }
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
google: {
|
|
26
|
+
name: "Google",
|
|
27
|
+
envKeyRequired: "GOOGLE_GENERATIVE_AI_API_KEY",
|
|
28
|
+
defaultModel: "gemini-1.5-pro",
|
|
29
|
+
models: [
|
|
30
|
+
{ id: "gemini-1.5-pro", name: "Gemini 1.5 Pro", contextWindow: 2097152 },
|
|
31
|
+
{ id: "gemini-1.5-flash", name: "Gemini 1.5 Flash", contextWindow: 1048576 },
|
|
32
|
+
{ id: "gemini-pro", name: "Gemini Pro", contextWindow: 32e3 }
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const DEFAULT_CHAT_CONFIG = {
|
|
37
|
+
providerId: "openai",
|
|
38
|
+
model: "gpt-4o"
|
|
39
|
+
};
|
|
40
|
+
function isProviderConfigured(providerId) {
|
|
41
|
+
switch (providerId) {
|
|
42
|
+
case "openai":
|
|
43
|
+
return Boolean(process.env.OPENAI_API_KEY?.trim());
|
|
44
|
+
case "anthropic":
|
|
45
|
+
return Boolean(process.env.ANTHROPIC_API_KEY?.trim());
|
|
46
|
+
case "google":
|
|
47
|
+
return Boolean(process.env.GOOGLE_GENERATIVE_AI_API_KEY?.trim());
|
|
48
|
+
default:
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function getConfiguredProviders() {
|
|
53
|
+
const providers = [];
|
|
54
|
+
const allProviders = ["openai", "anthropic", "google"];
|
|
55
|
+
for (const providerId of allProviders) {
|
|
56
|
+
if (isProviderConfigured(providerId)) {
|
|
57
|
+
providers.push(providerId);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return providers;
|
|
61
|
+
}
|
|
62
|
+
async function resolveChatConfig(resolver, options) {
|
|
63
|
+
const fallback = options?.defaultValue ?? null;
|
|
64
|
+
let service;
|
|
65
|
+
try {
|
|
66
|
+
service = resolver.resolve("moduleConfigService");
|
|
67
|
+
} catch {
|
|
68
|
+
return fallback;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const value = await service.getValue("ai_assistant", CHAT_CONFIG_KEY, { defaultValue: fallback });
|
|
72
|
+
return value;
|
|
73
|
+
} catch {
|
|
74
|
+
return fallback;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function saveChatConfig(resolver, config) {
|
|
78
|
+
let service;
|
|
79
|
+
try {
|
|
80
|
+
service = resolver.resolve("moduleConfigService");
|
|
81
|
+
} catch {
|
|
82
|
+
throw new Error("Configuration service unavailable");
|
|
83
|
+
}
|
|
84
|
+
const fullConfig = {
|
|
85
|
+
...config,
|
|
86
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
87
|
+
};
|
|
88
|
+
await service.setValue("ai_assistant", CHAT_CONFIG_KEY, fullConfig);
|
|
89
|
+
return fullConfig;
|
|
90
|
+
}
|
|
91
|
+
function createDefaultConfig() {
|
|
92
|
+
return { ...DEFAULT_CHAT_CONFIG, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
93
|
+
}
|
|
94
|
+
function getModelInfo(providerId, modelId) {
|
|
95
|
+
const provider = CHAT_PROVIDERS[providerId];
|
|
96
|
+
if (!provider) return null;
|
|
97
|
+
return provider.models.find((m) => m.id === modelId) ?? null;
|
|
98
|
+
}
|
|
99
|
+
function formatContextWindow(contextWindow) {
|
|
100
|
+
if (contextWindow >= 1e6) {
|
|
101
|
+
return `${(contextWindow / 1e6).toFixed(1)}M`;
|
|
102
|
+
}
|
|
103
|
+
return `${(contextWindow / 1e3).toFixed(0)}K`;
|
|
104
|
+
}
|
|
105
|
+
export {
|
|
106
|
+
CHAT_CONFIG_KEY,
|
|
107
|
+
CHAT_PROVIDERS,
|
|
108
|
+
DEFAULT_CHAT_CONFIG,
|
|
109
|
+
createDefaultConfig,
|
|
110
|
+
formatContextWindow,
|
|
111
|
+
getConfiguredProviders,
|
|
112
|
+
getModelInfo,
|
|
113
|
+
isProviderConfigured,
|
|
114
|
+
resolveChatConfig,
|
|
115
|
+
saveChatConfig
|
|
116
|
+
};
|
|
117
|
+
//# sourceMappingURL=chat-config.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/ai_assistant/lib/chat-config.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'\n\n// Types\nexport type ChatProviderId = 'openai' | 'anthropic' | 'google'\n\nexport type ChatModelInfo = {\n id: string\n name: string\n contextWindow: number\n}\n\nexport type ChatProviderInfo = {\n name: string\n envKeyRequired: string\n defaultModel: string\n models: ChatModelInfo[]\n}\n\nexport type ChatProviderConfig = {\n providerId: ChatProviderId\n model: string\n updatedAt: string\n}\n\n// Constants\nexport const CHAT_CONFIG_KEY = 'chat_provider'\n\nexport const CHAT_PROVIDERS: Record<ChatProviderId, ChatProviderInfo> = {\n openai: {\n name: 'OpenAI',\n envKeyRequired: 'OPENAI_API_KEY',\n defaultModel: 'gpt-4o',\n models: [\n { id: 'gpt-4o', name: 'GPT-4o', contextWindow: 128000 },\n { id: 'gpt-4o-mini', name: 'GPT-4o Mini', contextWindow: 128000 },\n { id: 'gpt-4-turbo', name: 'GPT-4 Turbo', contextWindow: 128000 },\n { id: 'gpt-3.5-turbo', name: 'GPT-3.5 Turbo', contextWindow: 16385 },\n ],\n },\n anthropic: {\n name: 'Anthropic',\n envKeyRequired: 'ANTHROPIC_API_KEY',\n defaultModel: 'claude-sonnet-4-5-20250929',\n models: [\n { id: 'claude-sonnet-4-5-20250929', name: 'Claude Sonnet 4.5', contextWindow: 200000 },\n { id: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4', contextWindow: 200000 },\n { id: 'claude-3-5-sonnet-20241022', name: 'Claude 3.5 Sonnet', contextWindow: 200000 },\n { id: 'claude-3-5-haiku-20241022', name: 'Claude 3.5 Haiku', contextWindow: 200000 },\n ],\n },\n google: {\n name: 'Google',\n envKeyRequired: 'GOOGLE_GENERATIVE_AI_API_KEY',\n defaultModel: 'gemini-1.5-pro',\n models: [\n { id: 'gemini-1.5-pro', name: 'Gemini 1.5 Pro', contextWindow: 2097152 },\n { id: 'gemini-1.5-flash', name: 'Gemini 1.5 Flash', contextWindow: 1048576 },\n { id: 'gemini-pro', name: 'Gemini Pro', contextWindow: 32000 },\n ],\n },\n}\n\nexport const DEFAULT_CHAT_CONFIG: Omit<ChatProviderConfig, 'updatedAt'> = {\n providerId: 'openai',\n model: 'gpt-4o',\n}\n\n// Provider configuration checks\nexport function isProviderConfigured(providerId: ChatProviderId): boolean {\n switch (providerId) {\n case 'openai':\n return Boolean(process.env.OPENAI_API_KEY?.trim())\n case 'anthropic':\n return Boolean(process.env.ANTHROPIC_API_KEY?.trim())\n case 'google':\n return Boolean(process.env.GOOGLE_GENERATIVE_AI_API_KEY?.trim())\n default:\n return false\n }\n}\n\nexport function getConfiguredProviders(): ChatProviderId[] {\n const providers: ChatProviderId[] = []\n const allProviders: ChatProviderId[] = ['openai', 'anthropic', 'google']\n for (const providerId of allProviders) {\n if (isProviderConfigured(providerId)) {\n providers.push(providerId)\n }\n }\n return providers\n}\n\n// Config resolution\ntype Resolver = {\n resolve: <T = unknown>(name: string) => T\n}\n\nexport async function resolveChatConfig(\n resolver: Resolver,\n options?: { defaultValue?: ChatProviderConfig | null }\n): Promise<ChatProviderConfig | null> {\n const fallback = options?.defaultValue ?? null\n let service: ModuleConfigService\n try {\n service = resolver.resolve<ModuleConfigService>('moduleConfigService')\n } catch {\n return fallback\n }\n try {\n const value = await service.getValue<ChatProviderConfig>('ai_assistant', CHAT_CONFIG_KEY, { defaultValue: fallback })\n return value\n } catch {\n return fallback\n }\n}\n\nexport async function saveChatConfig(\n resolver: Resolver,\n config: Omit<ChatProviderConfig, 'updatedAt'>\n): Promise<ChatProviderConfig> {\n let service: ModuleConfigService\n try {\n service = resolver.resolve<ModuleConfigService>('moduleConfigService')\n } catch {\n throw new Error('Configuration service unavailable')\n }\n const fullConfig: ChatProviderConfig = {\n ...config,\n updatedAt: new Date().toISOString(),\n }\n await service.setValue('ai_assistant', CHAT_CONFIG_KEY, fullConfig)\n return fullConfig\n}\n\nexport function createDefaultConfig(): ChatProviderConfig {\n return { ...DEFAULT_CHAT_CONFIG, updatedAt: new Date().toISOString() }\n}\n\n// Get model info by ID\nexport function getModelInfo(providerId: ChatProviderId, modelId: string): ChatModelInfo | null {\n const provider = CHAT_PROVIDERS[providerId]\n if (!provider) return null\n return provider.models.find((m) => m.id === modelId) ?? null\n}\n\n// Format context window for display\nexport function formatContextWindow(contextWindow: number): string {\n if (contextWindow >= 1000000) {\n return `${(contextWindow / 1000000).toFixed(1)}M`\n }\n return `${(contextWindow / 1000).toFixed(0)}K`\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
async function createMcpClient(options) {
|
|
2
|
+
const { mode, apiKeySecret } = options;
|
|
3
|
+
if (!apiKeySecret) {
|
|
4
|
+
throw new Error("API key secret is required");
|
|
5
|
+
}
|
|
6
|
+
switch (mode) {
|
|
7
|
+
case "in-process": {
|
|
8
|
+
if (!options.container) {
|
|
9
|
+
throw new Error("DI container is required for in-process mode");
|
|
10
|
+
}
|
|
11
|
+
const { InProcessMcpClient } = await import("./in-process-client.js");
|
|
12
|
+
return InProcessMcpClient.create({
|
|
13
|
+
apiKeySecret,
|
|
14
|
+
container: options.container
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
case "stdio": {
|
|
18
|
+
const { McpClient } = await import("./mcp-client.js");
|
|
19
|
+
const stdioOptions = {
|
|
20
|
+
transport: "stdio",
|
|
21
|
+
apiKeySecret
|
|
22
|
+
};
|
|
23
|
+
if (options.stdioCommand) {
|
|
24
|
+
stdioOptions.command = options.stdioCommand;
|
|
25
|
+
}
|
|
26
|
+
if (options.stdioArgs) {
|
|
27
|
+
stdioOptions.args = options.stdioArgs;
|
|
28
|
+
} else {
|
|
29
|
+
stdioOptions.args = [
|
|
30
|
+
"mercato",
|
|
31
|
+
"ai_assistant",
|
|
32
|
+
"mcp:serve",
|
|
33
|
+
"--api-key",
|
|
34
|
+
apiKeySecret
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
if (options.cwd) {
|
|
38
|
+
stdioOptions.cwd = options.cwd;
|
|
39
|
+
}
|
|
40
|
+
return McpClient.connect(stdioOptions);
|
|
41
|
+
}
|
|
42
|
+
case "http": {
|
|
43
|
+
if (!options.httpUrl) {
|
|
44
|
+
throw new Error("HTTP URL is required for http mode");
|
|
45
|
+
}
|
|
46
|
+
const { McpClient } = await import("./mcp-client.js");
|
|
47
|
+
return McpClient.connect({
|
|
48
|
+
transport: "http",
|
|
49
|
+
apiKeySecret,
|
|
50
|
+
url: options.httpUrl
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
default:
|
|
54
|
+
throw new Error(`Unknown client mode: ${mode}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export {
|
|
58
|
+
createMcpClient
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=client-factory.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/ai_assistant/lib/client-factory.ts"],
|
|
4
|
+
"sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport type { McpClientInterface } from './types'\n\n/**\n * Client connection mode.\n */\nexport type ClientMode = 'in-process' | 'stdio' | 'http'\n\n/**\n * Options for creating an MCP client.\n */\nexport type CreateClientOptions = {\n /** Connection mode */\n mode: ClientMode\n /** API key secret for authentication */\n apiKeySecret: string\n /** DI container (required for in-process mode) */\n container?: AwilixContainer\n /** HTTP server URL (required for http mode) */\n httpUrl?: string\n /** Custom command for stdio mode (default: 'yarn') */\n stdioCommand?: string\n /** Custom args for stdio mode (default: mercato mcp:serve) */\n stdioArgs?: string[]\n /** Working directory for stdio mode */\n cwd?: string\n}\n\n/**\n * Create an MCP client with the specified connection mode.\n *\n * All modes authenticate via API key, ensuring consistent ACL enforcement.\n *\n * @example\n * ```typescript\n * // In-process mode (fastest, same process)\n * const client = await createMcpClient({\n * mode: 'in-process',\n * apiKeySecret: 'omk_xxx.yyy',\n * container: diContainer,\n * })\n *\n * // Stdio mode (subprocess)\n * const client = await createMcpClient({\n * mode: 'stdio',\n * apiKeySecret: 'omk_xxx.yyy',\n * })\n *\n * // HTTP mode (network)\n * const client = await createMcpClient({\n * mode: 'http',\n * apiKeySecret: 'omk_xxx.yyy',\n * httpUrl: 'http://localhost:3001/mcp',\n * })\n *\n * // Use client (same interface for all modes)\n * const tools = await client.listTools()\n * const result = await client.callTool('search.query', { query: 'test' })\n * await client.close()\n * ```\n */\nexport async function createMcpClient(options: CreateClientOptions): Promise<McpClientInterface> {\n const { mode, apiKeySecret } = options\n\n if (!apiKeySecret) {\n throw new Error('API key secret is required')\n }\n\n switch (mode) {\n case 'in-process': {\n if (!options.container) {\n throw new Error('DI container is required for in-process mode')\n }\n\n const { InProcessMcpClient } = await import('./in-process-client')\n return InProcessMcpClient.create({\n apiKeySecret,\n container: options.container,\n })\n }\n\n case 'stdio': {\n const { McpClient } = await import('./mcp-client')\n\n const stdioOptions: any = {\n transport: 'stdio' as const,\n apiKeySecret,\n }\n\n if (options.stdioCommand) {\n stdioOptions.command = options.stdioCommand\n }\n\n if (options.stdioArgs) {\n stdioOptions.args = options.stdioArgs\n } else {\n // Default args include the API key\n stdioOptions.args = [\n 'mercato',\n 'ai_assistant',\n 'mcp:serve',\n '--api-key',\n apiKeySecret,\n ]\n }\n\n if (options.cwd) {\n stdioOptions.cwd = options.cwd\n }\n\n return McpClient.connect(stdioOptions)\n }\n\n case 'http': {\n if (!options.httpUrl) {\n throw new Error('HTTP URL is required for http mode')\n }\n\n const { McpClient } = await import('./mcp-client')\n return McpClient.connect({\n transport: 'http',\n apiKeySecret,\n url: options.httpUrl,\n })\n }\n\n default:\n throw new Error(`Unknown client mode: ${mode}`)\n }\n}\n"],
|
|
5
|
+
"mappings": "AA6DA,eAAsB,gBAAgB,SAA2D;AAC/F,QAAM,EAAE,MAAM,aAAa,IAAI;AAE/B,MAAI,CAAC,cAAc;AACjB,UAAM,IAAI,MAAM,4BAA4B;AAAA,EAC9C;AAEA,UAAQ,MAAM;AAAA,IACZ,KAAK,cAAc;AACjB,UAAI,CAAC,QAAQ,WAAW;AACtB,cAAM,IAAI,MAAM,8CAA8C;AAAA,MAChE;AAEA,YAAM,EAAE,mBAAmB,IAAI,MAAM,OAAO,qBAAqB;AACjE,aAAO,mBAAmB,OAAO;AAAA,QAC/B;AAAA,QACA,WAAW,QAAQ;AAAA,MACrB,CAAC;AAAA,IACH;AAAA,IAEA,KAAK,SAAS;AACZ,YAAM,EAAE,UAAU,IAAI,MAAM,OAAO,cAAc;AAEjD,YAAM,eAAoB;AAAA,QACxB,WAAW;AAAA,QACX;AAAA,MACF;AAEA,UAAI,QAAQ,cAAc;AACxB,qBAAa,UAAU,QAAQ;AAAA,MACjC;AAEA,UAAI,QAAQ,WAAW;AACrB,qBAAa,OAAO,QAAQ;AAAA,MAC9B,OAAO;AAEL,qBAAa,OAAO;AAAA,UAClB;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAEA,UAAI,QAAQ,KAAK;AACf,qBAAa,MAAM,QAAQ;AAAA,MAC7B;AAEA,aAAO,UAAU,QAAQ,YAAY;AAAA,IACvC;AAAA,IAEA,KAAK,QAAQ;AACX,UAAI,CAAC,QAAQ,SAAS;AACpB,cAAM,IAAI,MAAM,oCAAoC;AAAA,MACtD;AAEA,YAAM,EAAE,UAAU,IAAI,MAAM,OAAO,cAAc;AACjD,aAAO,UAAU,QAAQ;AAAA,QACvB,WAAW;AAAA,QACX;AAAA,QACA,KAAK,QAAQ;AAAA,MACf,CAAC;AAAA,IACH;AAAA,IAEA;AACE,YAAM,IAAI,MAAM,wBAAwB,IAAI,EAAE;AAAA,EAClD;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|