@techery/appspector-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Techery
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,293 @@
1
+ # AppSpector MCP Server
2
+
3
+ MCP (Model Context Protocol) server for AppSpector - enables loading console/network logs from mobile devices and executing curl commands from captured HTTP requests.
4
+
5
+ ## Quick Start with npx
6
+
7
+ Run directly without installation:
8
+
9
+ ```bash
10
+ npx @techery/appspector-mcp
11
+ ```
12
+
13
+ Or with authentication:
14
+
15
+ ```bash
16
+ EMAIL=your@email.com PASSWORD=yourpassword npx @techery/appspector-mcp
17
+ ```
18
+
19
+ ## Features
20
+
21
+ - **Authentication**: Login with email/password to AppSpector
22
+ - **Apps Management**: List all apps in your account
23
+ - **Sessions**: View online and historical sessions
24
+ - **Console Logs**: Fetch and filter console logs from sessions
25
+ - **HTTP Logs**: View network requests with full details (headers, body, timings)
26
+ - **cURL Generation**: Generate curl commands from captured HTTP requests
27
+ - **cURL Execution**: Execute curl commands directly to replay requests
28
+
29
+ ## Installation
30
+
31
+ ### Global Installation
32
+
33
+ ```bash
34
+ npm install -g @techery/appspector-mcp
35
+ ```
36
+
37
+ Then run:
38
+
39
+ ```bash
40
+ appspector-mcp
41
+ ```
42
+
43
+ ### Local Installation
44
+
45
+ ```bash
46
+ npm install @techery/appspector-mcp
47
+ ```
48
+
49
+ ## Configuration for Claude Desktop
50
+
51
+ Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json`):
52
+
53
+ ### Using npx (Recommended)
54
+
55
+ ```json
56
+ {
57
+ "mcpServers": {
58
+ "appspector": {
59
+ "command": "npx",
60
+ "args": ["-y", "@techery/appspector-mcp"],
61
+ "env": {
62
+ "EMAIL": "your@email.com",
63
+ "PASSWORD": "yourpassword"
64
+ }
65
+ }
66
+ }
67
+ }
68
+ ```
69
+
70
+ ### Using Global Installation
71
+
72
+ ```json
73
+ {
74
+ "mcpServers": {
75
+ "appspector": {
76
+ "command": "appspector-mcp",
77
+ "env": {
78
+ "EMAIL": "your@email.com",
79
+ "PASSWORD": "yourpassword"
80
+ }
81
+ }
82
+ }
83
+ }
84
+ ```
85
+
86
+ ### Without Auto-Login
87
+
88
+ If you prefer to login manually via the `login` tool:
89
+
90
+ ```json
91
+ {
92
+ "mcpServers": {
93
+ "appspector": {
94
+ "command": "npx",
95
+ "args": ["-y", "@techery/appspector-mcp"]
96
+ }
97
+ }
98
+ }
99
+ ```
100
+
101
+ ## Available Tools
102
+
103
+ ### `login`
104
+ Login to AppSpector with your email and password.
105
+
106
+ **Parameters:**
107
+ - `email` (string): Your AppSpector email
108
+ - `password` (string): Your AppSpector password
109
+
110
+ ### `get_apps`
111
+ Get list of all apps in your AppSpector account.
112
+
113
+ ### `get_sessions`
114
+ Get sessions for an app or all online sessions.
115
+
116
+ **Parameters:**
117
+ - `app_id` (number, optional): App ID to get sessions for
118
+ - `limit` (number, optional): Maximum number of sessions (default: 20)
119
+
120
+ ### `get_console_logs`
121
+ Get console logs from a session.
122
+
123
+ **Parameters:**
124
+ - `app_id` (number): App ID
125
+ - `session_id` (string): Session ID
126
+ - `limit` (number, optional): Maximum number of logs (default: 100)
127
+ - `level` (string, optional): Filter by log level (verbose, debug, info, warning, error)
128
+ - `search` (string, optional): Search term to filter logs
129
+
130
+ ### `get_http_logs`
131
+ Get HTTP/network logs from a session.
132
+
133
+ **Parameters:**
134
+ - `app_id` (number): App ID
135
+ - `session_id` (string): Session ID
136
+ - `limit` (number, optional): Maximum number of logs (default: 50)
137
+ - `method` (string, optional): Filter by HTTP method
138
+ - `status` (number, optional): Filter by status code
139
+ - `host` (string, optional): Filter by host
140
+ - `search` (string, optional): Search term to filter by URL
141
+
142
+ ### `get_http_log_details`
143
+ Get detailed information about a specific HTTP request.
144
+
145
+ **Parameters:**
146
+ - `app_id` (number): App ID
147
+ - `session_id` (string): Session ID
148
+ - `request_uid` (string): Request UID from get_http_logs
149
+
150
+ ### `get_curl`
151
+ Generate a curl command from an HTTP request.
152
+
153
+ **Parameters:**
154
+ - `app_id` (number): App ID
155
+ - `session_id` (string): Session ID
156
+ - `request_uid` (string): Request UID from get_http_logs
157
+
158
+ ### `execute_curl`
159
+ Execute a curl command (generated from HTTP request or custom).
160
+
161
+ **Parameters:**
162
+ - `app_id` (number, optional): App ID (required with request_uid)
163
+ - `session_id` (string, optional): Session ID (required with request_uid)
164
+ - `request_uid` (string, optional): Request UID to replay
165
+ - `curl_command` (string, optional): Custom curl command
166
+
167
+ ### `clear_cache`
168
+ Clear the session data cache.
169
+
170
+ **Parameters:**
171
+ - `session_id` (string, optional): Specific session to clear
172
+
173
+ ## Usage Example
174
+
175
+ ```
176
+ 1. Login to AppSpector
177
+ → Use login tool with email and password (or set EMAIL/PASSWORD env vars)
178
+
179
+ 2. List your apps
180
+ → Use get_apps tool
181
+
182
+ 3. Get sessions for an app
183
+ → Use get_sessions with app_id
184
+
185
+ 4. View HTTP logs
186
+ → Use get_http_logs with app_id and session_id
187
+
188
+ 5. Get details of a specific request
189
+ → Use get_http_log_details with request_uid
190
+
191
+ 6. Generate and execute curl
192
+ → Use get_curl to generate curl command
193
+ → Use execute_curl to replay the request
194
+ ```
195
+
196
+ ## Deployment Options
197
+
198
+ ### 1. npx (Recommended for Claude Desktop)
199
+
200
+ ```json
201
+ {
202
+ "mcpServers": {
203
+ "appspector": {
204
+ "command": "npx",
205
+ "args": ["-y", "@techery/appspector-mcp"],
206
+ "env": {
207
+ "EMAIL": "your@email.com",
208
+ "PASSWORD": "yourpassword"
209
+ }
210
+ }
211
+ }
212
+ }
213
+ ```
214
+
215
+ ### 2. HTTP Server (SSE) - for remote deployment
216
+
217
+ ```bash
218
+ npx @techery/appspector-mcp-http
219
+ ```
220
+
221
+ Or with environment variables:
222
+
223
+ ```bash
224
+ EMAIL=your@email.com PASSWORD=yourpassword npx @techery/appspector-mcp-http
225
+ ```
226
+
227
+ Server runs at `http://localhost:3000` with:
228
+ - `GET /sse` - SSE endpoint for MCP connections
229
+ - `GET /health` - Health check endpoint
230
+
231
+ ### 3. Docker
232
+
233
+ Build and run:
234
+ ```bash
235
+ # Clone and build
236
+ git clone https://github.com/techery/appspector-mcp.git
237
+ cd appspector-mcp
238
+ npm install
239
+ npm run docker:build
240
+
241
+ # Run
242
+ docker run -p 3000:3000 \
243
+ -e EMAIL=your@email.com \
244
+ -e PASSWORD=yourpassword \
245
+ appspector-mcp
246
+ ```
247
+
248
+ Or with docker-compose:
249
+
250
+ ```bash
251
+ # Create .env file
252
+ echo "EMAIL=your@email.com" >> .env
253
+ echo "PASSWORD=yourpassword" >> .env
254
+
255
+ # Run
256
+ docker-compose up -d
257
+ ```
258
+
259
+ ## Environment Variables
260
+
261
+ | Variable | Description |
262
+ |----------|-------------|
263
+ | `EMAIL` | AppSpector account email for auto-login |
264
+ | `PASSWORD` | AppSpector account password for auto-login |
265
+ | `PORT` | HTTP server port (default: 3000) |
266
+ | `HOST` | HTTP server bind address (default: 0.0.0.0) |
267
+
268
+ ## Development
269
+
270
+ ```bash
271
+ # Clone repository
272
+ git clone https://github.com/techery/appspector-mcp.git
273
+ cd appspector-mcp
274
+
275
+ # Install dependencies
276
+ npm install
277
+
278
+ # Run stdio server in dev mode
279
+ npm run dev
280
+
281
+ # Run HTTP server in dev mode
282
+ npm run dev:http
283
+
284
+ # Build
285
+ npm run build
286
+
287
+ # Lint
288
+ npm run lint
289
+ ```
290
+
291
+ ## License
292
+
293
+ MIT
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=api-client.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-client.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/api-client.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,88 @@
1
+ import { describe, it, expect, beforeAll } from 'vitest';
2
+ import { AppSpectorApiClient } from '../api-client.js';
3
+ const TEST_EMAIL = process.env.TEST_EMAIL;
4
+ const TEST_PASSWORD = process.env.TEST_PASSWORD;
5
+ describe('AppSpectorApiClient', () => {
6
+ const client = new AppSpectorApiClient();
7
+ describe('authentication', () => {
8
+ it('should fail login with invalid credentials', async () => {
9
+ await expect(client.login('invalid@email.com', 'wrongpassword')).rejects.toThrow();
10
+ });
11
+ it('should login with valid credentials', async () => {
12
+ if (!TEST_EMAIL || !TEST_PASSWORD) {
13
+ console.log('Skipping: TEST_EMAIL and TEST_PASSWORD not set');
14
+ return;
15
+ }
16
+ client.setCredentials({ email: TEST_EMAIL, password: TEST_PASSWORD });
17
+ const session = await client.login(TEST_EMAIL, TEST_PASSWORD);
18
+ expect(session).toBeDefined();
19
+ expect(session.email).toBe(TEST_EMAIL);
20
+ expect(session.token).toBeDefined();
21
+ expect(session.account).toBeDefined();
22
+ });
23
+ });
24
+ describe('apps', () => {
25
+ beforeAll(async () => {
26
+ if (TEST_EMAIL && TEST_PASSWORD) {
27
+ client.setCredentials({ email: TEST_EMAIL, password: TEST_PASSWORD });
28
+ await client.login(TEST_EMAIL, TEST_PASSWORD);
29
+ }
30
+ });
31
+ it('should get apps list', async () => {
32
+ if (!TEST_EMAIL || !TEST_PASSWORD) {
33
+ console.log('Skipping: TEST_EMAIL and TEST_PASSWORD not set');
34
+ return;
35
+ }
36
+ const apps = await client.getApps();
37
+ expect(apps).toBeDefined();
38
+ expect(Array.isArray(apps)).toBe(true);
39
+ if (apps.length > 0) {
40
+ const app = apps.at(0);
41
+ expect(app).toHaveProperty('id');
42
+ expect(app).toHaveProperty('name');
43
+ expect(app).toHaveProperty('platform');
44
+ expect(app).toHaveProperty('bundleId');
45
+ }
46
+ });
47
+ });
48
+ describe('sessions', () => {
49
+ beforeAll(async () => {
50
+ if (TEST_EMAIL && TEST_PASSWORD) {
51
+ client.setCredentials({ email: TEST_EMAIL, password: TEST_PASSWORD });
52
+ await client.login(TEST_EMAIL, TEST_PASSWORD);
53
+ }
54
+ });
55
+ it('should get online sessions', async () => {
56
+ if (!TEST_EMAIL || !TEST_PASSWORD) {
57
+ console.log('Skipping: TEST_EMAIL and TEST_PASSWORD not set');
58
+ return;
59
+ }
60
+ const sessions = await client.getOnlineSessions();
61
+ expect(sessions).toBeDefined();
62
+ expect(Array.isArray(sessions)).toBe(true);
63
+ });
64
+ it('should get sessions by app', async () => {
65
+ if (!TEST_EMAIL || !TEST_PASSWORD) {
66
+ console.log('Skipping: TEST_EMAIL and TEST_PASSWORD not set');
67
+ return;
68
+ }
69
+ const apps = await client.getApps();
70
+ if (apps.length === 0) {
71
+ console.log('Skipping: No apps available');
72
+ return;
73
+ }
74
+ const appId = apps.at(0).id;
75
+ const sessions = await client.getSessionsByApp({ appId, limit: 5 });
76
+ expect(sessions).toBeDefined();
77
+ expect(Array.isArray(sessions)).toBe(true);
78
+ if (sessions.length > 0) {
79
+ const session = sessions.at(0);
80
+ expect(session).toHaveProperty('id');
81
+ expect(session).toHaveProperty('appName');
82
+ expect(session).toHaveProperty('device');
83
+ expect(session).toHaveProperty('state');
84
+ }
85
+ });
86
+ });
87
+ });
88
+ //# sourceMappingURL=api-client.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-client.test.js","sourceRoot":"","sources":["../../src/__tests__/api-client.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=session-loader.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-loader.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/session-loader.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,189 @@
1
+ import { describe, it, expect, beforeAll } from 'vitest';
2
+ import { AppSpectorApiClient } from '../api-client.js';
3
+ import { loadSessionHistory, createCurlString } from '../session-loader.js';
4
+ const TEST_EMAIL = process.env.TEST_EMAIL;
5
+ const TEST_PASSWORD = process.env.TEST_PASSWORD;
6
+ describe('Session Loader', () => {
7
+ const client = new AppSpectorApiClient();
8
+ beforeAll(async () => {
9
+ if (TEST_EMAIL && TEST_PASSWORD) {
10
+ client.setCredentials({ email: TEST_EMAIL, password: TEST_PASSWORD });
11
+ await client.login(TEST_EMAIL, TEST_PASSWORD);
12
+ }
13
+ });
14
+ describe('loadSessionHistory', () => {
15
+ it('should load session history with logs', async () => {
16
+ if (!TEST_EMAIL || !TEST_PASSWORD) {
17
+ console.log('Skipping: TEST_EMAIL and TEST_PASSWORD not set');
18
+ return;
19
+ }
20
+ const apps = await client.getApps();
21
+ if (apps.length === 0) {
22
+ console.log('Skipping: No apps available');
23
+ return;
24
+ }
25
+ const appId = apps.at(0).id;
26
+ const sessions = await client.getSessionsByApp({ appId, limit: 5 });
27
+ if (sessions.length === 0) {
28
+ console.log('Skipping: No sessions available');
29
+ return;
30
+ }
31
+ const session = sessions.at(0);
32
+ const token = client.getToken();
33
+ if (!token) {
34
+ throw new Error('No token available');
35
+ }
36
+ const history = await loadSessionHistory({ session, userToken: token });
37
+ expect(history).toBeDefined();
38
+ expect(history).toHaveProperty('httpLogs');
39
+ expect(history).toHaveProperty('consoleLogs');
40
+ expect(Array.isArray(history.httpLogs)).toBe(true);
41
+ expect(Array.isArray(history.consoleLogs)).toBe(true);
42
+ console.log(`Loaded ${history.httpLogs.length} HTTP logs, ${history.consoleLogs.length} console logs`);
43
+ });
44
+ });
45
+ describe('createCurlString', () => {
46
+ it('should generate curl for GET request', () => {
47
+ const log = {
48
+ uid: 'test-uid-1',
49
+ timestamp: Date.now(),
50
+ request: {
51
+ method: 'GET',
52
+ url: 'https://api.example.com/users?page=1',
53
+ protocol: 'https',
54
+ host: 'api.example.com',
55
+ pathname: '/users?page=1',
56
+ headers: {
57
+ 'Authorization': 'Bearer token123',
58
+ 'Accept': 'application/json',
59
+ },
60
+ body: new Uint8Array(),
61
+ isLargeBody: false,
62
+ },
63
+ response: {
64
+ statusCode: 200,
65
+ headers: { 'Content-Type': 'application/json' },
66
+ body: new Uint8Array(),
67
+ duration: 100,
68
+ isLargeBody: false,
69
+ },
70
+ };
71
+ const curl = createCurlString(log);
72
+ expect(curl).toBeDefined();
73
+ expect(curl).toContain('curl');
74
+ expect(curl).toContain('https://api.example.com/users?page=1');
75
+ expect(curl).toContain("-H 'Authorization: Bearer token123'");
76
+ expect(curl).toContain("-H 'Accept: application/json'");
77
+ });
78
+ it('should generate curl for POST request with body', () => {
79
+ const bodyContent = JSON.stringify({ name: 'Test', email: 'test@example.com' });
80
+ const bodyBytes = new TextEncoder().encode(bodyContent);
81
+ const log = {
82
+ uid: 'test-uid-2',
83
+ timestamp: Date.now(),
84
+ request: {
85
+ method: 'POST',
86
+ url: 'https://api.example.com/users',
87
+ protocol: 'https',
88
+ host: 'api.example.com',
89
+ pathname: '/users',
90
+ headers: {
91
+ 'Content-Type': 'application/json',
92
+ 'Authorization': 'Bearer token123',
93
+ },
94
+ body: bodyBytes,
95
+ isLargeBody: false,
96
+ },
97
+ response: {
98
+ statusCode: 201,
99
+ headers: { 'Content-Type': 'application/json' },
100
+ body: new Uint8Array(),
101
+ duration: 150,
102
+ isLargeBody: false,
103
+ },
104
+ };
105
+ const curl = createCurlString(log);
106
+ expect(curl).toBeDefined();
107
+ expect(curl).toContain('curl');
108
+ expect(curl).toContain('-X POST');
109
+ expect(curl).toContain('https://api.example.com/users');
110
+ expect(curl).toContain("-H 'Content-Type: application/json'");
111
+ expect(curl).toContain("-d '");
112
+ });
113
+ it('should generate curl for PUT request', () => {
114
+ const log = {
115
+ uid: 'test-uid-3',
116
+ timestamp: Date.now(),
117
+ request: {
118
+ method: 'PUT',
119
+ url: 'https://api.example.com/users/123',
120
+ protocol: 'https',
121
+ host: 'api.example.com',
122
+ pathname: '/users/123',
123
+ headers: {
124
+ 'Content-Type': 'application/json',
125
+ },
126
+ body: new TextEncoder().encode('{"name":"Updated"}'),
127
+ isLargeBody: false,
128
+ },
129
+ response: {
130
+ statusCode: 200,
131
+ headers: {},
132
+ body: new Uint8Array(),
133
+ duration: 120,
134
+ isLargeBody: false,
135
+ },
136
+ };
137
+ const curl = createCurlString(log);
138
+ expect(curl).toBeDefined();
139
+ expect(curl).toContain('-X PUT');
140
+ });
141
+ it('should generate curl for DELETE request', () => {
142
+ const log = {
143
+ uid: 'test-uid-4',
144
+ timestamp: Date.now(),
145
+ request: {
146
+ method: 'DELETE',
147
+ url: 'https://api.example.com/users/123',
148
+ protocol: 'https',
149
+ host: 'api.example.com',
150
+ pathname: '/users/123',
151
+ headers: {},
152
+ body: new Uint8Array(),
153
+ isLargeBody: false,
154
+ },
155
+ response: {
156
+ statusCode: 204,
157
+ headers: {},
158
+ body: new Uint8Array(),
159
+ duration: 80,
160
+ isLargeBody: false,
161
+ },
162
+ };
163
+ const curl = createCurlString(log);
164
+ expect(curl).toBeDefined();
165
+ expect(curl).toContain('-X DELETE');
166
+ });
167
+ it('should handle request without response', () => {
168
+ const log = {
169
+ uid: 'test-uid-5',
170
+ timestamp: Date.now(),
171
+ request: {
172
+ method: 'GET',
173
+ url: 'https://api.example.com/pending',
174
+ protocol: 'https',
175
+ host: 'api.example.com',
176
+ pathname: '/pending',
177
+ headers: {},
178
+ body: new Uint8Array(),
179
+ isLargeBody: false,
180
+ },
181
+ };
182
+ const curl = createCurlString(log);
183
+ expect(curl).toBeDefined();
184
+ expect(curl).toContain('curl');
185
+ expect(curl).toContain('https://api.example.com/pending');
186
+ });
187
+ });
188
+ });
189
+ //# sourceMappingURL=session-loader.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-loader.test.js","sourceRoot":"","sources":["../../src/__tests__/session-loader.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ import type { App, Session, UserSession } from './types.js';
2
+ export declare class AppSpectorApiClient {
3
+ private token;
4
+ private sessionCookie;
5
+ private email;
6
+ private password;
7
+ constructor(email?: string, password?: string);
8
+ setCredentials({ email, password }: {
9
+ email: string;
10
+ password: string;
11
+ }): void;
12
+ private ensureAuthenticated;
13
+ private request;
14
+ login(email: string, password: string): Promise<UserSession>;
15
+ getApps(): Promise<ReadonlyArray<App>>;
16
+ getSessionsByApp({ appId, limit, fromId, query, }: {
17
+ appId: number;
18
+ limit?: number;
19
+ fromId?: number;
20
+ query?: string;
21
+ }): Promise<ReadonlyArray<Session>>;
22
+ getOnlineSessions(): Promise<ReadonlyArray<Session>>;
23
+ getSession({ appId, sessionId }: {
24
+ appId: number;
25
+ sessionId: string;
26
+ }): Promise<Session>;
27
+ getToken(): string | null;
28
+ }
29
+ //# sourceMappingURL=api-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAI5D,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,aAAa,CAAuB;IAC5C,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,QAAQ,CAAuB;gBAE3B,KAAK,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM;IAK7C,cAAc,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;YAOhE,mBAAmB;YAYnB,OAAO;IAoCf,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IA+B5D,OAAO,IAAI,OAAO,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;IAMtC,gBAAgB,CAAC,EACrB,KAAK,EACL,KAAU,EACV,MAAU,EACV,KAAU,GACX,EAAE;QACD,KAAK,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,GAAG,OAAO,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;IAM7B,iBAAiB,IAAI,OAAO,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;IAMpD,UAAU,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC;IAM9F,QAAQ,IAAI,MAAM,GAAG,IAAI;CAG1B"}