@frmwrkd/snag-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.
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Token accounting — the single number that shapes this entire package.
3
+ *
4
+ * doc 03 §4.3 **[V]**:
5
+ *
6
+ * > **MCP tool output: warns at 10,000 tokens, hard-capped at 25,000**
7
+ * > (`MAX_MCP_OUTPUT_TOKENS`). This is the single most important number for
8
+ * > handoff design — it's why `.snag/` files, not MCP, carry the bulk.
9
+ *
10
+ * And doc 02 §9.2:
11
+ *
12
+ * > **Five tools. Not fifty.** Chrome DevTools MCP ships ~17,000 tokens of tool
13
+ * > definitions before a single call; that cost is paid on every turn.
14
+ *
15
+ * The estimator here is deliberately the crude `chars / 4` heuristic rather
16
+ * than a real tokenizer: no tokenizer is installed, and the point of these
17
+ * numbers is to fail a regression test when a response grows by an order of
18
+ * magnitude, not to bill anyone. It over-estimates on JSON (punctuation-heavy
19
+ * text tokenizes denser than 4 chars/token), which is the safe direction.
20
+ */
21
+ /** Claude Code prints a warning above this many tokens of tool output. */
22
+ export declare const MCP_OUTPUT_WARN_TOKENS = 10000;
23
+ /** `MAX_MCP_OUTPUT_TOKENS` — Claude Code truncates above this. */
24
+ export declare const MCP_OUTPUT_HARD_CAP_TOKENS = 25000;
25
+ /**
26
+ * doc 02 §9.3: the `SessionStart` hook injects a **≤400-token** digest via
27
+ * `hookSpecificOutput.additionalContext`.
28
+ */
29
+ export declare const SESSION_START_DIGEST_MAX_TOKENS = 400;
30
+ /**
31
+ * doc 03 §4.3: "auto-compaction re-attaches only the first 5,000 tokens of each
32
+ * skill within a 25,000-token combined budget. Keep bodies under ~1,500 tokens."
33
+ */
34
+ export declare const SKILL_BODY_MAX_TOKENS = 1500;
35
+ /**
36
+ * doc 03 §4.4 **[V]**: the standard tier downscales an image to a 1568px long
37
+ * edge, so a screenshot costs **at most** 1,568 visual tokens — roughly 1,200
38
+ * words of prose. A 1440×900 shot lands almost exactly on that ceiling.
39
+ *
40
+ * Used as the per-image charge when estimating a tool result, because the real
41
+ * charge depends on pixel dimensions this package never decodes.
42
+ */
43
+ export declare const IMAGE_VISUAL_TOKENS_STANDARD = 1568;
44
+ /** Characters per token in the heuristic. */
45
+ export declare const CHARS_PER_TOKEN = 4;
46
+ export declare function estimateTokens(text: string): number;
47
+ export declare function estimateJsonTokens(value: unknown): number;
48
+ /** The minimum shape of a `CallToolResult` this estimator understands. */
49
+ export interface EstimableResult {
50
+ content?: readonly unknown[];
51
+ structuredContent?: unknown;
52
+ }
53
+ /**
54
+ * Approximate what a tool result costs the client's output budget.
55
+ *
56
+ * Image blocks are charged at `IMAGE_VISUAL_TOKENS_STANDARD` rather than at
57
+ * the length of their base64 payload: doc 03 §4.4 records that Claude
58
+ * tokenizes images in 28×28 patches after downscaling, so the base64 length is
59
+ * not the cost. **[U]** — the appendix also notes the MCP spec gives no
60
+ * explicit byte limit on image content and that the binding constraint is the
61
+ * client's own accounting, which is not published. If a client charged base64
62
+ * length instead, a single screenshot would blow the cap; that is exactly why
63
+ * `snag_get` never inlines one (D13) and `snag_screenshot` caps the bytes it
64
+ * will inline (see `tools.ts`).
65
+ */
66
+ export declare function estimateResultTokens(result: EstimableResult): number;
67
+ /** Cut `text` to at most `maxTokens`, on a word boundary, with an ellipsis. */
68
+ export declare function truncateToTokens(text: string, maxTokens: number): string;
69
+ //# sourceMappingURL=tokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,0EAA0E;AAC1E,eAAO,MAAM,sBAAsB,QAAS,CAAC;AAE7C,kEAAkE;AAClE,eAAO,MAAM,0BAA0B,QAAS,CAAC;AAEjD;;;GAGG;AACH,eAAO,MAAM,+BAA+B,MAAM,CAAC;AAEnD;;;GAGG;AACH,eAAO,MAAM,qBAAqB,OAAQ,CAAC;AAE3C;;;;;;;GAOG;AACH,eAAO,MAAM,4BAA4B,OAAQ,CAAC;AAElD,6CAA6C;AAC7C,eAAO,MAAM,eAAe,IAAI,CAAC;AAEjC,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEzD;AAED,0EAA0E;AAC1E,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;IAC7B,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CAiBpE;AAED,+EAA+E;AAC/E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAMxE"}
package/dist/tokens.js ADDED
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Token accounting — the single number that shapes this entire package.
3
+ *
4
+ * doc 03 §4.3 **[V]**:
5
+ *
6
+ * > **MCP tool output: warns at 10,000 tokens, hard-capped at 25,000**
7
+ * > (`MAX_MCP_OUTPUT_TOKENS`). This is the single most important number for
8
+ * > handoff design — it's why `.snag/` files, not MCP, carry the bulk.
9
+ *
10
+ * And doc 02 §9.2:
11
+ *
12
+ * > **Five tools. Not fifty.** Chrome DevTools MCP ships ~17,000 tokens of tool
13
+ * > definitions before a single call; that cost is paid on every turn.
14
+ *
15
+ * The estimator here is deliberately the crude `chars / 4` heuristic rather
16
+ * than a real tokenizer: no tokenizer is installed, and the point of these
17
+ * numbers is to fail a regression test when a response grows by an order of
18
+ * magnitude, not to bill anyone. It over-estimates on JSON (punctuation-heavy
19
+ * text tokenizes denser than 4 chars/token), which is the safe direction.
20
+ */
21
+ /** Claude Code prints a warning above this many tokens of tool output. */
22
+ export const MCP_OUTPUT_WARN_TOKENS = 10_000;
23
+ /** `MAX_MCP_OUTPUT_TOKENS` — Claude Code truncates above this. */
24
+ export const MCP_OUTPUT_HARD_CAP_TOKENS = 25_000;
25
+ /**
26
+ * doc 02 §9.3: the `SessionStart` hook injects a **≤400-token** digest via
27
+ * `hookSpecificOutput.additionalContext`.
28
+ */
29
+ export const SESSION_START_DIGEST_MAX_TOKENS = 400;
30
+ /**
31
+ * doc 03 §4.3: "auto-compaction re-attaches only the first 5,000 tokens of each
32
+ * skill within a 25,000-token combined budget. Keep bodies under ~1,500 tokens."
33
+ */
34
+ export const SKILL_BODY_MAX_TOKENS = 1_500;
35
+ /**
36
+ * doc 03 §4.4 **[V]**: the standard tier downscales an image to a 1568px long
37
+ * edge, so a screenshot costs **at most** 1,568 visual tokens — roughly 1,200
38
+ * words of prose. A 1440×900 shot lands almost exactly on that ceiling.
39
+ *
40
+ * Used as the per-image charge when estimating a tool result, because the real
41
+ * charge depends on pixel dimensions this package never decodes.
42
+ */
43
+ export const IMAGE_VISUAL_TOKENS_STANDARD = 1_568;
44
+ /** Characters per token in the heuristic. */
45
+ export const CHARS_PER_TOKEN = 4;
46
+ export function estimateTokens(text) {
47
+ return Math.ceil(text.length / CHARS_PER_TOKEN);
48
+ }
49
+ export function estimateJsonTokens(value) {
50
+ return estimateTokens(JSON.stringify(value) ?? '');
51
+ }
52
+ /**
53
+ * Approximate what a tool result costs the client's output budget.
54
+ *
55
+ * Image blocks are charged at `IMAGE_VISUAL_TOKENS_STANDARD` rather than at
56
+ * the length of their base64 payload: doc 03 §4.4 records that Claude
57
+ * tokenizes images in 28×28 patches after downscaling, so the base64 length is
58
+ * not the cost. **[U]** — the appendix also notes the MCP spec gives no
59
+ * explicit byte limit on image content and that the binding constraint is the
60
+ * client's own accounting, which is not published. If a client charged base64
61
+ * length instead, a single screenshot would blow the cap; that is exactly why
62
+ * `snag_get` never inlines one (D13) and `snag_screenshot` caps the bytes it
63
+ * will inline (see `tools.ts`).
64
+ */
65
+ export function estimateResultTokens(result) {
66
+ let total = 0;
67
+ for (const block of result.content ?? []) {
68
+ if (typeof block === 'object' &&
69
+ block !== null &&
70
+ block.type === 'image') {
71
+ total += IMAGE_VISUAL_TOKENS_STANDARD;
72
+ continue;
73
+ }
74
+ total += estimateJsonTokens(block);
75
+ }
76
+ if (result.structuredContent !== undefined) {
77
+ total += estimateJsonTokens(result.structuredContent);
78
+ }
79
+ return total;
80
+ }
81
+ /** Cut `text` to at most `maxTokens`, on a word boundary, with an ellipsis. */
82
+ export function truncateToTokens(text, maxTokens) {
83
+ const maxChars = maxTokens * CHARS_PER_TOKEN;
84
+ if (text.length <= maxChars)
85
+ return text;
86
+ const hard = text.slice(0, Math.max(0, maxChars - 1));
87
+ const lastSpace = hard.lastIndexOf(' ');
88
+ return `${lastSpace > maxChars / 2 ? hard.slice(0, lastSpace) : hard}…`;
89
+ }
90
+ //# sourceMappingURL=tokens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,0EAA0E;AAC1E,MAAM,CAAC,MAAM,sBAAsB,GAAG,MAAM,CAAC;AAE7C,kEAAkE;AAClE,MAAM,CAAC,MAAM,0BAA0B,GAAG,MAAM,CAAC;AAEjD;;;GAGG;AACH,MAAM,CAAC,MAAM,+BAA+B,GAAG,GAAG,CAAC;AAEnD;;;GAGG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,KAAK,CAAC;AAE3C;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAAG,KAAK,CAAC;AAElD,6CAA6C;AAC7C,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC;AAEjC,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,eAAe,CAAC,CAAC;AAClD,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,KAAc;IAC/C,OAAO,cAAc,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;AACrD,CAAC;AAQD;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAAuB;IAC1D,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,OAAO,IAAI,EAAE,EAAE,CAAC;QACzC,IACE,OAAO,KAAK,KAAK,QAAQ;YACzB,KAAK,KAAK,IAAI;YACb,KAA4B,CAAC,IAAI,KAAK,OAAO,EAC9C,CAAC;YACD,KAAK,IAAI,4BAA4B,CAAC;YACtC,SAAS;QACX,CAAC;QACD,KAAK,IAAI,kBAAkB,CAAC,KAAK,CAAC,CAAC;IACrC,CAAC;IACD,IAAI,MAAM,CAAC,iBAAiB,KAAK,SAAS,EAAE,CAAC;QAC3C,KAAK,IAAI,kBAAkB,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC;IACxD,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,SAAiB;IAC9D,MAAM,QAAQ,GAAG,SAAS,GAAG,eAAe,CAAC;IAC7C,IAAI,IAAI,CAAC,MAAM,IAAI,QAAQ;QAAE,OAAO,IAAI,CAAC;IACzC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC;IACtD,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACxC,OAAO,GAAG,SAAS,GAAG,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC;AAC1E,CAAC"}
@@ -0,0 +1,115 @@
1
+ import type { Task } from '@frmwrkd/snag-core';
2
+ import type { CallToolResult, Tool, ToolAnnotations } from '@modelcontextprotocol/sdk/types.js';
3
+ import { z } from 'zod';
4
+ import type { SnagBackend } from './client.js';
5
+ /** Belt-and-braces on the list size. 50 summaries is ~2,400 tokens. */
6
+ export declare const LIST_LIMIT_DEFAULT = 20;
7
+ export declare const LIST_LIMIT_MAX = 50;
8
+ /**
9
+ * Refuse to inline a screenshot larger than this. doc 03 §4.4 records that the
10
+ * MCP spec gives **no** explicit byte limit on image content and marks the real
11
+ * binding constraint **[U]**; the provider limit is 10 MB of base64. 3.5 MB of
12
+ * PNG is ~4.7 MB base64, comfortably inside that, and anything bigger is a
13
+ * full-page capture that the agent should `Read` from disk instead.
14
+ */
15
+ export declare const SCREENSHOT_MAX_INLINE_BYTES = 3500000;
16
+ /**
17
+ * doc 02 §9.2: "compact task list — id, title, severity, wave, first suspect
18
+ * file". `uid` is deliberately absent: it is 30 characters × every row, and
19
+ * every tool here accepts the `SNAG-<n>` label instead. `kind` is one token and
20
+ * tells the agent whether `snag_screenshot` is worth calling at all.
21
+ */
22
+ declare const TaskSummarySchema: z.ZodObject<{
23
+ id: z.ZodString;
24
+ title: z.ZodString;
25
+ severity: z.ZodEnum<["p0", "p1", "p2", "p3"]>;
26
+ kind: z.ZodString;
27
+ wave: z.ZodNumber;
28
+ status: z.ZodEnum<["open", "claimed", "in_progress", "verifying", "needs_review", "resolved", "wontfix"]>;
29
+ /** VERBATIM. The first `suspect_files` entry — never paraphrased. */
30
+ suspect_file: z.ZodNullable<z.ZodString>;
31
+ suspect_lines: z.ZodNullable<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>>;
32
+ }, "strip", z.ZodTypeAny, {
33
+ id: string;
34
+ title: string;
35
+ status: "open" | "resolved" | "claimed" | "in_progress" | "verifying" | "needs_review" | "wontfix";
36
+ kind: string;
37
+ severity: "p0" | "p1" | "p2" | "p3";
38
+ wave: number;
39
+ suspect_file: string | null;
40
+ suspect_lines: [number, number] | null;
41
+ }, {
42
+ id: string;
43
+ title: string;
44
+ status: "open" | "resolved" | "claimed" | "in_progress" | "verifying" | "needs_review" | "wontfix";
45
+ kind: string;
46
+ severity: "p0" | "p1" | "p2" | "p3";
47
+ wave: number;
48
+ suspect_file: string | null;
49
+ suspect_lines: [number, number] | null;
50
+ }>;
51
+ export type TaskSummary = z.infer<typeof TaskSummarySchema>;
52
+ export declare function summarise(task: Task): TaskSummary;
53
+ export interface SnagToolDefinition {
54
+ readonly name: string;
55
+ readonly title: string;
56
+ readonly description: string;
57
+ readonly inputSchema: z.ZodTypeAny;
58
+ readonly outputSchema?: z.ZodTypeAny;
59
+ readonly annotations: ToolAnnotations;
60
+ handle(args: unknown, backend: SnagBackend): Promise<CallToolResult>;
61
+ }
62
+ /**
63
+ * ⚠️ EXACTLY FIVE. doc 02 §9.2 and doc 03 §1.3: Chrome DevTools MCP ships
64
+ * ~17,000 tokens of tool definitions before a single call and pays that on
65
+ * every turn; Vercel measured a 3.5× speed-up and 37% fewer tokens from cutting
66
+ * an agent's toolset by 80%. Adding a sixth tool taxes every turn of every
67
+ * session in every repo that installs this server. If you need one more verb,
68
+ * add a parameter to an existing tool or put the data in `.snag/`, which has no
69
+ * cap at all.
70
+ */
71
+ export declare const TOOLS: readonly SnagToolDefinition[];
72
+ export declare const TOOL_COUNT = 5;
73
+ export declare function findTool(name: string): SnagToolDefinition | undefined;
74
+ /**
75
+ * Drop the redundant `items: false` from a length-pinned tuple.
76
+ *
77
+ * `@frmwrkd/snag-core`'s `toInlineJsonSchema` correctly rewrites a draft-7 tuple into
78
+ * the 2020-12 form:
79
+ *
80
+ * ```json
81
+ * { "type": "array", "minItems": 2, "maxItems": 2,
82
+ * "prefixItems": [{"type":"integer"},{"type":"integer"}], "items": false }
83
+ * ```
84
+ *
85
+ * That is exactly right per the spec — and the MCP TypeScript SDK v1.30.0
86
+ * rejects it. Its default validator is `AjvJsonSchemaValidator`, which
87
+ * constructs `new Ajv(...)` from `ajv`'s **draft-07** entry point. Draft-07 has
88
+ * no `prefixItems` keyword, so Ajv ignores it and applies `items: false` to
89
+ * every element, and a client validating `structuredContent` against the
90
+ * declared `outputSchema` fails a perfectly valid `[80, 96]` with:
91
+ *
92
+ * > MCP error -32602: Structured content does not match the tool's output
93
+ * > schema: data/tasks/0/suspect_lines/0 boolean schema is false
94
+ *
95
+ * Verified against `ajv@8.20.0` and reproduced end to end over stdio, which is
96
+ * the only place it shows up — the server-side path never sees it.
97
+ *
98
+ * When `maxItems === prefixItems.length`, `items` is vacuous in 2020-12 (there
99
+ * can be no element past the prefix), so deleting it changes nothing for a
100
+ * 2020-12 validator and makes a draft-07 validator fall back to `minItems` /
101
+ * `maxItems`, which still pin the arity. Where the arity is *not* pinned,
102
+ * `items: false` is load-bearing and is left alone.
103
+ *
104
+ * `Task.suspect_files[].lines` is the tuple that hits this, and it is the one
105
+ * field an agent uses to open a file at the right place. Losing it to a
106
+ * validation error would be a silent, total failure of `snag_get`.
107
+ */
108
+ export declare function openTupleTail(node: unknown): unknown;
109
+ export declare function toolDefinitions(): Tool[];
110
+ /** Dispatch a `tools/call`. Unknown names are an error result, not a throw. */
111
+ export declare function callTool(name: string, args: unknown, backend: SnagBackend): Promise<CallToolResult>;
112
+ /** Exposed for the regression test that pins the status vocabulary. */
113
+ export declare const KNOWN_TASK_STATUSES: readonly ["open", "claimed", "in_progress", "verifying", "needs_review", "resolved", "wontfix"];
114
+ export {};
115
+ //# sourceMappingURL=tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAsDA,OAAO,KAAK,EAAQ,IAAI,EAAc,MAAM,oBAAoB,CAAC;AACjE,OAAO,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAChG,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EACV,WAAW,EAIZ,MAAM,aAAa,CAAC;AAqBrB,uEAAuE;AACvE,eAAO,MAAM,kBAAkB,KAAK,CAAC;AACrC,eAAO,MAAM,cAAc,KAAK,CAAC;AAEjC;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,UAAY,CAAC;AAIrD;;;;;GAKG;AACH,QAAA,MAAM,iBAAiB;;;;;;;IAOrB,qEAAqE;;;;;;;;;;;;;;;;;;;;;EAGrE,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,WAAW,CAYjD;AAyFD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,UAAU,CAAC;IACnC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,UAAU,CAAC;IACrC,QAAQ,CAAC,WAAW,EAAE,eAAe,CAAC;IACtC,MAAM,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CACtE;AA8pBD;;;;;;;;GAQG;AACH,eAAO,MAAM,KAAK,EAAE,SAAS,kBAAkB,EAM9C,CAAC;AAEF,eAAO,MAAM,UAAU,IAAI,CAAC;AAE5B,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS,CAErE;AAkBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAkBpD;AAED,wBAAgB,eAAe,IAAI,IAAI,EAAE,CAiBxC;AAED,+EAA+E;AAC/E,wBAAsB,QAAQ,CAC5B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,cAAc,CAAC,CAgBzB;AAED,uEAAuE;AACvE,eAAO,MAAM,mBAAmB,iGAAgB,CAAC"}