rewind-mcp-server 0.1.1 → 0.3.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/README.md +84 -11
- package/dist/__tests__/helpers.test.d.ts +1 -0
- package/dist/__tests__/helpers.test.js +131 -0
- package/dist/__tests__/helpers.test.js.map +1 -0
- package/dist/__tests__/image-block.live.d.ts +1 -0
- package/dist/__tests__/image-block.live.js +41 -0
- package/dist/__tests__/image-block.live.js.map +1 -0
- package/dist/__tests__/server.test.js +710 -16
- package/dist/__tests__/server.test.js.map +1 -1
- package/dist/__tests__/stdio-e2e.live.d.ts +1 -0
- package/dist/__tests__/stdio-e2e.live.js +60 -0
- package/dist/__tests__/stdio-e2e.live.js.map +1 -0
- package/dist/__tests__/ui-resource.live.d.ts +1 -0
- package/dist/__tests__/ui-resource.live.js +71 -0
- package/dist/__tests__/ui-resource.live.js.map +1 -0
- package/dist/client.d.ts +9 -0
- package/dist/client.js +15 -0
- package/dist/client.js.map +1 -1
- package/dist/index.js +0 -0
- package/dist/prompts.js +101 -0
- package/dist/prompts.js.map +1 -1
- package/dist/resources/ui.d.ts +33 -0
- package/dist/resources/ui.js +14 -0
- package/dist/resources/ui.js.map +1 -0
- package/dist/resources.js +128 -0
- package/dist/resources.js.map +1 -1
- package/dist/server.js +55 -1
- package/dist/server.js.map +1 -1
- package/dist/tools/collecting.js +78 -37
- package/dist/tools/collecting.js.map +1 -1
- package/dist/tools/cross-domain.js +74 -17
- package/dist/tools/cross-domain.js.map +1 -1
- package/dist/tools/debug.d.ts +6 -0
- package/dist/tools/debug.js +26 -0
- package/dist/tools/debug.js.map +1 -0
- package/dist/tools/helpers.d.ts +81 -5
- package/dist/tools/helpers.js +138 -4
- package/dist/tools/helpers.js.map +1 -1
- package/dist/tools/listening.js +223 -52
- package/dist/tools/listening.js.map +1 -1
- package/dist/tools/reading.js +74 -22
- package/dist/tools/reading.js.map +1 -1
- package/dist/tools/running.js +87 -43
- package/dist/tools/running.js.map +1 -1
- package/dist/tools/watching.js +199 -32
- package/dist/tools/watching.js.map +1 -1
- package/dist/ui-bundles.d.ts +3 -0
- package/dist/ui-bundles.js +8 -0
- package/dist/ui-bundles.js.map +1 -0
- package/dist/worker.d.ts +2 -1
- package/dist/worker.js +55 -30
- package/dist/worker.js.map +1 -1
- package/package.json +13 -2
package/README.md
CHANGED
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
[](https://docs.rewind.rest/mcp-server)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
|
|
8
|
-
MCP server for the [Rewind](https://rewind.rest) personal data API. Gives
|
|
8
|
+
MCP server for the [Rewind](https://rewind.rest) personal data API. Gives AI assistants access to your listening, running, watching, collecting, and reading data.
|
|
9
9
|
|
|
10
10
|
## Setup
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
### Desktop Apps
|
|
13
|
+
|
|
14
|
+
Add to your MCP client config (Claude Desktop, ChatGPT, Gemini, etc.):
|
|
13
15
|
|
|
14
16
|
```json
|
|
15
17
|
{
|
|
@@ -25,24 +27,65 @@ Add to your [Claude Desktop config](https://modelcontextprotocol.io/quickstart/u
|
|
|
25
27
|
}
|
|
26
28
|
```
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
### Mobile & Web
|
|
31
|
+
|
|
32
|
+
Add as a remote integration in your client's settings:
|
|
33
|
+
|
|
34
|
+
- **URL**: `https://mcp.rewind.rest/mcp`
|
|
35
|
+
- **Authorization**: `Bearer rw_live_your_key_here`
|
|
36
|
+
|
|
37
|
+
<details>
|
|
38
|
+
<summary>Claude Code</summary>
|
|
29
39
|
|
|
30
40
|
```bash
|
|
31
41
|
claude mcp add rewind -- npx -y rewind-mcp-server
|
|
32
42
|
```
|
|
33
43
|
|
|
44
|
+
</details>
|
|
45
|
+
|
|
34
46
|
Requires a [Rewind API key](https://docs.rewind.rest/authentication). `REWIND_API_URL` defaults to `https://api.rewind.rest`.
|
|
35
47
|
|
|
36
48
|
## Tools
|
|
37
49
|
|
|
38
|
-
| Domain | Source | Tools
|
|
39
|
-
| ---------------- | ---------------- |
|
|
40
|
-
| **Listening** | Last.fm | Now playing, recent scrobbles, stats, top artists/albums/tracks, streaks, artist and album details |
|
|
41
|
-
| **Running** | Strava | Stats, recent runs, personal records, streaks, activity details, per-mile splits
|
|
42
|
-
| **Watching** | Plex, Letterboxd | Recent watches, movie details, browse by genre/decade/director, stats
|
|
43
|
-
| **Collecting** | Discogs, Trakt | Vinyl collection, physical media (Blu-ray/4K/HD DVD), collection stats
|
|
44
|
-
| **Reading** | Instapaper | Recent articles, highlights, random highlight, stats
|
|
45
|
-
| **Cross-domain** | All | Full-text search, unified feed, on-this-day
|
|
50
|
+
| Domain | Source | Tools |
|
|
51
|
+
| ---------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
52
|
+
| **Listening** | Last.fm | Now playing, recent scrobbles, stats, top artists/albums/tracks, streaks, artist and album details, genre breakdown over time |
|
|
53
|
+
| **Running** | Strava | Stats, recent runs, personal records, streaks, activity details, per-mile splits, per-year summaries |
|
|
54
|
+
| **Watching** | Plex, Letterboxd | Recent watches, movie details, browse by genre/decade/director, stats, genre/decade/director breakdowns |
|
|
55
|
+
| **Collecting** | Discogs, Trakt | Vinyl collection, physical media (Blu-ray/4K UHD/HD DVD), collection and media stats |
|
|
56
|
+
| **Reading** | Instapaper | Recent articles, highlights, random highlight, stats |
|
|
57
|
+
| **Cross-domain** | All | Full-text search, unified feed, on-this-day |
|
|
58
|
+
|
|
59
|
+
## Rich responses
|
|
60
|
+
|
|
61
|
+
Tool responses follow the MCP 2025-06-18 content model and include more than plain text:
|
|
62
|
+
|
|
63
|
+
- **Images.** Detail tools (`get_movie_details`, `get_album_details`, `get_artist_details`) and list tools (`get_recent_watches`, `get_recent_listens`, `get_top_albums`, `get_vinyl_collection`, etc.) return cover art / posters / artist imagery as `image` content blocks (top-N on lists, default N=5). Pass `include_images: false` to skip them when keeping responses compact matters.
|
|
64
|
+
- **Resource links.** External platform URLs come back as `resource_link` content blocks rather than being baked into prose -- Letterboxd reviews, Strava activities, Discogs releases, Apple Music pages, original article URLs, Last.fm pages.
|
|
65
|
+
- **Structured content.** Every tool also returns `structuredContent` with a JSON shape that mirrors the underlying API response, so the model can reason over exact numbers without re-parsing prose.
|
|
66
|
+
|
|
67
|
+
## Entity resources
|
|
68
|
+
|
|
69
|
+
The server exposes `@`-mentionable resources for fetching full detail on any entity:
|
|
70
|
+
|
|
71
|
+
| Entity | URI | Source endpoint |
|
|
72
|
+
| -------------- | -------------------------------- | --------------------------------- |
|
|
73
|
+
| Movie | `rewind://movie/{id}` | `/v1/watching/movies/{id}` |
|
|
74
|
+
| Show | `rewind://show/{id}` | `/v1/watching/shows/{id}` |
|
|
75
|
+
| Album | `rewind://album/{id}` | `/v1/listening/albums/{id}` |
|
|
76
|
+
| Artist | `rewind://artist/{id}` | `/v1/listening/artists/{id}` |
|
|
77
|
+
| Vinyl | `rewind://vinyl/{id}` | `/v1/collecting/vinyl/{id}` |
|
|
78
|
+
| Physical media | `rewind://physical-media/{id}` | `/v1/collecting/media/{id}` |
|
|
79
|
+
| Article | `rewind://article/{id}` | `/v1/reading/articles/{id}` |
|
|
80
|
+
| Activity | `rewind://activity/{id}` | `/v1/running/activities/{id}` |
|
|
81
|
+
| Sync status | `rewind://sync/status` | `/v1/health/sync` |
|
|
82
|
+
| Year in review | `rewind://{domain}/year/{year}` | `/v1/{domain}/year/{year}` |
|
|
83
|
+
|
|
84
|
+
`search` returns `resource_link`s pointing at these URIs so clients can drill from a match straight into the full record.
|
|
85
|
+
|
|
86
|
+
## Interactive UI (MCP Apps)
|
|
87
|
+
|
|
88
|
+
`get_recent_watches` advertises a [MCP Apps](https://blog.modelcontextprotocol.io/posts/2026-01-26-mcp-apps/) UI resource. On clients that render MCP Apps (Claude Desktop, Claude web, VS Code GitHub Copilot, Goose) the tool returns an interactive poster grid inline in the conversation: thumbhash placeholders, theme-aware cards, clickable Letterboxd reviews, hover states. On clients that do not render MCP Apps the existing text + image + resource_link + structuredContent response is unchanged, so no caller regresses.
|
|
46
89
|
|
|
47
90
|
## Example Queries
|
|
48
91
|
|
|
@@ -52,3 +95,33 @@ Requires a [Rewind API key](https://docs.rewind.rest/authentication). `REWIND_AP
|
|
|
52
95
|
- "What Beastie Boys records are missing from my vinyl collection?"
|
|
53
96
|
- "How many articles did I read last year and stack-rank the top 10 sources"
|
|
54
97
|
- "Can you give me a quick summary of everything I did last week?"
|
|
98
|
+
|
|
99
|
+
## Prompts
|
|
100
|
+
|
|
101
|
+
The server exposes slash-command prompts that orchestrate multiple tools for common asks:
|
|
102
|
+
|
|
103
|
+
- `weekly-summary` -- rolls up activity across all domains for the past 7 days
|
|
104
|
+
- `year-in-review` -- comprehensive yearly recap
|
|
105
|
+
- `compare-periods` -- parametric comparison between two time windows in a single domain
|
|
106
|
+
- `letterboxd-review-draft` -- drafts a Letterboxd-style review for your most recent unrated film
|
|
107
|
+
- `training-report` -- coach-style running report for the last 7-14 days
|
|
108
|
+
- `film-diet` -- portrait of your film-watching taste (genre mix, decades, directors)
|
|
109
|
+
|
|
110
|
+
## Authentication
|
|
111
|
+
|
|
112
|
+
**Desktop apps** use the `REWIND_API_KEY` environment variable passed directly to the server process. The server authenticates with the Rewind API using this key as a Bearer token.
|
|
113
|
+
|
|
114
|
+
**Mobile and web clients** connect to the remote server at `mcp.rewind.rest`, which uses OAuth 2.1 with GitHub as the identity provider. The flow:
|
|
115
|
+
|
|
116
|
+
1. Client discovers endpoints via `/.well-known/oauth-authorization-server`
|
|
117
|
+
2. Client dynamically registers via `/register`
|
|
118
|
+
3. User authenticates with GitHub and approves access
|
|
119
|
+
4. Client receives scoped tokens (1h access, 90d refresh, PKCE S256)
|
|
120
|
+
|
|
121
|
+
The server has **read-only access** to your data. No write or admin operations are exposed.
|
|
122
|
+
|
|
123
|
+
## Privacy & Support
|
|
124
|
+
|
|
125
|
+
- [Privacy Policy](https://rewind.rest/privacy)
|
|
126
|
+
- [Documentation](https://docs.rewind.rest/mcp-server)
|
|
127
|
+
- [Report an issue](https://github.com/pdugan20/rewind/issues)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { text, resourceLink, imageBlock, withRichResponse, withErrorHandling, } from '../tools/helpers.js';
|
|
3
|
+
import { RewindClient } from '../client.js';
|
|
4
|
+
describe('helpers', () => {
|
|
5
|
+
describe('text()', () => {
|
|
6
|
+
it('wraps a string in a text content block', () => {
|
|
7
|
+
expect(text('hello')).toEqual({ type: 'text', text: 'hello' });
|
|
8
|
+
});
|
|
9
|
+
it('accepts an empty string', () => {
|
|
10
|
+
expect(text('')).toEqual({ type: 'text', text: '' });
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
describe('resourceLink()', () => {
|
|
14
|
+
it('builds a resource_link block for a valid URI', () => {
|
|
15
|
+
const block = resourceLink('https://letterboxd.com/u/film/', 'Letterboxd review');
|
|
16
|
+
expect(block).toEqual({
|
|
17
|
+
type: 'resource_link',
|
|
18
|
+
uri: 'https://letterboxd.com/u/film/',
|
|
19
|
+
name: 'Letterboxd review',
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
it('includes optional mimeType and description', () => {
|
|
23
|
+
const block = resourceLink('https://example.com', 'Example', {
|
|
24
|
+
mimeType: 'text/html',
|
|
25
|
+
description: 'An example site',
|
|
26
|
+
});
|
|
27
|
+
expect(block).toMatchObject({
|
|
28
|
+
type: 'resource_link',
|
|
29
|
+
uri: 'https://example.com',
|
|
30
|
+
name: 'Example',
|
|
31
|
+
mimeType: 'text/html',
|
|
32
|
+
description: 'An example site',
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
it('returns null when uri is null/undefined/empty', () => {
|
|
36
|
+
expect(resourceLink(null, 'x')).toBeNull();
|
|
37
|
+
expect(resourceLink(undefined, 'x')).toBeNull();
|
|
38
|
+
expect(resourceLink('', 'x')).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
describe('imageBlock()', () => {
|
|
42
|
+
const bytes = new Uint8Array([0xff, 0xd8, 0xff, 0xe0]); // JPEG magic
|
|
43
|
+
it('base64-encodes bytes fetched from image.cdn_url', async () => {
|
|
44
|
+
const client = new RewindClient('https://api.test', 'rw_test');
|
|
45
|
+
const spy = vi.spyOn(client, 'getBinaryFromUrl').mockResolvedValue({
|
|
46
|
+
bytes,
|
|
47
|
+
mimeType: 'image/jpeg',
|
|
48
|
+
});
|
|
49
|
+
const block = await imageBlock(client, {
|
|
50
|
+
cdn_url: 'https://cdn.rewind.rest/watching/movies/1/original.jpg',
|
|
51
|
+
});
|
|
52
|
+
expect(block).not.toBeNull();
|
|
53
|
+
expect(block?.type).toBe('image');
|
|
54
|
+
expect(block?.mimeType).toBe('image/jpeg');
|
|
55
|
+
expect(block?.data).toBe('/9j/4A==');
|
|
56
|
+
expect(spy).toHaveBeenCalledWith('https://cdn.rewind.rest/watching/movies/1/original.jpg');
|
|
57
|
+
});
|
|
58
|
+
it('falls back to image.url when cdn_url is absent', async () => {
|
|
59
|
+
const client = new RewindClient('https://api.test', 'rw_test');
|
|
60
|
+
const spy = vi.spyOn(client, 'getBinaryFromUrl').mockResolvedValue({
|
|
61
|
+
bytes,
|
|
62
|
+
mimeType: 'image/jpeg',
|
|
63
|
+
});
|
|
64
|
+
await imageBlock(client, { url: 'https://cdn.example/x.jpg' });
|
|
65
|
+
expect(spy).toHaveBeenCalledWith('https://cdn.example/x.jpg');
|
|
66
|
+
});
|
|
67
|
+
it('returns null when image is null or undefined', async () => {
|
|
68
|
+
const client = new RewindClient('https://api.test', 'rw_test');
|
|
69
|
+
expect(await imageBlock(client, null)).toBeNull();
|
|
70
|
+
expect(await imageBlock(client, undefined)).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
it('returns null when image has no cdn_url or url', async () => {
|
|
73
|
+
const client = new RewindClient('https://api.test', 'rw_test');
|
|
74
|
+
expect(await imageBlock(client, { thumbhash: 'x' })).toBeNull();
|
|
75
|
+
});
|
|
76
|
+
it('returns null on fetch failure instead of throwing', async () => {
|
|
77
|
+
const client = new RewindClient('https://api.test', 'rw_test');
|
|
78
|
+
vi.spyOn(client, 'getBinaryFromUrl').mockRejectedValue(new Error('network down'));
|
|
79
|
+
const block = await imageBlock(client, {
|
|
80
|
+
cdn_url: 'https://cdn.example/x.jpg',
|
|
81
|
+
});
|
|
82
|
+
expect(block).toBeNull();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
describe('withRichResponse()', () => {
|
|
86
|
+
it('returns the handler result on success', async () => {
|
|
87
|
+
const result = await withRichResponse(async () => ({
|
|
88
|
+
content: [text('ok')],
|
|
89
|
+
structuredContent: { value: 42 },
|
|
90
|
+
}));
|
|
91
|
+
expect(result).toEqual({
|
|
92
|
+
content: [{ type: 'text', text: 'ok' }],
|
|
93
|
+
structuredContent: { value: 42 },
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
it('catches exceptions and returns isError=true', async () => {
|
|
97
|
+
const result = await withRichResponse(async () => {
|
|
98
|
+
throw new Error('boom');
|
|
99
|
+
});
|
|
100
|
+
expect(result.isError).toBe(true);
|
|
101
|
+
expect(result.content).toHaveLength(1);
|
|
102
|
+
expect(result.content[0]).toEqual({
|
|
103
|
+
type: 'text',
|
|
104
|
+
text: 'Error: boom',
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
it('handles non-Error throwables', async () => {
|
|
108
|
+
const result = await withRichResponse(async () => {
|
|
109
|
+
throw 'string error';
|
|
110
|
+
});
|
|
111
|
+
expect(result.isError).toBe(true);
|
|
112
|
+
expect(result.content[0].text).toBe('Error: string error');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
describe('withErrorHandling() (legacy text-only wrapper)', () => {
|
|
116
|
+
it('still works for text-only tool handlers', async () => {
|
|
117
|
+
const result = await withErrorHandling(async () => 'plain text');
|
|
118
|
+
expect(result).toEqual({
|
|
119
|
+
content: [{ type: 'text', text: 'plain text' }],
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
it('catches and reports errors', async () => {
|
|
123
|
+
const result = await withErrorHandling(async () => {
|
|
124
|
+
throw new Error('nope');
|
|
125
|
+
});
|
|
126
|
+
expect(result.isError).toBe(true);
|
|
127
|
+
expect(result.content[0].text).toBe('Error: nope');
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
//# sourceMappingURL=helpers.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.test.js","sourceRoot":"","sources":["../../src/__tests__/helpers.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live diagnostic: does imageBlock() actually produce a valid image
|
|
3
|
+
* content block when run against the real Rewind CDN? No mocks.
|
|
4
|
+
*
|
|
5
|
+
* Run: npx tsx src/__tests__/image-block.live.ts
|
|
6
|
+
*/
|
|
7
|
+
import { RewindClient } from '../client.js';
|
|
8
|
+
import { imageBlock } from '../tools/helpers.js';
|
|
9
|
+
const FIXTURE = {
|
|
10
|
+
movieId: 710,
|
|
11
|
+
title: "I'm Chevy Chase and You're Not",
|
|
12
|
+
cdn_url: 'https://cdn.rewind.rest/watching/movies/710/original.jpg?width=300&height=300&fit=cover&format=auto&quality=85&v=1',
|
|
13
|
+
};
|
|
14
|
+
async function main() {
|
|
15
|
+
// Base URL + api key are irrelevant here -- imageBlock goes through
|
|
16
|
+
// getBinaryFromUrl which hits the full CDN URL without auth.
|
|
17
|
+
const client = new RewindClient('https://api.rewind.rest', 'rw_unused');
|
|
18
|
+
console.log(`Testing imageBlock against live CDN...`);
|
|
19
|
+
console.log(`Fixture: ${FIXTURE.title} (movie ${FIXTURE.movieId})`);
|
|
20
|
+
console.log(`URL: ${FIXTURE.cdn_url}\n`);
|
|
21
|
+
const start = Date.now();
|
|
22
|
+
const block = await imageBlock(client, { cdn_url: FIXTURE.cdn_url });
|
|
23
|
+
const ms = Date.now() - start;
|
|
24
|
+
if (!block) {
|
|
25
|
+
console.error(`FAIL: imageBlock returned null (fetched in ${ms}ms)`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
console.log(`SUCCESS (${ms}ms)`);
|
|
29
|
+
console.log(`- type: ${block.type}`);
|
|
30
|
+
console.log(`- mimeType: ${block.mimeType}`);
|
|
31
|
+
console.log(`- data size: ${block.data.length} base64 chars`);
|
|
32
|
+
console.log(`- sniff: ${block.data.slice(0, 24)}...`);
|
|
33
|
+
// JPEG base64 starts with /9j/
|
|
34
|
+
const looksLikeJpeg = block.data.startsWith('/9j/');
|
|
35
|
+
console.log(`- JPEG? ${looksLikeJpeg ? 'yes (starts with /9j/)' : 'NO -- unexpected'}`);
|
|
36
|
+
}
|
|
37
|
+
main().catch((err) => {
|
|
38
|
+
console.error('Unexpected error:', err);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
});
|
|
41
|
+
//# sourceMappingURL=image-block.live.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-block.live.js","sourceRoot":"","sources":["../../src/__tests__/image-block.live.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEjD,MAAM,OAAO,GAAG;IACd,OAAO,EAAE,GAAG;IACZ,KAAK,EAAE,gCAAgC;IACvC,OAAO,EACL,oHAAoH;CACvH,CAAC;AAEF,KAAK,UAAU,IAAI;IACjB,oEAAoE;IACpE,6DAA6D;IAC7D,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC,yBAAyB,EAAE,WAAW,CAAC,CAAC;IAExE,OAAO,CAAC,GAAG,CAAC,wCAAwC,CAAC,CAAC;IACtD,OAAO,CAAC,GAAG,CAAC,YAAY,OAAO,CAAC,KAAK,WAAW,OAAO,CAAC,OAAO,GAAG,CAAC,CAAC;IACpE,OAAO,CAAC,GAAG,CAAC,QAAQ,OAAO,CAAC,OAAO,IAAI,CAAC,CAAC;IAEzC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACzB,MAAM,KAAK,GAAG,MAAM,UAAU,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;IACrE,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC;IAE9B,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,CAAC,KAAK,CAAC,8CAA8C,EAAE,KAAK,CAAC,CAAC;QACrE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC;IACjC,OAAO,CAAC,GAAG,CAAC,gBAAgB,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;IAC1C,OAAO,CAAC,GAAG,CAAC,gBAAgB,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC9C,OAAO,CAAC,GAAG,CAAC,gBAAgB,KAAK,CAAC,IAAI,CAAC,MAAM,eAAe,CAAC,CAAC;IAC9D,OAAO,CAAC,GAAG,CAAC,gBAAgB,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC;IAC1D,+BAA+B;IAC/B,MAAM,aAAa,GAAG,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;IACpD,OAAO,CAAC,GAAG,CACT,gBAAgB,aAAa,CAAC,CAAC,CAAC,wBAAwB,CAAC,CAAC,CAAC,kBAAkB,EAAE,CAChF,CAAC;AACJ,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,mBAAmB,EAAE,GAAG,CAAC,CAAC;IACxC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|