snowbll-mcp 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/LICENSE +21 -0
- package/README.md +234 -0
- package/dist/client.js +107 -0
- package/dist/client.js.map +1 -0
- package/dist/communityClient.js +96 -0
- package/dist/communityClient.js.map +1 -0
- package/dist/crossCheckClient.js +83 -0
- package/dist/crossCheckClient.js.map +1 -0
- package/dist/http.js +70 -0
- package/dist/http.js.map +1 -0
- package/dist/index.js +391 -0
- package/dist/index.js.map +1 -0
- package/dist/mockData.js +471 -0
- package/dist/mockData.js.map +1 -0
- package/dist/prompts.js +48 -0
- package/dist/prompts.js.map +1 -0
- package/dist/resources.js +54 -0
- package/dist/resources.js.map +1 -0
- package/dist/schemas.js +174 -0
- package/dist/schemas.js.map +1 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Snowbll
|
|
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,234 @@
|
|
|
1
|
+
# Snowbll MCP
|
|
2
|
+
|
|
3
|
+
**Snowbll gives AI agents deep Gaming Behaviour Intelligence.**
|
|
4
|
+
|
|
5
|
+
> Not another game recommender. A behaviour intelligence layer for gaming agents.
|
|
6
|
+
|
|
7
|
+
Snowbll MCP is a [Model Context Protocol](https://modelcontextprotocol.io) server
|
|
8
|
+
that exposes Snowbll's gaming-behaviour intelligence as tools any AI agent can
|
|
9
|
+
call — Claude, Cursor, Windsurf, OpenAI Agents SDK, LangChain, CrewAI, AutoGen,
|
|
10
|
+
the Vercel AI SDK, and custom TypeScript/Python agents.
|
|
11
|
+
|
|
12
|
+
It helps agents understand how a gamer actually plays: their playstyle DNA,
|
|
13
|
+
gaming persona, session patterns, completion behavior, purchase behavior,
|
|
14
|
+
genre mismatches, game-fit prediction, and likely next best game.
|
|
15
|
+
|
|
16
|
+
## What this is (and is not)
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
AI Agent
|
|
20
|
+
→ Snowbll MCP Server (stdio)
|
|
21
|
+
→ Snowbll API or mock data
|
|
22
|
+
→ Gaming Behaviour Intelligence
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Snowbll MCP is **not** the agent. It is a **tool provider**. The agent decides
|
|
26
|
+
when to call a tool; Snowbll MCP returns structured behaviour intelligence as
|
|
27
|
+
JSON. The first version ships with realistic **mock data** so you can build and
|
|
28
|
+
test integrations immediately, with no backend required.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install
|
|
34
|
+
npm run build
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Run locally
|
|
38
|
+
|
|
39
|
+
Run the server in mock mode (the default — no API key needed):
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm run dev
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
You should see, on stderr:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
[snowbll-mcp] running on stdio — data mode: mock
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The server speaks MCP over stdio, so it's meant to be launched by an MCP client
|
|
52
|
+
(Claude Desktop, Cursor, etc.) rather than used interactively. To smoke-test the
|
|
53
|
+
built binary directly:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm run build
|
|
57
|
+
node dist/index.js
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Connect it to Claude / Cursor (local development)
|
|
61
|
+
|
|
62
|
+
After `npm run build`, point your MCP client at the built file. Use an absolute
|
|
63
|
+
path to `dist/index.js`.
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"mcpServers": {
|
|
68
|
+
"snowbll": {
|
|
69
|
+
"command": "node",
|
|
70
|
+
"args": ["C:/PATH/TO/snowbll-mcp/dist/index.js"],
|
|
71
|
+
"env": {
|
|
72
|
+
"SNOWBLL_USE_MOCK": "true"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
- **Claude Desktop:** add this to `claude_desktop_config.json`
|
|
80
|
+
(Settings → Developer → Edit Config), then restart Claude Desktop.
|
|
81
|
+
- **Cursor / Windsurf:** add it to the editor's MCP settings (`mcp.json`).
|
|
82
|
+
|
|
83
|
+
### Future: install from npm
|
|
84
|
+
|
|
85
|
+
Once Snowbll MCP is published and you have an API key, no local build is needed:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"mcpServers": {
|
|
90
|
+
"snowbll": {
|
|
91
|
+
"command": "npx",
|
|
92
|
+
"args": ["-y", "snowbll-mcp"],
|
|
93
|
+
"env": {
|
|
94
|
+
"SNOWBLL_API_KEY": "snb_YOUR_API_KEY"
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Configuration
|
|
102
|
+
|
|
103
|
+
| Variable | Default | Description |
|
|
104
|
+
| ------------------ | ----------------------------- | --------------------------------------------------------------------------- |
|
|
105
|
+
| `SNOWBLL_USE_MOCK` | `true` | Use local mock intelligence. Set to `false` to call the real Snowbll API. |
|
|
106
|
+
| `SNOWBLL_API_URL` | `https://api.snowbll.com/v1` | Base URL of the Snowbll API (live mode only). |
|
|
107
|
+
| `SNOWBLL_API_KEY` | _(none)_ | Required when `SNOWBLL_USE_MOCK=false`. Format `snb_...`. |
|
|
108
|
+
| `SNOWBLL_API_TIMEOUT_MS` | `10000` | Per-request timeout (live mode). Hung requests abort and surface an error. |
|
|
109
|
+
| `SNOWBLL_API_RETRIES` | `2` | Retries after the first attempt for transient errors (timeout, 429, 5xx). |
|
|
110
|
+
|
|
111
|
+
Mock mode is the default: the server runs with zero configuration and never
|
|
112
|
+
needs an API key. In live mode (`SNOWBLL_USE_MOCK=false`), a missing
|
|
113
|
+
`SNOWBLL_API_KEY` produces a clear, actionable error. Live requests time out and
|
|
114
|
+
retry transient failures (429/5xx) with exponential backoff; non-retryable 4xx
|
|
115
|
+
errors fail fast.
|
|
116
|
+
|
|
117
|
+
## Tests
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npm test
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Runs the suite via Node's built-in test runner (`tsx test/all.test.ts`): mock
|
|
124
|
+
intelligence logic, output-schema round-trips, client mock/live routing, and the
|
|
125
|
+
HTTP layer's timeout/retry behaviour (against a local server). No network.
|
|
126
|
+
|
|
127
|
+
## Common workflows
|
|
128
|
+
|
|
129
|
+
**Understand a player**
|
|
130
|
+
1. `get_player_overview` — high-level identity and current objective.
|
|
131
|
+
2. `analyze_player_behaviour` — full behavioural analysis (`depth: "deep"` for correlations and risks).
|
|
132
|
+
3. `get_persona_cards` / `get_playstyle_dna` — evidence-backed detail.
|
|
133
|
+
|
|
134
|
+
**Explain taste and behaviour**
|
|
135
|
+
- `explain_taste_pattern` with a question like _"Why do I abandon open-world RPGs?"_
|
|
136
|
+
- `detect_genre_mismatch` to surface where stated taste diverges from observed behaviour.
|
|
137
|
+
|
|
138
|
+
**Predict game fit**
|
|
139
|
+
- `predict_game_fit` for one title.
|
|
140
|
+
- `compare_games_for_player` to rank 2–5 candidates.
|
|
141
|
+
|
|
142
|
+
**Recommend what to play next**
|
|
143
|
+
- `recommend_games` with an objective (`likely_to_finish`, `hidden_gems`, `deep_systems`, `comfort_games`, `short_sessions`, `high_progression`).
|
|
144
|
+
|
|
145
|
+
## Tool catalog
|
|
146
|
+
|
|
147
|
+
| Tool | Input | What it returns |
|
|
148
|
+
| ---------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------- |
|
|
149
|
+
| `get_player_overview` | _none_ | Totals, persona match, current objective, identity summary. |
|
|
150
|
+
| `get_playstyle_dna` | _none_ | Playstyle traits with scores and tiers, plus the player's unique edge. |
|
|
151
|
+
| `get_persona_cards` | _none_ | Persona cards with rarity, level, progress, and evidence. |
|
|
152
|
+
| `get_activity_patterns` | _none_ | Active hours, session-length distribution, platform split, play rhythm. |
|
|
153
|
+
| `get_completion_behavior` | _none_ | Completion rate, strong/weak categories, drop-off signals. |
|
|
154
|
+
| `get_purchase_behavior` | `includeSensitiveSignals?` | Purchase aggregates and buy-vs-play mismatch. |
|
|
155
|
+
| `detect_genre_mismatch` | `includePurchaseSignals?`, `includeCompletionSignals?` | Where claimed/purchased taste diverges from observed behaviour. |
|
|
156
|
+
| `analyze_player_behaviour` | `depth?`, `includeEvidence?` | Full behavioural analysis with evidence and interpretation. |
|
|
157
|
+
| `explain_taste_pattern` | `topic` | Explanation, supporting signals, and actionable advice for a topic. |
|
|
158
|
+
| `predict_game_fit` | `gameTitle`, `platform?` | Fit score, finish likelihood, drop-off risk, and reasoning for one game. |
|
|
159
|
+
| `compare_games_for_player` | `games[2..5]` | Ranked comparison with a best pick. |
|
|
160
|
+
| `recommend_games` | `objective?`, `limit?` | Behaviour-fit game recommendations for a chosen objective. |
|
|
161
|
+
| `crosscheck_recommendations` | `objective?`, `limit?` | Games validated by TWO engines (consensus + referee) with an agreement score. |
|
|
162
|
+
|
|
163
|
+
## Resources
|
|
164
|
+
|
|
165
|
+
Attach a player's Snowbll context directly (read-only) instead of calling several tools:
|
|
166
|
+
|
|
167
|
+
| Resource URI | Contents |
|
|
168
|
+
| -------------------------------- | ------------------------------------------------------- |
|
|
169
|
+
| `snowbll://player/profile` | Combined overview + playstyle DNA + personas + activity |
|
|
170
|
+
| `snowbll://player/overview` | High-level identity and current objective |
|
|
171
|
+
| `snowbll://player/playstyle-dna` | Playstyle traits with scores and tiers |
|
|
172
|
+
| `snowbll://player/personas` | Persona cards |
|
|
173
|
+
|
|
174
|
+
## Prompts
|
|
175
|
+
|
|
176
|
+
Ready-made templates, surfaced as slash-commands in Claude Desktop / Cursor:
|
|
177
|
+
|
|
178
|
+
| Prompt | Arguments | What it does |
|
|
179
|
+
| ------------------- | ------------ | -------------------------------------------- |
|
|
180
|
+
| `understand-player` | — | Build a full profile of the player |
|
|
181
|
+
| `what-to-play` | `objective?` | Recommend the next game and explain why |
|
|
182
|
+
| `should-i-play` | `gameTitle` | Predict fit and finish likelihood for a game |
|
|
183
|
+
| `compare-games` | `games` | Rank 2–5 games by behavioural fit |
|
|
184
|
+
|
|
185
|
+
> **Structured output:** the behaviour-intelligence tools return validated
|
|
186
|
+
> `structuredContent` (machine-readable JSON) alongside the text result.
|
|
187
|
+
|
|
188
|
+
## Privacy
|
|
189
|
+
|
|
190
|
+
By default, Snowbll MCP tools return **aggregate behaviour intelligence** — not
|
|
191
|
+
raw session logs, raw purchase data, private posts, or friend data.
|
|
192
|
+
`get_purchase_behavior` only exposes raw, purchase-level detail when explicitly
|
|
193
|
+
asked via `includeSensitiveSignals: true`; otherwise it returns aggregates only.
|
|
194
|
+
|
|
195
|
+
## Extending to a real backend
|
|
196
|
+
|
|
197
|
+
`src/client.ts` is the single seam between the MCP tools and the data source.
|
|
198
|
+
Each tool maps to one `client.call(endpoint, payload, mockFn)`:
|
|
199
|
+
|
|
200
|
+
- In **mock mode**, `mockFn()` (from `src/mockData.ts`) returns the data.
|
|
201
|
+
- In **live mode**, the same call `POST`s `payload` to `SNOWBLL_API_URL + endpoint`
|
|
202
|
+
with `Authorization: Bearer <SNOWBLL_API_KEY>` and returns the JSON response.
|
|
203
|
+
|
|
204
|
+
To wire up the real Snowbll API, implement those endpoints to match the response
|
|
205
|
+
shapes in `src/mockData.ts` (exported as TypeScript interfaces) and set
|
|
206
|
+
`SNOWBLL_USE_MOCK=false`. No changes to `index.ts` are required.
|
|
207
|
+
|
|
208
|
+
## Project layout
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
snowbll-mcp/
|
|
212
|
+
package.json
|
|
213
|
+
tsconfig.json
|
|
214
|
+
README.md
|
|
215
|
+
src/
|
|
216
|
+
index.ts # MCP server: registers tools, resources & prompts over stdio
|
|
217
|
+
client.ts # game data layer: mock-by-default, future real-API client
|
|
218
|
+
communityClient.ts # community/forum data layer (Community Recommendation Engine)
|
|
219
|
+
mockData.ts # typed mock dataset + helpers (game response shapes)
|
|
220
|
+
schemas.ts # zod output schemas (structured output) + type-sync guard
|
|
221
|
+
resources.ts # MCP resources (player snapshots)
|
|
222
|
+
prompts.ts # MCP prompts (slash-command templates)
|
|
223
|
+
http.ts # HTTP helper: timeout, retry, typed errors
|
|
224
|
+
test/ # node:test suite (npm test)
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Tech stack
|
|
228
|
+
|
|
229
|
+
TypeScript (strict, ESM) · Node.js ≥ 18 · `@modelcontextprotocol/sdk` · `zod` ·
|
|
230
|
+
stdio MCP transport.
|
|
231
|
+
|
|
232
|
+
## License
|
|
233
|
+
|
|
234
|
+
MIT
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SnowbllClient — the data layer behind the MCP tools.
|
|
3
|
+
*
|
|
4
|
+
* Mock mode is the DEFAULT. The client only talks to the real Snowbll API when
|
|
5
|
+
* SNOWBLL_USE_MOCK is explicitly set to the string "false". This lets the MCP
|
|
6
|
+
* server run with zero configuration today, while keeping a single seam through
|
|
7
|
+
* which a real backend can be wired in later without touching index.ts.
|
|
8
|
+
*
|
|
9
|
+
* Environment variables:
|
|
10
|
+
* SNOWBLL_USE_MOCK "false" -> call the real API. Anything else (or unset) -> mock.
|
|
11
|
+
* SNOWBLL_API_URL Base URL of the Snowbll API (default https://api.snowbll.com/v1).
|
|
12
|
+
* SNOWBLL_API_KEY Required only in live mode; format snb_...
|
|
13
|
+
* SNOWBLL_API_TIMEOUT_MS Per-request timeout in ms (default 10000).
|
|
14
|
+
* SNOWBLL_API_RETRIES Retries after the first attempt for transient errors (default 2).
|
|
15
|
+
*/
|
|
16
|
+
import { requestJson } from "./http.js";
|
|
17
|
+
import * as mock from "./mockData.js";
|
|
18
|
+
const DEFAULT_API_URL = "https://api.snowbll.com/v1";
|
|
19
|
+
/** Parse a positive-number env var, falling back to a default. */
|
|
20
|
+
function numEnv(value, fallback, min = 0) {
|
|
21
|
+
const n = Number(value);
|
|
22
|
+
return Number.isFinite(n) && n >= min ? n : fallback;
|
|
23
|
+
}
|
|
24
|
+
export class SnowbllClient {
|
|
25
|
+
useMock;
|
|
26
|
+
apiUrl;
|
|
27
|
+
apiKey;
|
|
28
|
+
timeoutMs;
|
|
29
|
+
retries;
|
|
30
|
+
constructor() {
|
|
31
|
+
// Mock is the default. Only the literal "false" opts into the live API.
|
|
32
|
+
this.useMock = process.env.SNOWBLL_USE_MOCK !== "false";
|
|
33
|
+
this.apiUrl = (process.env.SNOWBLL_API_URL ?? DEFAULT_API_URL).replace(/\/+$/, "");
|
|
34
|
+
this.apiKey = process.env.SNOWBLL_API_KEY;
|
|
35
|
+
this.timeoutMs = numEnv(process.env.SNOWBLL_API_TIMEOUT_MS, 10000, 1);
|
|
36
|
+
this.retries = numEnv(process.env.SNOWBLL_API_RETRIES, 2, 0);
|
|
37
|
+
}
|
|
38
|
+
/** "mock" when serving local data, "live" when calling the real Snowbll API. */
|
|
39
|
+
get mode() {
|
|
40
|
+
return this.useMock ? "mock" : "live";
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Route a tool call to mock data or the real API.
|
|
44
|
+
* In mock mode the network is never touched and no API key is required.
|
|
45
|
+
*/
|
|
46
|
+
async call(endpoint, payload, mockFn) {
|
|
47
|
+
if (this.useMock) {
|
|
48
|
+
return mockFn();
|
|
49
|
+
}
|
|
50
|
+
if (!this.apiKey) {
|
|
51
|
+
throw new Error("Snowbll live API mode is enabled (SNOWBLL_USE_MOCK=false) but SNOWBLL_API_KEY is not set.\n" +
|
|
52
|
+
"Fix one of the following:\n" +
|
|
53
|
+
" - Set SNOWBLL_API_KEY=snb_... to call the real Snowbll API, or\n" +
|
|
54
|
+
" - Set SNOWBLL_USE_MOCK=true (or unset it) to use local mock intelligence.");
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
return await requestJson(`${this.apiUrl}${endpoint}`, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: { authorization: `Bearer ${this.apiKey}` },
|
|
60
|
+
body: payload,
|
|
61
|
+
timeoutMs: this.timeoutMs,
|
|
62
|
+
retries: this.retries,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
67
|
+
throw new Error(`Snowbll API request to ${endpoint} failed: ${message}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
getPlayerOverview() {
|
|
71
|
+
return this.call("/player/overview", {}, () => mock.playerOverview);
|
|
72
|
+
}
|
|
73
|
+
getPlaystyleDna() {
|
|
74
|
+
return this.call("/player/playstyle-dna", {}, () => mock.playstyleDna);
|
|
75
|
+
}
|
|
76
|
+
getPersonaCards() {
|
|
77
|
+
return this.call("/player/persona-cards", {}, () => mock.personaCards);
|
|
78
|
+
}
|
|
79
|
+
getActivityPatterns() {
|
|
80
|
+
return this.call("/player/activity-patterns", {}, () => mock.activityPatterns);
|
|
81
|
+
}
|
|
82
|
+
getCompletionBehavior() {
|
|
83
|
+
return this.call("/player/completion-behavior", {}, () => mock.completionBehavior);
|
|
84
|
+
}
|
|
85
|
+
getPurchaseBehavior(includeSensitiveSignals) {
|
|
86
|
+
return this.call("/player/purchase-behavior", { includeSensitiveSignals }, () => mock.getPurchaseBehavior(includeSensitiveSignals));
|
|
87
|
+
}
|
|
88
|
+
detectGenreMismatch(includePurchaseSignals, includeCompletionSignals) {
|
|
89
|
+
return this.call("/player/genre-mismatch", { includePurchaseSignals, includeCompletionSignals }, () => mock.detectGenreMismatch(includePurchaseSignals, includeCompletionSignals));
|
|
90
|
+
}
|
|
91
|
+
analyzePlayerBehaviour(depth, includeEvidence) {
|
|
92
|
+
return this.call("/player/analyze", { depth, includeEvidence }, () => mock.analyzePlayerBehaviour(depth, includeEvidence));
|
|
93
|
+
}
|
|
94
|
+
explainTastePattern(topic) {
|
|
95
|
+
return this.call("/player/taste-pattern", { topic }, () => mock.explainTastePattern(topic));
|
|
96
|
+
}
|
|
97
|
+
predictGameFit(gameTitle, platform) {
|
|
98
|
+
return this.call("/predict/game-fit", { gameTitle, platform }, () => mock.predictGameFit(gameTitle, platform));
|
|
99
|
+
}
|
|
100
|
+
compareGamesForPlayer(games) {
|
|
101
|
+
return this.call("/predict/compare", { games }, () => mock.compareGames(games));
|
|
102
|
+
}
|
|
103
|
+
recommendGames(objective, limit) {
|
|
104
|
+
return this.call("/recommend", { objective, limit }, () => mock.recommendGames(objective, limit));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CommunityClient — the data layer behind the community MCP tools.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors {@link SnowbllClient}: mock mode is the DEFAULT. In mock mode the
|
|
5
|
+
* deterministic Community Recommendation Engine runs IN-PROCESS (full fidelity,
|
|
6
|
+
* zero config, no network). When SNOWBLL_USE_MOCK="false" it calls the live
|
|
7
|
+
* Community Recommendation REST API instead.
|
|
8
|
+
*
|
|
9
|
+
* The in-process engine (snowbll-community-recommendation-engine) is a PRIVATE
|
|
10
|
+
* package loaded LAZILY, only in mock mode, and declared as a devDependency — so
|
|
11
|
+
* the published MCP never ships or requires it (published builds use live mode /
|
|
12
|
+
* REST for community tools).
|
|
13
|
+
*
|
|
14
|
+
* Environment variables:
|
|
15
|
+
* SNOWBLL_USE_MOCK "false" -> call the live API. Anything else -> in-process engine.
|
|
16
|
+
* SNOWBLL_COMMUNITY_API_URL Base URL of the community API (default https://api.snowbll.com/v1/community).
|
|
17
|
+
* SNOWBLL_COMMUNITY_API_KEY Required only in live mode; sent as Authorization: Bearer <key>.
|
|
18
|
+
*/
|
|
19
|
+
const DEFAULT_API_URL = "https://api.snowbll.com/v1/community";
|
|
20
|
+
let enginePromise;
|
|
21
|
+
function loadEngine() {
|
|
22
|
+
if (!enginePromise) {
|
|
23
|
+
enginePromise = import("snowbll-community-recommendation-engine").catch(() => {
|
|
24
|
+
throw new Error("The in-process community engine is not available in this build.\n" +
|
|
25
|
+
"Set SNOWBLL_USE_MOCK=false and SNOWBLL_COMMUNITY_API_KEY to use the live community API instead.");
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return enginePromise;
|
|
29
|
+
}
|
|
30
|
+
export class CommunityClient {
|
|
31
|
+
useMock;
|
|
32
|
+
apiUrl;
|
|
33
|
+
apiKey;
|
|
34
|
+
constructor() {
|
|
35
|
+
// Mock is the default. Only the literal "false" opts into the live API.
|
|
36
|
+
this.useMock = process.env.SNOWBLL_USE_MOCK !== "false";
|
|
37
|
+
this.apiUrl = (process.env.SNOWBLL_COMMUNITY_API_URL ?? DEFAULT_API_URL).replace(/\/+$/, "");
|
|
38
|
+
this.apiKey = process.env.SNOWBLL_COMMUNITY_API_KEY;
|
|
39
|
+
}
|
|
40
|
+
/** "mock" when running the engine in-process, "live" when calling the REST API. */
|
|
41
|
+
get mode() {
|
|
42
|
+
return this.useMock ? "mock" : "live";
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Route a tool call to the in-process engine (mock) or the live REST API.
|
|
46
|
+
* In mock mode the network is never touched and no API key is required.
|
|
47
|
+
*/
|
|
48
|
+
async call(endpoint, payload, mockFn) {
|
|
49
|
+
if (this.useMock) {
|
|
50
|
+
return mockFn(await loadEngine());
|
|
51
|
+
}
|
|
52
|
+
if (!this.apiKey) {
|
|
53
|
+
throw new Error("Snowbll live API mode is enabled (SNOWBLL_USE_MOCK=false) but SNOWBLL_COMMUNITY_API_KEY is not set.\n" +
|
|
54
|
+
"Fix one of the following:\n" +
|
|
55
|
+
" - Set SNOWBLL_COMMUNITY_API_KEY=snb_comm_... to call the live community API, or\n" +
|
|
56
|
+
" - Set SNOWBLL_USE_MOCK=true (or unset it) to use the local deterministic engine.");
|
|
57
|
+
}
|
|
58
|
+
let res;
|
|
59
|
+
try {
|
|
60
|
+
res = await fetch(`${this.apiUrl}${endpoint}`, {
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: {
|
|
63
|
+
"content-type": "application/json",
|
|
64
|
+
authorization: `Bearer ${this.apiKey}`,
|
|
65
|
+
},
|
|
66
|
+
body: JSON.stringify(payload),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
catch (cause) {
|
|
70
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
71
|
+
throw new Error(`Failed to reach the Snowbll community API at ${this.apiUrl}${endpoint}: ${message}`);
|
|
72
|
+
}
|
|
73
|
+
if (!res.ok) {
|
|
74
|
+
const detail = await res.text().catch(() => "");
|
|
75
|
+
throw new Error(`Snowbll community API error ${res.status} ${res.statusText} for ${endpoint}` +
|
|
76
|
+
(detail ? `: ${detail}` : ""));
|
|
77
|
+
}
|
|
78
|
+
return (await res.json());
|
|
79
|
+
}
|
|
80
|
+
recommendForumThreads(args) {
|
|
81
|
+
return this.call("/recommend/threads", { ...args }, (engine) => engine.recommendForumThreads({ ...args }));
|
|
82
|
+
}
|
|
83
|
+
predictThreadFit(threadTitle) {
|
|
84
|
+
return this.call("/predict/thread-fit", { threadTitle }, (engine) => engine.predictThreadFit({ threadTitle }));
|
|
85
|
+
}
|
|
86
|
+
compareThreadsForPlayer(threads) {
|
|
87
|
+
return this.call("/compare/threads", { threads }, (engine) => engine.compareThreadsForPlayer({ threads }));
|
|
88
|
+
}
|
|
89
|
+
explainThreadRecommendation(threadTitle) {
|
|
90
|
+
return this.call("/explain/thread", { threadTitle }, (engine) => engine.explainThreadRecommendation({ threadTitle }));
|
|
91
|
+
}
|
|
92
|
+
detectThreadsToAvoid(limit) {
|
|
93
|
+
return this.call("/detect-threads-to-avoid", { limit }, (engine) => engine.detectThreadsToAvoid({ limit }));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=communityClient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"communityClient.js","sourceRoot":"","sources":["../src/communityClient.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CrossCheckClient — the data layer behind the `crosscheck_recommendations` tool.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors {@link SnowbllClient} / {@link CommunityClient}: mock mode is the DEFAULT
|
|
5
|
+
* and runs the deterministic Recommendation Engine's cross-check IN-PROCESS (full
|
|
6
|
+
* fidelity, zero config, no network). When SNOWBLL_USE_MOCK="false" it calls the
|
|
7
|
+
* live Recommendation REST API (POST /recommend/consensus) instead.
|
|
8
|
+
*
|
|
9
|
+
* The cross-check reconciles TWO independent Snowbll recommenders (this engine and
|
|
10
|
+
* the Persona Engine's own recommender) into one validated answer.
|
|
11
|
+
*
|
|
12
|
+
* The in-process engine (snowbll-recommendation-engine) is a PRIVATE package loaded
|
|
13
|
+
* LAZILY, only in mock mode, and declared as a devDependency — so the published MCP
|
|
14
|
+
* never ships or requires it (published builds use live mode / REST).
|
|
15
|
+
*
|
|
16
|
+
* Environment variables:
|
|
17
|
+
* SNOWBLL_USE_MOCK "false" -> call the live API. Anything else -> in-process engine.
|
|
18
|
+
* SNOWBLL_RECO_API_URL Base URL of the recommendation API (default https://api.snowbll.com/v1).
|
|
19
|
+
* SNOWBLL_RECO_API_KEY OPTIONAL — sent as Authorization: Bearer <key> when set.
|
|
20
|
+
* The recommendation API is open by default, so no key is required.
|
|
21
|
+
*/
|
|
22
|
+
const DEFAULT_API_URL = "https://api.snowbll.com/v1";
|
|
23
|
+
let recoEnginePromise;
|
|
24
|
+
function loadRecoEngine() {
|
|
25
|
+
if (!recoEnginePromise) {
|
|
26
|
+
recoEnginePromise = import("snowbll-recommendation-engine").catch(() => {
|
|
27
|
+
throw new Error("The in-process recommendation engine is not available in this build.\n" +
|
|
28
|
+
"Set SNOWBLL_USE_MOCK=false (optionally with SNOWBLL_RECO_API_KEY) to use the live recommendation API instead.");
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return recoEnginePromise;
|
|
32
|
+
}
|
|
33
|
+
export class CrossCheckClient {
|
|
34
|
+
useMock;
|
|
35
|
+
apiUrl;
|
|
36
|
+
apiKey;
|
|
37
|
+
constructor() {
|
|
38
|
+
// Mock is the default. Only the literal "false" opts into the live API.
|
|
39
|
+
this.useMock = process.env.SNOWBLL_USE_MOCK !== "false";
|
|
40
|
+
this.apiUrl = (process.env.SNOWBLL_RECO_API_URL ?? DEFAULT_API_URL).replace(/\/+$/, "");
|
|
41
|
+
this.apiKey = process.env.SNOWBLL_RECO_API_KEY;
|
|
42
|
+
}
|
|
43
|
+
/** "mock" when running the engine in-process, "live" when calling the REST API. */
|
|
44
|
+
get mode() {
|
|
45
|
+
return this.useMock ? "mock" : "live";
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Cross-check the two engines for an objective. In mock mode the reconciliation
|
|
49
|
+
* runs in-process; in live mode it POSTs to /recommend/consensus.
|
|
50
|
+
*/
|
|
51
|
+
async crossCheckRecommendations(args) {
|
|
52
|
+
if (this.useMock) {
|
|
53
|
+
const engine = await loadRecoEngine();
|
|
54
|
+
return engine.crossCheckRecommendations({
|
|
55
|
+
objective: args.objective,
|
|
56
|
+
limit: args.limit,
|
|
57
|
+
includeCommunity: args.includeCommunity,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
const headers = { "content-type": "application/json" };
|
|
61
|
+
if (this.apiKey)
|
|
62
|
+
headers.authorization = `Bearer ${this.apiKey}`;
|
|
63
|
+
let res;
|
|
64
|
+
try {
|
|
65
|
+
res = await fetch(`${this.apiUrl}/recommend/consensus`, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers,
|
|
68
|
+
body: JSON.stringify(args),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch (cause) {
|
|
72
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
73
|
+
throw new Error(`Failed to reach the Snowbll recommendation API at ${this.apiUrl}/recommend/consensus: ${message}`);
|
|
74
|
+
}
|
|
75
|
+
if (!res.ok) {
|
|
76
|
+
const detail = await res.text().catch(() => "");
|
|
77
|
+
throw new Error(`Snowbll recommendation API error ${res.status} ${res.statusText} for /recommend/consensus` +
|
|
78
|
+
(detail ? `: ${detail}` : ""));
|
|
79
|
+
}
|
|
80
|
+
return (await res.json());
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=crossCheckClient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crossCheckClient.js","sourceRoot":"","sources":["../src/crossCheckClient.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,MAAM,eAAe,GAAG,4BAA4B,CAAC;AAIrD,IAAI,iBAAwD,CAAC;AAE7D,SAAS,cAAc;IACrB,IAAI,CAAC,iBAAiB,EAAE,CAAC;QACvB,iBAAiB,GAAG,MAAM,CAAC,+BAA+B,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE;YACrE,MAAM,IAAI,KAAK,CACb,wEAAwE;gBACtE,+GAA+G,CAClH,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC;IACD,OAAO,iBAAiB,CAAC;AAC3B,CAAC;AAQD,MAAM,OAAO,gBAAgB;IACV,OAAO,CAAU;IACjB,MAAM,CAAS;IACf,MAAM,CAAqB;IAE5C;QACE,wEAAwE;QACxE,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,KAAK,OAAO,CAAC;QACxD,IAAI,CAAC,MAAM,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,eAAe,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QACxF,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC;IACjD,CAAC;IAED,mFAAmF;IACnF,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;IACxC,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,yBAAyB,CAAC,IAAoB;QAClD,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,MAAM,MAAM,GAAG,MAAM,cAAc,EAAE,CAAC;YACtC,OAAO,MAAM,CAAC,yBAAyB,CAAC;gBACtC,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,gBAAgB,EAAE,IAAI,CAAC,gBAAgB;aACxC,CAAC,CAAC;QACL,CAAC;QAED,MAAM,OAAO,GAA2B,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC;QAC/E,IAAI,IAAI,CAAC,MAAM;YAAE,OAAO,CAAC,aAAa,GAAG,UAAU,IAAI,CAAC,MAAM,EAAE,CAAC;QAEjE,IAAI,GAAa,CAAC;QAClB,IAAI,CAAC;YACH,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,MAAM,sBAAsB,EAAE;gBACtD,MAAM,EAAE,MAAM;gBACd,OAAO;gBACP,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;aAC3B,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACvE,MAAM,IAAI,KAAK,CACb,qDAAqD,IAAI,CAAC,MAAM,yBAAyB,OAAO,EAAE,CACnG,CAAC;QACJ,CAAC;QAED,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;YAChD,MAAM,IAAI,KAAK,CACb,oCAAoC,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,UAAU,2BAA2B;gBACzF,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAChC,CAAC;QACJ,CAAC;QAED,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAoB,CAAC;IAC/C,CAAC;CACF"}
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small, dependency-free HTTP helper with timeout, retry, and typed errors.
|
|
3
|
+
*
|
|
4
|
+
* Used by SnowbllClient in live mode so a slow or flaky Snowbll API can't hang a
|
|
5
|
+
* tool call forever and transient failures recover automatically.
|
|
6
|
+
*/
|
|
7
|
+
export class HttpError extends Error {
|
|
8
|
+
status;
|
|
9
|
+
statusText;
|
|
10
|
+
detail;
|
|
11
|
+
constructor(status, statusText, detail) {
|
|
12
|
+
super(`HTTP ${status} ${statusText}${detail ? `: ${detail}` : ""}`);
|
|
13
|
+
this.status = status;
|
|
14
|
+
this.statusText = statusText;
|
|
15
|
+
this.detail = detail;
|
|
16
|
+
this.name = "HttpError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
20
|
+
/** Exponential backoff, capped at 1s. */
|
|
21
|
+
const backoffMs = (attempt) => Math.min(1000, 200 * 2 ** attempt);
|
|
22
|
+
/** 429 (rate limited) and 5xx (server) are transient; other 4xx are not. */
|
|
23
|
+
const isRetryableStatus = (status) => status === 429 || status >= 500;
|
|
24
|
+
/**
|
|
25
|
+
* POST/GET JSON with timeout + retry. Resolves the parsed JSON body, or throws:
|
|
26
|
+
* - HttpError for a non-OK response (after retries for 429/5xx),
|
|
27
|
+
* - Error("request timed out …") on timeout,
|
|
28
|
+
* - Error("network error …") on connection failure.
|
|
29
|
+
*/
|
|
30
|
+
export async function requestJson(url, options = {}) {
|
|
31
|
+
const { method = "POST", headers = {}, body, timeoutMs = 10000, retries = 2, } = options;
|
|
32
|
+
let lastError = new Error("request not attempted");
|
|
33
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
36
|
+
let res;
|
|
37
|
+
try {
|
|
38
|
+
res = await fetch(url, {
|
|
39
|
+
method,
|
|
40
|
+
headers: { "content-type": "application/json", ...headers },
|
|
41
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
42
|
+
signal: controller.signal,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
lastError = controller.signal.aborted
|
|
48
|
+
? new Error(`request timed out after ${timeoutMs}ms`)
|
|
49
|
+
: new Error(`network error: ${err instanceof Error ? err.message : String(err)}`);
|
|
50
|
+
if (attempt < retries) {
|
|
51
|
+
await sleep(backoffMs(attempt));
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
throw lastError;
|
|
55
|
+
}
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
if (res.ok) {
|
|
58
|
+
return (await res.json());
|
|
59
|
+
}
|
|
60
|
+
if (isRetryableStatus(res.status) && attempt < retries) {
|
|
61
|
+
lastError = new HttpError(res.status, res.statusText, "");
|
|
62
|
+
await sleep(backoffMs(attempt));
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const detail = await res.text().catch(() => "");
|
|
66
|
+
throw new HttpError(res.status, res.statusText, detail);
|
|
67
|
+
}
|
|
68
|
+
throw lastError;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=http.js.map
|
package/dist/http.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,OAAO,SAAU,SAAQ,KAAK;IAEhB;IACA;IACA;IAHlB,YACkB,MAAc,EACd,UAAkB,EAClB,MAAc;QAE9B,KAAK,CAAC,QAAQ,MAAM,IAAI,UAAU,GAAG,MAAM,CAAC,CAAC,CAAC,KAAK,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAJpD,WAAM,GAAN,MAAM,CAAQ;QACd,eAAU,GAAV,UAAU,CAAQ;QAClB,WAAM,GAAN,MAAM,CAAQ;QAG9B,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;IAC1B,CAAC;CACF;AAaD,MAAM,KAAK,GAAG,CAAC,EAAU,EAAiB,EAAE,CAC1C,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAEpD,yCAAyC;AACzC,MAAM,SAAS,GAAG,CAAC,OAAe,EAAU,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,GAAG,CAAC,IAAI,OAAO,CAAC,CAAC;AAElF,4EAA4E;AAC5E,MAAM,iBAAiB,GAAG,CAAC,MAAc,EAAW,EAAE,CACpD,MAAM,KAAK,GAAG,IAAI,MAAM,IAAI,GAAG,CAAC;AAElC;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,GAAW,EACX,UAA0B,EAAE;IAE5B,MAAM,EACJ,MAAM,GAAG,MAAM,EACf,OAAO,GAAG,EAAE,EACZ,IAAI,EACJ,SAAS,GAAG,KAAK,EACjB,OAAO,GAAG,CAAC,GACZ,GAAG,OAAO,CAAC;IAEZ,IAAI,SAAS,GAAU,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC;IAE1D,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,OAAO,EAAE,OAAO,EAAE,EAAE,CAAC;QACpD,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,SAAS,CAAC,CAAC;QAE9D,IAAI,GAAa,CAAC;QAClB,IAAI,CAAC;YACH,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;gBACrB,MAAM;gBACN,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,GAAG,OAAO,EAAE;gBAC3D,IAAI,EAAE,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;gBAC3D,MAAM,EAAE,UAAU,CAAC,MAAM;aAC1B,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,SAAS,GAAG,UAAU,CAAC,MAAM,CAAC,OAAO;gBACnC,CAAC,CAAC,IAAI,KAAK,CAAC,2BAA2B,SAAS,IAAI,CAAC;gBACrD,CAAC,CAAC,IAAI,KAAK,CACP,kBAAkB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CACrE,CAAC;YACN,IAAI,OAAO,GAAG,OAAO,EAAE,CAAC;gBACtB,MAAM,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC;gBAChC,SAAS;YACX,CAAC;YACD,MAAM,SAAS,CAAC;QAClB,CAAC;QACD,YAAY,CAAC,KAAK,CAAC,CAAC;QAEpB,IAAI,GAAG,CAAC,EAAE,EAAE,CAAC;YACX,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAM,CAAC;QACjC,CAAC;QAED,IAAI,iBAAiB,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,OAAO,GAAG,OAAO,EAAE,CAAC;YACvD,SAAS,GAAG,IAAI,SAAS,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;YAC1D,MAAM,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC;YAChC,SAAS;QACX,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QAChD,MAAM,IAAI,SAAS,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IAC1D,CAAC;IAED,MAAM,SAAS,CAAC;AAClB,CAAC"}
|