rewind-mcp-server 0.1.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 +140 -0
- package/dist/client.d.ts +21 -0
- package/dist/client.js +62 -0
- package/dist/client.js.map +1 -0
- package/dist/consent.d.ts +10 -0
- package/dist/consent.js +148 -0
- package/dist/consent.js.map +1 -0
- package/dist/github-auth.d.ts +21 -0
- package/dist/github-auth.js +60 -0
- package/dist/github-auth.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts.d.ts +2 -0
- package/dist/prompts.js +93 -0
- package/dist/prompts.js.map +1 -0
- package/dist/resources.d.ts +3 -0
- package/dist/resources.js +80 -0
- package/dist/resources.js.map +1 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.js +59 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/collecting.d.ts +3 -0
- package/dist/tools/collecting.js +92 -0
- package/dist/tools/collecting.js.map +1 -0
- package/dist/tools/cross-domain.d.ts +3 -0
- package/dist/tools/cross-domain.js +114 -0
- package/dist/tools/cross-domain.js.map +1 -0
- package/dist/tools/helpers.d.ts +32 -0
- package/dist/tools/helpers.js +69 -0
- package/dist/tools/helpers.js.map +1 -0
- package/dist/tools/listening.d.ts +3 -0
- package/dist/tools/listening.js +176 -0
- package/dist/tools/listening.js.map +1 -0
- package/dist/tools/reading.d.ts +3 -0
- package/dist/tools/reading.js +85 -0
- package/dist/tools/reading.js.map +1 -0
- package/dist/tools/running.d.ts +3 -0
- package/dist/tools/running.js +135 -0
- package/dist/tools/running.js.map +1 -0
- package/dist/tools/watching.d.ts +3 -0
- package/dist/tools/watching.js +138 -0
- package/dist/tools/watching.js.map +1 -0
- package/dist/worker.d.ts +20 -0
- package/dist/worker.js +173 -0
- package/dist/worker.js.map +1 -0
- package/package.json +59 -0
package/README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# rewind-mcp-server
|
|
2
|
+
|
|
3
|
+
MCP (Model Context Protocol) server for the [Rewind](https://rewind.rest) personal data API. Gives Claude natural language access to your listening, running, watching, collecting, and reading data.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **26 tools** across 5 domains (listening, running, watching, collecting, reading) + cross-domain search and feed
|
|
8
|
+
- **4 resources** for sync status and year-in-review data
|
|
9
|
+
- **3 prompt templates** for weekly summaries, year-in-review, and period comparisons
|
|
10
|
+
- Works with Claude Desktop, Claude Code, Claude iOS, and claude.ai
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
### Claude Desktop (Mac)
|
|
15
|
+
|
|
16
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"mcpServers": {
|
|
21
|
+
"rewind": {
|
|
22
|
+
"command": "npx",
|
|
23
|
+
"args": ["-y", "rewind-mcp-server"],
|
|
24
|
+
"env": {
|
|
25
|
+
"REWIND_API_KEY": "rw_live_your_key_here"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Restart Claude Desktop after saving.
|
|
33
|
+
|
|
34
|
+
### Claude Code
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
claude mcp add rewind -- npx -y rewind-mcp-server
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Or add `.mcp.json` to your project root:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"mcpServers": {
|
|
45
|
+
"rewind": {
|
|
46
|
+
"command": "npx",
|
|
47
|
+
"args": ["-y", "rewind-mcp-server"],
|
|
48
|
+
"env": {
|
|
49
|
+
"REWIND_API_KEY": "rw_live_your_key_here"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Claude iOS / claude.ai (Web)
|
|
57
|
+
|
|
58
|
+
These platforms connect to the hosted remote server. Go to Settings > Integrations and add:
|
|
59
|
+
|
|
60
|
+
- **URL:** `https://mcp.rewind.rest/mcp`
|
|
61
|
+
- **Authorization:** `Bearer rw_live_your_key_here`
|
|
62
|
+
|
|
63
|
+
## Environment Variables
|
|
64
|
+
|
|
65
|
+
| Variable | Required | Default | Description |
|
|
66
|
+
| ---------------- | -------- | ------------------------- | ----------------------------------- |
|
|
67
|
+
| `REWIND_API_KEY` | Yes | -- | Your Rewind API key (`rw_live_...`) |
|
|
68
|
+
| `REWIND_API_URL` | No | `https://api.rewind.rest` | API base URL |
|
|
69
|
+
|
|
70
|
+
## Available Tools
|
|
71
|
+
|
|
72
|
+
### Listening (Last.fm)
|
|
73
|
+
|
|
74
|
+
| Tool | Description |
|
|
75
|
+
| ----------------------- | ---------------------------- |
|
|
76
|
+
| `get_now_playing` | Currently playing track |
|
|
77
|
+
| `get_recent_listens` | Recent scrobbles |
|
|
78
|
+
| `get_listening_stats` | Overall listening statistics |
|
|
79
|
+
| `get_top_artists` | Top artists by period |
|
|
80
|
+
| `get_top_albums` | Top albums by period |
|
|
81
|
+
| `get_top_tracks` | Top tracks by period |
|
|
82
|
+
| `get_listening_streaks` | Current and longest streaks |
|
|
83
|
+
| `get_artist_details` | Artist info by ID |
|
|
84
|
+
| `get_album_details` | Album info by ID |
|
|
85
|
+
|
|
86
|
+
### Running (Strava)
|
|
87
|
+
|
|
88
|
+
| Tool | Description |
|
|
89
|
+
| ---------------------- | --------------------------- |
|
|
90
|
+
| `get_running_stats` | Overall running statistics |
|
|
91
|
+
| `get_recent_runs` | Recent activities |
|
|
92
|
+
| `get_personal_records` | PRs at standard distances |
|
|
93
|
+
| `get_running_streaks` | Current and longest streaks |
|
|
94
|
+
| `get_activity_details` | Single run details by ID |
|
|
95
|
+
|
|
96
|
+
### Watching (Plex / Letterboxd)
|
|
97
|
+
|
|
98
|
+
| Tool | Description |
|
|
99
|
+
| -------------------- | --------------------------- |
|
|
100
|
+
| `get_recent_watches` | Recently watched movies/TV |
|
|
101
|
+
| `get_movie_details` | Movie info by ID |
|
|
102
|
+
| `get_watching_stats` | Overall watching statistics |
|
|
103
|
+
|
|
104
|
+
### Collecting (Discogs)
|
|
105
|
+
|
|
106
|
+
| Tool | Description |
|
|
107
|
+
| ---------------------- | --------------------- |
|
|
108
|
+
| `get_vinyl_collection` | Browse vinyl records |
|
|
109
|
+
| `get_collecting_stats` | Collection statistics |
|
|
110
|
+
|
|
111
|
+
### Reading (Instapaper)
|
|
112
|
+
|
|
113
|
+
| Tool | Description |
|
|
114
|
+
| ------------------------ | ----------------------- |
|
|
115
|
+
| `get_recent_reads` | Recently saved articles |
|
|
116
|
+
| `get_reading_highlights` | Saved highlights |
|
|
117
|
+
| `get_random_highlight` | Random highlight |
|
|
118
|
+
| `get_reading_stats` | Reading statistics |
|
|
119
|
+
|
|
120
|
+
### Cross-Domain
|
|
121
|
+
|
|
122
|
+
| Tool | Description |
|
|
123
|
+
| ----------------- | ----------------------------------- |
|
|
124
|
+
| `search` | Full-text search across all domains |
|
|
125
|
+
| `get_feed` | Unified activity feed |
|
|
126
|
+
| `get_on_this_day` | Historical "on this day" items |
|
|
127
|
+
| `get_health` | API and sync health status |
|
|
128
|
+
|
|
129
|
+
## Development
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
cd mcp-server
|
|
133
|
+
npm install
|
|
134
|
+
npm run build
|
|
135
|
+
REWIND_API_KEY=rw_live_... npm start
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
MIT
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client for the Rewind API (api.rewind.rest).
|
|
3
|
+
* All logging goes to stderr -- stdout is reserved for MCP stdio transport.
|
|
4
|
+
*/
|
|
5
|
+
export declare class RewindClient {
|
|
6
|
+
private baseUrl;
|
|
7
|
+
private apiKey;
|
|
8
|
+
constructor(baseUrl: string, apiKey: string);
|
|
9
|
+
get<T>(path: string, params?: Record<string, string | number | undefined>): Promise<T>;
|
|
10
|
+
}
|
|
11
|
+
export declare class RewindApiError extends Error {
|
|
12
|
+
status: number;
|
|
13
|
+
statusText: string;
|
|
14
|
+
body: string;
|
|
15
|
+
constructor(status: number, statusText: string, body: string);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Create a RewindClient from environment variables.
|
|
19
|
+
* Throws if required vars are missing.
|
|
20
|
+
*/
|
|
21
|
+
export declare function createClientFromEnv(): RewindClient;
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client for the Rewind API (api.rewind.rest).
|
|
3
|
+
* All logging goes to stderr -- stdout is reserved for MCP stdio transport.
|
|
4
|
+
*/
|
|
5
|
+
const log = (...args) => console.error('[rewind-mcp]', ...args);
|
|
6
|
+
export class RewindClient {
|
|
7
|
+
baseUrl;
|
|
8
|
+
apiKey;
|
|
9
|
+
constructor(baseUrl, apiKey) {
|
|
10
|
+
// Strip trailing slash
|
|
11
|
+
this.baseUrl = baseUrl.replace(/\/+$/, '');
|
|
12
|
+
this.apiKey = apiKey;
|
|
13
|
+
}
|
|
14
|
+
async get(path, params) {
|
|
15
|
+
const url = new URL(`/v1${path}`, this.baseUrl);
|
|
16
|
+
if (params) {
|
|
17
|
+
for (const [key, value] of Object.entries(params)) {
|
|
18
|
+
if (value !== undefined && value !== null) {
|
|
19
|
+
url.searchParams.set(key, String(value));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
log(`GET ${url.pathname}${url.search}`);
|
|
24
|
+
const res = await fetch(url.toString(), {
|
|
25
|
+
headers: {
|
|
26
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
27
|
+
Accept: 'application/json',
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
if (!res.ok) {
|
|
31
|
+
const body = await res.text().catch(() => '');
|
|
32
|
+
throw new RewindApiError(res.status, res.statusText, body);
|
|
33
|
+
}
|
|
34
|
+
return res.json();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export class RewindApiError extends Error {
|
|
38
|
+
status;
|
|
39
|
+
statusText;
|
|
40
|
+
body;
|
|
41
|
+
constructor(status, statusText, body) {
|
|
42
|
+
super(`Rewind API error: ${status} ${statusText}`);
|
|
43
|
+
this.status = status;
|
|
44
|
+
this.statusText = statusText;
|
|
45
|
+
this.body = body;
|
|
46
|
+
this.name = 'RewindApiError';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Create a RewindClient from environment variables.
|
|
51
|
+
* Throws if required vars are missing.
|
|
52
|
+
*/
|
|
53
|
+
export function createClientFromEnv() {
|
|
54
|
+
const apiKey = process.env.REWIND_API_KEY;
|
|
55
|
+
const apiUrl = process.env.REWIND_API_URL ?? 'https://api.rewind.rest';
|
|
56
|
+
if (!apiKey) {
|
|
57
|
+
throw new Error('REWIND_API_KEY environment variable is required. ' +
|
|
58
|
+
'Set it to your Rewind API key (rw_live_... or rw_admin_...).');
|
|
59
|
+
}
|
|
60
|
+
return new RewindClient(apiUrl, apiKey);
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,GAAG,GAAG,CAAC,GAAG,IAAe,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,GAAG,IAAI,CAAC,CAAC;AAE3E,MAAM,OAAO,YAAY;IACf,OAAO,CAAS;IAChB,MAAM,CAAS;IAEvB,YAAY,OAAe,EAAE,MAAc;QACzC,uBAAuB;QACvB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAC3C,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IAED,KAAK,CAAC,GAAG,CACP,IAAY,EACZ,MAAoD;QAEpD,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,IAAI,EAAE,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;QAChD,IAAI,MAAM,EAAE,CAAC;YACX,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;gBAClD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;oBAC1C,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;gBAC3C,CAAC;YACH,CAAC;QACH,CAAC;QAED,GAAG,CAAC,OAAO,GAAG,CAAC,QAAQ,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;QAExC,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE;YACtC,OAAO,EAAE;gBACP,aAAa,EAAE,UAAU,IAAI,CAAC,MAAM,EAAE;gBACtC,MAAM,EAAE,kBAAkB;aAC3B;SACF,CAAC,CAAC;QAEH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;YAC9C,MAAM,IAAI,cAAc,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;QAC7D,CAAC;QAED,OAAO,GAAG,CAAC,IAAI,EAAgB,CAAC;IAClC,CAAC;CACF;AAED,MAAM,OAAO,cAAe,SAAQ,KAAK;IAE9B;IACA;IACA;IAHT,YACS,MAAc,EACd,UAAkB,EAClB,IAAY;QAEnB,KAAK,CAAC,qBAAqB,MAAM,IAAI,UAAU,EAAE,CAAC,CAAC;QAJ5C,WAAM,GAAN,MAAM,CAAQ;QACd,eAAU,GAAV,UAAU,CAAQ;QAClB,SAAI,GAAJ,IAAI,CAAQ;QAGnB,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;IAC/B,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB;IACjC,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;IAC1C,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,yBAAyB,CAAC;IAEvE,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CACb,mDAAmD;YACjD,8DAA8D,CACjE,CAAC;IACJ,CAAC;IAED,OAAO,IAAI,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAC1C,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consent page HTML for the OAuth authorization flow.
|
|
3
|
+
* Shows "Sign in with GitHub" and the requested scopes.
|
|
4
|
+
*/
|
|
5
|
+
export declare function renderConsentPage(options: {
|
|
6
|
+
clientName: string;
|
|
7
|
+
scopes: string[];
|
|
8
|
+
gitHubAuthorizeUrl: string;
|
|
9
|
+
error?: string;
|
|
10
|
+
}): string;
|
package/dist/consent.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consent page HTML for the OAuth authorization flow.
|
|
3
|
+
* Shows "Sign in with GitHub" and the requested scopes.
|
|
4
|
+
*/
|
|
5
|
+
export function renderConsentPage(options) {
|
|
6
|
+
const scopeList = options.scopes.length
|
|
7
|
+
? options.scopes
|
|
8
|
+
.map((s) => `<li>${escapeHtml(formatScope(s))}</li>`)
|
|
9
|
+
.join('')
|
|
10
|
+
: '<li>Read access to all domains</li>';
|
|
11
|
+
const errorHtml = options.error
|
|
12
|
+
? `<div class="error">${escapeHtml(options.error)}</div>`
|
|
13
|
+
: '';
|
|
14
|
+
return `<!DOCTYPE html>
|
|
15
|
+
<html lang="en">
|
|
16
|
+
<head>
|
|
17
|
+
<meta charset="UTF-8">
|
|
18
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
19
|
+
<title>Connect to Rewind</title>
|
|
20
|
+
<style>
|
|
21
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
22
|
+
body {
|
|
23
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
24
|
+
background: #0a0a0a;
|
|
25
|
+
color: #e5e5e5;
|
|
26
|
+
display: flex;
|
|
27
|
+
justify-content: center;
|
|
28
|
+
align-items: center;
|
|
29
|
+
min-height: 100vh;
|
|
30
|
+
padding: 20px;
|
|
31
|
+
}
|
|
32
|
+
.card {
|
|
33
|
+
background: #1a1a1a;
|
|
34
|
+
border: 1px solid #2a2a2a;
|
|
35
|
+
border-radius: 12px;
|
|
36
|
+
padding: 40px;
|
|
37
|
+
max-width: 420px;
|
|
38
|
+
width: 100%;
|
|
39
|
+
}
|
|
40
|
+
.logo {
|
|
41
|
+
font-size: 24px;
|
|
42
|
+
font-weight: 700;
|
|
43
|
+
color: #6874e8;
|
|
44
|
+
margin-bottom: 8px;
|
|
45
|
+
}
|
|
46
|
+
.subtitle {
|
|
47
|
+
color: #888;
|
|
48
|
+
font-size: 14px;
|
|
49
|
+
margin-bottom: 24px;
|
|
50
|
+
}
|
|
51
|
+
.client-name {
|
|
52
|
+
font-weight: 600;
|
|
53
|
+
color: #e5e5e5;
|
|
54
|
+
}
|
|
55
|
+
h2 {
|
|
56
|
+
font-size: 16px;
|
|
57
|
+
font-weight: 600;
|
|
58
|
+
margin-bottom: 12px;
|
|
59
|
+
}
|
|
60
|
+
ul {
|
|
61
|
+
list-style: none;
|
|
62
|
+
margin-bottom: 24px;
|
|
63
|
+
}
|
|
64
|
+
li {
|
|
65
|
+
padding: 8px 0;
|
|
66
|
+
border-bottom: 1px solid #222;
|
|
67
|
+
font-size: 14px;
|
|
68
|
+
color: #ccc;
|
|
69
|
+
}
|
|
70
|
+
li:before {
|
|
71
|
+
content: "\\2713";
|
|
72
|
+
color: #6874e8;
|
|
73
|
+
margin-right: 8px;
|
|
74
|
+
}
|
|
75
|
+
.github-btn {
|
|
76
|
+
display: block;
|
|
77
|
+
width: 100%;
|
|
78
|
+
padding: 12px;
|
|
79
|
+
background: #6874e8;
|
|
80
|
+
color: white;
|
|
81
|
+
border: none;
|
|
82
|
+
border-radius: 8px;
|
|
83
|
+
font-size: 16px;
|
|
84
|
+
font-weight: 600;
|
|
85
|
+
cursor: pointer;
|
|
86
|
+
text-decoration: none;
|
|
87
|
+
text-align: center;
|
|
88
|
+
transition: background 0.2s;
|
|
89
|
+
}
|
|
90
|
+
.github-btn:hover { background: #5563d6; }
|
|
91
|
+
.footer {
|
|
92
|
+
margin-top: 16px;
|
|
93
|
+
font-size: 12px;
|
|
94
|
+
color: #666;
|
|
95
|
+
text-align: center;
|
|
96
|
+
}
|
|
97
|
+
.footer a { color: #6874e8; text-decoration: none; }
|
|
98
|
+
.error {
|
|
99
|
+
background: #2d1515;
|
|
100
|
+
border: 1px solid #5c2020;
|
|
101
|
+
color: #f87171;
|
|
102
|
+
padding: 12px;
|
|
103
|
+
border-radius: 8px;
|
|
104
|
+
font-size: 14px;
|
|
105
|
+
margin-bottom: 16px;
|
|
106
|
+
}
|
|
107
|
+
</style>
|
|
108
|
+
</head>
|
|
109
|
+
<body>
|
|
110
|
+
<div class="card">
|
|
111
|
+
<div class="logo">Rewind</div>
|
|
112
|
+
<p class="subtitle">
|
|
113
|
+
<span class="client-name">${escapeHtml(options.clientName)}</span> wants to access your Rewind data
|
|
114
|
+
</p>
|
|
115
|
+
${errorHtml}
|
|
116
|
+
<h2>This will allow access to:</h2>
|
|
117
|
+
<ul>${scopeList}</ul>
|
|
118
|
+
<a href="${escapeHtml(options.gitHubAuthorizeUrl)}" class="github-btn">
|
|
119
|
+
Sign in with GitHub
|
|
120
|
+
</a>
|
|
121
|
+
<p class="footer">
|
|
122
|
+
<a href="https://rewind.rest/privacy">Privacy Policy</a>
|
|
123
|
+
</p>
|
|
124
|
+
</div>
|
|
125
|
+
</body>
|
|
126
|
+
</html>`;
|
|
127
|
+
}
|
|
128
|
+
function formatScope(scope) {
|
|
129
|
+
const labels = {
|
|
130
|
+
read: 'Read access to all domains',
|
|
131
|
+
'read:listening': 'Listening history and stats (Last.fm)',
|
|
132
|
+
'read:running': 'Running activities and stats (Strava)',
|
|
133
|
+
'read:watching': 'Watch history and stats (Plex, Letterboxd)',
|
|
134
|
+
'read:collecting': 'Collection data (Discogs)',
|
|
135
|
+
'read:reading': 'Reading data and highlights (Instapaper)',
|
|
136
|
+
'read:feed': 'Cross-domain feed and search',
|
|
137
|
+
};
|
|
138
|
+
return labels[scope] ?? scope;
|
|
139
|
+
}
|
|
140
|
+
function escapeHtml(str) {
|
|
141
|
+
return str
|
|
142
|
+
.replace(/&/g, '&')
|
|
143
|
+
.replace(/</g, '<')
|
|
144
|
+
.replace(/>/g, '>')
|
|
145
|
+
.replace(/"/g, '"')
|
|
146
|
+
.replace(/'/g, ''');
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=consent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consent.js","sourceRoot":"","sources":["../src/consent.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,UAAU,iBAAiB,CAAC,OAKjC;IACC,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM;QACrC,CAAC,CAAC,OAAO,CAAC,MAAM;aACX,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;aACpD,IAAI,CAAC,EAAE,CAAC;QACb,CAAC,CAAC,qCAAqC,CAAC;IAE1C,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK;QAC7B,CAAC,CAAC,sBAAsB,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ;QACzD,CAAC,CAAC,EAAE,CAAC;IAEP,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kCAmGyB,UAAU,CAAC,OAAO,CAAC,UAAU,CAAC;;MAE1D,SAAS;;UAEL,SAAS;eACJ,UAAU,CAAC,OAAO,CAAC,kBAAkB,CAAC;;;;;;;;QAQ7C,CAAC;AACT,CAAC;AAED,SAAS,WAAW,CAAC,KAAa;IAChC,MAAM,MAAM,GAA2B;QACrC,IAAI,EAAE,4BAA4B;QAClC,gBAAgB,EAAE,uCAAuC;QACzD,cAAc,EAAE,uCAAuC;QACvD,eAAe,EAAE,4CAA4C;QAC7D,iBAAiB,EAAE,2BAA2B;QAC9C,cAAc,EAAE,0CAA0C;QAC1D,WAAW,EAAE,8BAA8B;KAC5C,CAAC;IACF,OAAO,MAAM,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC;AAChC,CAAC;AAED,SAAS,UAAU,CAAC,GAAW;IAC7B,OAAO,GAAG;SACP,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;AAC5B,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub OAuth helpers for upstream authentication delegation.
|
|
3
|
+
* The MCP server uses GitHub as the identity provider -- users authenticate
|
|
4
|
+
* with GitHub, and we map their GitHub user ID to a Rewind user_id.
|
|
5
|
+
*/
|
|
6
|
+
export interface GitHubUser {
|
|
7
|
+
id: number;
|
|
8
|
+
login: string;
|
|
9
|
+
name: string | null;
|
|
10
|
+
avatar_url: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Build the GitHub OAuth authorization URL.
|
|
14
|
+
* Redirects the user to GitHub to authenticate.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getGitHubAuthorizeUrl(clientId: string, redirectUri: string, state: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Exchange a GitHub authorization code for an access token,
|
|
19
|
+
* then fetch the authenticated user's profile.
|
|
20
|
+
*/
|
|
21
|
+
export declare function exchangeGitHubCode(code: string, clientId: string, clientSecret: string): Promise<GitHubUser>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub OAuth helpers for upstream authentication delegation.
|
|
3
|
+
* The MCP server uses GitHub as the identity provider -- users authenticate
|
|
4
|
+
* with GitHub, and we map their GitHub user ID to a Rewind user_id.
|
|
5
|
+
*/
|
|
6
|
+
const GITHUB_AUTHORIZE_URL = 'https://github.com/login/oauth/authorize';
|
|
7
|
+
const GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token';
|
|
8
|
+
const GITHUB_USER_URL = 'https://api.github.com/user';
|
|
9
|
+
/**
|
|
10
|
+
* Build the GitHub OAuth authorization URL.
|
|
11
|
+
* Redirects the user to GitHub to authenticate.
|
|
12
|
+
*/
|
|
13
|
+
export function getGitHubAuthorizeUrl(clientId, redirectUri, state) {
|
|
14
|
+
const params = new URLSearchParams({
|
|
15
|
+
client_id: clientId,
|
|
16
|
+
redirect_uri: redirectUri,
|
|
17
|
+
scope: 'read:user',
|
|
18
|
+
state,
|
|
19
|
+
});
|
|
20
|
+
return `${GITHUB_AUTHORIZE_URL}?${params.toString()}`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Exchange a GitHub authorization code for an access token,
|
|
24
|
+
* then fetch the authenticated user's profile.
|
|
25
|
+
*/
|
|
26
|
+
export async function exchangeGitHubCode(code, clientId, clientSecret) {
|
|
27
|
+
// Exchange code for token
|
|
28
|
+
const tokenRes = await fetch(GITHUB_TOKEN_URL, {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: {
|
|
31
|
+
'Content-Type': 'application/json',
|
|
32
|
+
Accept: 'application/json',
|
|
33
|
+
},
|
|
34
|
+
body: JSON.stringify({
|
|
35
|
+
client_id: clientId,
|
|
36
|
+
client_secret: clientSecret,
|
|
37
|
+
code,
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
40
|
+
if (!tokenRes.ok) {
|
|
41
|
+
throw new Error(`GitHub token exchange failed: ${tokenRes.status}`);
|
|
42
|
+
}
|
|
43
|
+
const tokenData = (await tokenRes.json());
|
|
44
|
+
if (tokenData.error || !tokenData.access_token) {
|
|
45
|
+
throw new Error(`GitHub OAuth error: ${tokenData.error_description ?? tokenData.error ?? 'no access token'}`);
|
|
46
|
+
}
|
|
47
|
+
// Fetch user profile
|
|
48
|
+
const userRes = await fetch(GITHUB_USER_URL, {
|
|
49
|
+
headers: {
|
|
50
|
+
Authorization: `Bearer ${tokenData.access_token}`,
|
|
51
|
+
Accept: 'application/vnd.github+json',
|
|
52
|
+
'User-Agent': 'rewind-mcp-server',
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
if (!userRes.ok) {
|
|
56
|
+
throw new Error(`GitHub user fetch failed: ${userRes.status}`);
|
|
57
|
+
}
|
|
58
|
+
return (await userRes.json());
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=github-auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github-auth.js","sourceRoot":"","sources":["../src/github-auth.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,oBAAoB,GAAG,0CAA0C,CAAC;AACxE,MAAM,gBAAgB,GAAG,6CAA6C,CAAC;AACvE,MAAM,eAAe,GAAG,6BAA6B,CAAC;AAStD;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CACnC,QAAgB,EAChB,WAAmB,EACnB,KAAa;IAEb,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC;QACjC,SAAS,EAAE,QAAQ;QACnB,YAAY,EAAE,WAAW;QACzB,KAAK,EAAE,WAAW;QAClB,KAAK;KACN,CAAC,CAAC;IACH,OAAO,GAAG,oBAAoB,IAAI,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC;AACxD,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,IAAY,EACZ,QAAgB,EAChB,YAAoB;IAEpB,0BAA0B;IAC1B,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,gBAAgB,EAAE;QAC7C,MAAM,EAAE,MAAM;QACd,OAAO,EAAE;YACP,cAAc,EAAE,kBAAkB;YAClC,MAAM,EAAE,kBAAkB;SAC3B;QACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACnB,SAAS,EAAE,QAAQ;YACnB,aAAa,EAAE,YAAY;YAC3B,IAAI;SACL,CAAC;KACH,CAAC,CAAC;IAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,iCAAiC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IACtE,CAAC;IAED,MAAM,SAAS,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAIvC,CAAC;IAEF,IAAI,SAAS,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,YAAY,EAAE,CAAC;QAC/C,MAAM,IAAI,KAAK,CACb,uBAAuB,SAAS,CAAC,iBAAiB,IAAI,SAAS,CAAC,KAAK,IAAI,iBAAiB,EAAE,CAC7F,CAAC;IACJ,CAAC;IAED,qBAAqB;IACrB,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC,eAAe,EAAE;QAC3C,OAAO,EAAE;YACP,aAAa,EAAE,UAAU,SAAS,CAAC,YAAY,EAAE;YACjD,MAAM,EAAE,6BAA6B;YACrC,YAAY,EAAE,mBAAmB;SAClC;KACF,CAAC,CAAC;IAEH,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC;QAChB,MAAM,IAAI,KAAK,CAAC,6BAA6B,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACjE,CAAC;IAED,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,EAAE,CAAe,CAAC;AAC9C,CAAC"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Stdio entry point for local usage (Claude Desktop, Claude Code).
|
|
4
|
+
* Launched as a child process, communicates over stdin/stdout via JSON-RPC.
|
|
5
|
+
*/
|
|
6
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
|
+
import { createClientFromEnv } from './client.js';
|
|
8
|
+
import { createServer } from './server.js';
|
|
9
|
+
const log = (...args) => console.error('[rewind-mcp]', ...args);
|
|
10
|
+
async function main() {
|
|
11
|
+
const client = createClientFromEnv();
|
|
12
|
+
const server = createServer(client);
|
|
13
|
+
const transport = new StdioServerTransport();
|
|
14
|
+
await server.connect(transport);
|
|
15
|
+
log('Server started on stdio transport');
|
|
16
|
+
}
|
|
17
|
+
main().catch((error) => {
|
|
18
|
+
log('Fatal error:', error);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
});
|
|
21
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;GAGG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,GAAG,GAAG,CAAC,GAAG,IAAe,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,GAAG,IAAI,CAAC,CAAC;AAE3E,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,mBAAmB,EAAE,CAAC;IACrC,MAAM,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;IAEpC,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,GAAG,CAAC,mCAAmC,CAAC,CAAC;AAC3C,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IACrB,GAAG,CAAC,cAAc,EAAE,KAAK,CAAC,CAAC;IAC3B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
package/dist/prompts.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export function registerPrompts(server) {
|
|
3
|
+
// Weekly summary prompt
|
|
4
|
+
server.prompt('weekly-summary', 'Generate a summary of activity across all domains for the past week', {}, () => ({
|
|
5
|
+
messages: [
|
|
6
|
+
{
|
|
7
|
+
role: 'user',
|
|
8
|
+
content: {
|
|
9
|
+
type: 'text',
|
|
10
|
+
text: [
|
|
11
|
+
'Please summarize my activity from the past week across all domains. Use the following tools to gather the data:',
|
|
12
|
+
'',
|
|
13
|
+
"1. get_recent_listens (limit 20) -- what I've been listening to",
|
|
14
|
+
'2. get_recent_runs (limit 10) -- any runs this week',
|
|
15
|
+
"3. get_recent_watches (limit 10) -- movies or TV I've watched",
|
|
16
|
+
"4. get_recent_reads (limit 10) -- articles I've been reading",
|
|
17
|
+
'5. get_feed (limit 30, from: 7 days ago) -- unified activity',
|
|
18
|
+
'',
|
|
19
|
+
'Organize the summary by domain. Highlight patterns, notable items, and any streaks or milestones.',
|
|
20
|
+
'Keep the tone casual and reflective. Use specific names, numbers, and dates from the data.',
|
|
21
|
+
].join('\n'),
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
}));
|
|
26
|
+
// Year in review prompt
|
|
27
|
+
server.prompt('year-in-review', 'Generate a comprehensive year-in-review for a given year across all domains', {
|
|
28
|
+
year: z.string().describe('The year to review (e.g. 2025)'),
|
|
29
|
+
}, ({ year }) => ({
|
|
30
|
+
messages: [
|
|
31
|
+
{
|
|
32
|
+
role: 'user',
|
|
33
|
+
content: {
|
|
34
|
+
type: 'text',
|
|
35
|
+
text: [
|
|
36
|
+
`Generate a year-in-review for ${year}. Gather data from all domains:`,
|
|
37
|
+
'',
|
|
38
|
+
'1. get_listening_stats -- overall listening numbers',
|
|
39
|
+
`2. get_top_artists (period: 12month, limit: 10) -- top artists`,
|
|
40
|
+
`3. get_top_albums (period: 12month, limit: 10) -- top albums`,
|
|
41
|
+
'4. get_running_stats -- running totals',
|
|
42
|
+
'5. get_watching_stats -- movie and TV totals',
|
|
43
|
+
'6. get_collecting_stats -- collection growth',
|
|
44
|
+
'7. get_reading_stats -- reading numbers',
|
|
45
|
+
'',
|
|
46
|
+
`Create a comprehensive but concise year-in-review for ${year}. Include:`,
|
|
47
|
+
'- Key stats and totals per domain',
|
|
48
|
+
'- Standout items (most-played artist, longest run, favorite movie, etc.)',
|
|
49
|
+
'- Interesting patterns or changes from previous years if apparent',
|
|
50
|
+
'- A brief overall reflection',
|
|
51
|
+
'',
|
|
52
|
+
'Format it as a readable narrative, not just a list of numbers.',
|
|
53
|
+
].join('\n'),
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
}));
|
|
58
|
+
// Compare periods prompt
|
|
59
|
+
server.prompt('compare-periods', 'Compare activity between two time periods for a specific domain', {
|
|
60
|
+
domain: z
|
|
61
|
+
.string()
|
|
62
|
+
.describe('Domain to compare (listening, running, watching, collecting, reading)'),
|
|
63
|
+
period1: z
|
|
64
|
+
.string()
|
|
65
|
+
.describe("First period description (e.g. 'January 2025', 'last month')"),
|
|
66
|
+
period2: z
|
|
67
|
+
.string()
|
|
68
|
+
.describe("Second period description (e.g. 'January 2024', 'this month')"),
|
|
69
|
+
}, ({ domain, period1, period2 }) => ({
|
|
70
|
+
messages: [
|
|
71
|
+
{
|
|
72
|
+
role: 'user',
|
|
73
|
+
content: {
|
|
74
|
+
type: 'text',
|
|
75
|
+
text: [
|
|
76
|
+
`Compare my ${domain} activity between ${period1} and ${period2}.`,
|
|
77
|
+
'',
|
|
78
|
+
`Use the appropriate tools for the ${domain} domain to gather data for both periods.`,
|
|
79
|
+
'For each period, get stats, recent activity, and any relevant top lists.',
|
|
80
|
+
'',
|
|
81
|
+
'Present the comparison as:',
|
|
82
|
+
'- Side-by-side key metrics',
|
|
83
|
+
'- Notable differences and trends',
|
|
84
|
+
'- Standout items unique to each period',
|
|
85
|
+
'',
|
|
86
|
+
'Keep the analysis specific and data-driven.',
|
|
87
|
+
].join('\n'),
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=prompts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompts.js","sourceRoot":"","sources":["../src/prompts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,MAAM,UAAU,eAAe,CAAC,MAAiB;IAC/C,wBAAwB;IACxB,MAAM,CAAC,MAAM,CACX,gBAAgB,EAChB,qEAAqE,EACrE,EAAE,EACF,GAAG,EAAE,CAAC,CAAC;QACL,QAAQ,EAAE;YACR;gBACE,IAAI,EAAE,MAAe;gBACrB,OAAO,EAAE;oBACP,IAAI,EAAE,MAAe;oBACrB,IAAI,EAAE;wBACJ,iHAAiH;wBACjH,EAAE;wBACF,iEAAiE;wBACjE,qDAAqD;wBACrD,+DAA+D;wBAC/D,8DAA8D;wBAC9D,8DAA8D;wBAC9D,EAAE;wBACF,mGAAmG;wBACnG,4FAA4F;qBAC7F,CAAC,IAAI,CAAC,IAAI,CAAC;iBACb;aACF;SACF;KACF,CAAC,CACH,CAAC;IAEF,wBAAwB;IACxB,MAAM,CAAC,MAAM,CACX,gBAAgB,EAChB,6EAA6E,EAC7E;QACE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,gCAAgC,CAAC;KAC5D,EACD,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;QACb,QAAQ,EAAE;YACR;gBACE,IAAI,EAAE,MAAe;gBACrB,OAAO,EAAE;oBACP,IAAI,EAAE,MAAe;oBACrB,IAAI,EAAE;wBACJ,iCAAiC,IAAI,iCAAiC;wBACtE,EAAE;wBACF,qDAAqD;wBACrD,gEAAgE;wBAChE,8DAA8D;wBAC9D,wCAAwC;wBACxC,8CAA8C;wBAC9C,8CAA8C;wBAC9C,yCAAyC;wBACzC,EAAE;wBACF,yDAAyD,IAAI,YAAY;wBACzE,mCAAmC;wBACnC,0EAA0E;wBAC1E,mEAAmE;wBACnE,8BAA8B;wBAC9B,EAAE;wBACF,gEAAgE;qBACjE,CAAC,IAAI,CAAC,IAAI,CAAC;iBACb;aACF;SACF;KACF,CAAC,CACH,CAAC;IAEF,yBAAyB;IACzB,MAAM,CAAC,MAAM,CACX,iBAAiB,EACjB,iEAAiE,EACjE;QACE,MAAM,EAAE,CAAC;aACN,MAAM,EAAE;aACR,QAAQ,CACP,uEAAuE,CACxE;QACH,OAAO,EAAE,CAAC;aACP,MAAM,EAAE;aACR,QAAQ,CACP,8DAA8D,CAC/D;QACH,OAAO,EAAE,CAAC;aACP,MAAM,EAAE;aACR,QAAQ,CACP,+DAA+D,CAChE;KACJ,EACD,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC;QACjC,QAAQ,EAAE;YACR;gBACE,IAAI,EAAE,MAAe;gBACrB,OAAO,EAAE;oBACP,IAAI,EAAE,MAAe;oBACrB,IAAI,EAAE;wBACJ,cAAc,MAAM,qBAAqB,OAAO,QAAQ,OAAO,GAAG;wBAClE,EAAE;wBACF,qCAAqC,MAAM,0CAA0C;wBACrF,0EAA0E;wBAC1E,EAAE;wBACF,4BAA4B;wBAC5B,4BAA4B;wBAC5B,kCAAkC;wBAClC,wCAAwC;wBACxC,EAAE;wBACF,6CAA6C;qBAC9C,CAAC,IAAI,CAAC,IAAI,CAAC;iBACb;aACF;SACF;KACF,CAAC,CACH,CAAC;AACJ,CAAC"}
|