@overlive/emotes 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,45 @@
1
+ import type { EmotePlatform, MessageToken } from '@overlive/core';
2
+ /**
3
+ * Fetches and caches emotes from all configured platforms,
4
+ * then resolves chat message text into typed tokens.
5
+ *
6
+ * Usage:
7
+ * const resolver = new EmoteResolver({ channelId: '...', twitchClientId: '...' })
8
+ * await resolver.warmup()
9
+ * const tokens = await resolver.resolve(messageText, twitchEmoteData)
10
+ */
11
+ export declare class EmoteResolver {
12
+ private readonly config;
13
+ private readonly cache;
14
+ private readonly platforms;
15
+ constructor(config: {
16
+ /** Twitch broadcaster user ID */
17
+ channelId: string;
18
+ /** Twitch Client-ID for emote API */
19
+ twitchClientId?: string;
20
+ twitchAccessToken?: string;
21
+ /** Which emote platforms to resolve. Defaults to all. */
22
+ platforms?: EmotePlatform[];
23
+ });
24
+ /**
25
+ * Pre-fetch all emote sets. Call this after connecting to warm the cache.
26
+ */
27
+ warmup(): Promise<void>;
28
+ /**
29
+ * Resolve a message string into typed tokens, injecting emote objects
30
+ * where emote names are found in the text.
31
+ *
32
+ * @param text Raw message text
33
+ * @param twitchEmoteRanges Optional raw Twitch emote positions from IRC
34
+ * (format: "id:start-end,start-end/id:...")
35
+ */
36
+ resolve(text: string, twitchEmoteRanges?: string): Promise<MessageToken[]>;
37
+ private getEmotes;
38
+ private fetchTwitch;
39
+ private fetch7TV;
40
+ private fetchBTTV;
41
+ private fetchFFZ;
42
+ /** Invalidate the cache — call this if emotes are updated mid-stream */
43
+ invalidate(): void;
44
+ }
45
+ //# sourceMappingURL=EmoteResolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EmoteResolver.d.ts","sourceRoot":"","sources":["../src/EmoteResolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,aAAa,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAWhF;;;;;;;;GAQG;AACH,qBAAa,aAAa;IAKtB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAJzB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgC;IACtD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAoB;gBAG3B,MAAM,EAAE;QACvB,iCAAiC;QACjC,SAAS,EAAE,MAAM,CAAA;QACjB,qCAAqC;QACrC,cAAc,CAAC,EAAE,MAAM,CAAA;QACvB,iBAAiB,CAAC,EAAE,MAAM,CAAA;QAC1B,yDAAyD;QACzD,SAAS,CAAC,EAAE,aAAa,EAAE,CAAA;KAC5B;IAKH;;OAEG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAI7B;;;;;;;OAOG;IACG,OAAO,CACX,IAAI,EAAE,MAAM,EACZ,iBAAiB,CAAC,EAAE,MAAM,GACzB,OAAO,CAAC,YAAY,EAAE,CAAC;YAmDZ,SAAS;YAgCT,WAAW;YAiCX,QAAQ;YAgCR,SAAS;YA4BT,QAAQ;IAkCtB,wEAAwE;IACxE,UAAU,IAAI,IAAI;CAGnB"}
@@ -0,0 +1,237 @@
1
+ const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
2
+ /**
3
+ * Fetches and caches emotes from all configured platforms,
4
+ * then resolves chat message text into typed tokens.
5
+ *
6
+ * Usage:
7
+ * const resolver = new EmoteResolver({ channelId: '...', twitchClientId: '...' })
8
+ * await resolver.warmup()
9
+ * const tokens = await resolver.resolve(messageText, twitchEmoteData)
10
+ */
11
+ export class EmoteResolver {
12
+ config;
13
+ cache = new Map();
14
+ platforms;
15
+ constructor(config) {
16
+ this.config = config;
17
+ this.platforms = new Set(config.platforms ?? ['twitch', '7tv', 'bttv', 'ffz']);
18
+ }
19
+ /**
20
+ * Pre-fetch all emote sets. Call this after connecting to warm the cache.
21
+ */
22
+ async warmup() {
23
+ await this.getEmotes();
24
+ }
25
+ /**
26
+ * Resolve a message string into typed tokens, injecting emote objects
27
+ * where emote names are found in the text.
28
+ *
29
+ * @param text Raw message text
30
+ * @param twitchEmoteRanges Optional raw Twitch emote positions from IRC
31
+ * (format: "id:start-end,start-end/id:...")
32
+ */
33
+ async resolve(text, twitchEmoteRanges) {
34
+ const emotes = await this.getEmotes();
35
+ // Build a set of Twitch emote positions from IRC metadata if available
36
+ const twitchPositions = parseTwitchEmoteRanges(twitchEmoteRanges ?? '');
37
+ const tokens = [];
38
+ const words = text.split(' ');
39
+ let charPos = 0;
40
+ for (let i = 0; i < words.length; i++) {
41
+ const word = words[i] ?? '';
42
+ const wordEnd = charPos + word.length;
43
+ // Check if this word is at a known Twitch emote position
44
+ const twitchEmoteId = twitchPositions.get(charPos);
45
+ if (twitchEmoteId) {
46
+ const twitchEmote = emotes.get(`twitch:${twitchEmoteId}`) ?? emotes.get(`twitch:name:${word}`);
47
+ if (twitchEmote) {
48
+ tokens.push({ type: 'emote', emote: twitchEmote });
49
+ charPos = wordEnd + 1;
50
+ continue;
51
+ }
52
+ }
53
+ // Check by name across all platforms
54
+ const emote = emotes.get(word);
55
+ if (emote) {
56
+ tokens.push({ type: 'emote', emote });
57
+ }
58
+ else if (word.startsWith('@')) {
59
+ tokens.push({ type: 'mention', username: word.slice(1) });
60
+ }
61
+ else if (word.startsWith('http://') || word.startsWith('https://')) {
62
+ tokens.push({ type: 'url', href: word, display: word });
63
+ }
64
+ else {
65
+ // Merge consecutive text tokens
66
+ const last = tokens[tokens.length - 1];
67
+ if (last?.type === 'text') {
68
+ last.value += ` ${word}`;
69
+ }
70
+ else {
71
+ tokens.push({ type: 'text', value: word });
72
+ }
73
+ }
74
+ charPos = wordEnd + 1;
75
+ }
76
+ return tokens;
77
+ }
78
+ // ─── Emote fetching ───────────────────────────────────────────────────────
79
+ async getEmotes() {
80
+ const cacheKey = this.config.channelId;
81
+ const cached = this.cache.get(cacheKey);
82
+ if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
83
+ return cached.emotes;
84
+ }
85
+ const results = await Promise.allSettled([
86
+ this.platforms.has('twitch') ? this.fetchTwitch() : Promise.resolve([]),
87
+ this.platforms.has('7tv') ? this.fetch7TV() : Promise.resolve([]),
88
+ this.platforms.has('bttv') ? this.fetchBTTV() : Promise.resolve([]),
89
+ this.platforms.has('ffz') ? this.fetchFFZ() : Promise.resolve([]),
90
+ ]);
91
+ const emoteMap = new Map();
92
+ for (const result of results) {
93
+ if (result.status === 'fulfilled') {
94
+ for (const emote of result.value) {
95
+ // Primary key: emote name (for text matching)
96
+ emoteMap.set(emote.name, emote);
97
+ // Secondary key: platform:id (for IRC position matching)
98
+ emoteMap.set(`${emote.platform}:${emote.id}`, emote);
99
+ }
100
+ }
101
+ }
102
+ this.cache.set(cacheKey, { emotes: emoteMap, fetchedAt: Date.now() });
103
+ return emoteMap;
104
+ }
105
+ async fetchTwitch() {
106
+ if (!this.config.twitchClientId || !this.config.twitchAccessToken)
107
+ return [];
108
+ const res = await fetch(`https://api.twitch.tv/helix/chat/emotes?broadcaster_id=${this.config.channelId}`, {
109
+ headers: {
110
+ 'Client-Id': this.config.twitchClientId,
111
+ Authorization: `Bearer ${this.config.twitchAccessToken}`,
112
+ },
113
+ });
114
+ if (!res.ok)
115
+ return [];
116
+ const data = await res.json();
117
+ return data.data.map((e) => {
118
+ const emote = e;
119
+ const images = (emote['images'] ?? {});
120
+ return {
121
+ id: String(emote['id'] ?? ''),
122
+ name: String(emote['name'] ?? ''),
123
+ platform: 'twitch',
124
+ animated: String(emote['format'] ?? '').includes('animated'),
125
+ urls: {
126
+ x1: images['url_1x'] ?? '',
127
+ x2: images['url_2x'] ?? '',
128
+ x4: images['url_4x'] ?? '',
129
+ },
130
+ };
131
+ });
132
+ }
133
+ async fetch7TV() {
134
+ const res = await fetch(`https://7tv.io/v3/users/twitch/${this.config.channelId}`);
135
+ if (!res.ok)
136
+ return [];
137
+ const data = await res.json();
138
+ const emoteSet = (data['emote_set'] ?? {});
139
+ const emotes = (emoteSet['emotes'] ?? []);
140
+ return emotes.map((e) => {
141
+ const emote = e;
142
+ const emoteData = (emote['data'] ?? {});
143
+ const host = (emoteData['host'] ?? {});
144
+ const hostUrl = String(host['url'] ?? '');
145
+ const files = (host['files'] ?? []);
146
+ const bySize = (size) => {
147
+ const f = files.find((f) => f['name'] === `${size}.webp`);
148
+ return f ? `https:${hostUrl}/${f['name']}` : '';
149
+ };
150
+ return {
151
+ id: String(emote['id'] ?? ''),
152
+ name: String(emote['name'] ?? ''),
153
+ platform: '7tv',
154
+ animated: Boolean(emoteData['animated']),
155
+ urls: { x1: bySize('1x'), x2: bySize('2x'), x4: bySize('4x') },
156
+ };
157
+ });
158
+ }
159
+ async fetchBTTV() {
160
+ const res = await fetch(`https://api.betterttv.net/3/cached/users/twitch/${this.config.channelId}`);
161
+ if (!res.ok)
162
+ return [];
163
+ const data = await res.json();
164
+ const channelEmotes = (data['channelEmotes'] ?? []);
165
+ const sharedEmotes = (data['sharedEmotes'] ?? []);
166
+ const all = [...channelEmotes, ...sharedEmotes];
167
+ return all.map((e) => {
168
+ const emote = e;
169
+ const id = String(emote['id'] ?? '');
170
+ return {
171
+ id,
172
+ name: String(emote['code'] ?? ''),
173
+ platform: 'bttv',
174
+ animated: String(emote['imageType'] ?? '') === 'gif',
175
+ urls: {
176
+ x1: `https://cdn.betterttv.net/emote/${id}/1x`,
177
+ x2: `https://cdn.betterttv.net/emote/${id}/2x`,
178
+ x4: `https://cdn.betterttv.net/emote/${id}/3x`,
179
+ },
180
+ };
181
+ });
182
+ }
183
+ async fetchFFZ() {
184
+ const res = await fetch(`https://api.frankerfacez.com/v1/room/id/${this.config.channelId}`);
185
+ if (!res.ok)
186
+ return [];
187
+ const data = await res.json();
188
+ const sets = (data['sets'] ?? {});
189
+ const emotes = [];
190
+ for (const set of Object.values(sets)) {
191
+ const setData = set;
192
+ const setEmotes = (setData['emoticons'] ?? []);
193
+ for (const e of setEmotes) {
194
+ const emote = e;
195
+ const urls = (emote['urls'] ?? {});
196
+ emotes.push({
197
+ id: String(emote['id'] ?? ''),
198
+ name: String(emote['name'] ?? ''),
199
+ platform: 'ffz',
200
+ animated: false, // FFZ doesn't support animated emotes
201
+ urls: {
202
+ x1: urls['1'] ? `https:${urls['1']}` : '',
203
+ x2: urls['2'] ? `https:${urls['2']}` : '',
204
+ x4: urls['4'] ? `https:${urls['4']}` : '',
205
+ },
206
+ });
207
+ }
208
+ }
209
+ return emotes;
210
+ }
211
+ /** Invalidate the cache — call this if emotes are updated mid-stream */
212
+ invalidate() {
213
+ this.cache.delete(this.config.channelId);
214
+ }
215
+ }
216
+ // ─── Twitch IRC emote range parser ────────────────────────────────────────────
217
+ /**
218
+ * Parses Twitch IRC emote metadata into a map of startPosition → emoteId.
219
+ * Format: "emoteId:start-end,start-end/emoteId:start-end"
220
+ */
221
+ function parseTwitchEmoteRanges(raw) {
222
+ const positions = new Map();
223
+ if (!raw)
224
+ return positions;
225
+ for (const part of raw.split('/')) {
226
+ const [id, ranges] = part.split(':');
227
+ if (!id || !ranges)
228
+ continue;
229
+ for (const range of ranges.split(',')) {
230
+ const [start] = range.split('-');
231
+ if (start !== undefined)
232
+ positions.set(Number(start), id);
233
+ }
234
+ }
235
+ return positions;
236
+ }
237
+ //# sourceMappingURL=EmoteResolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EmoteResolver.js","sourceRoot":"","sources":["../src/EmoteResolver.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export { EmoteResolver } from './EmoteResolver.js';
2
+ export { tokensToHtml } from './tokensToHtml.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { EmoteResolver } from './EmoteResolver.js';
2
+ export { tokensToHtml } from './tokensToHtml.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA"}
@@ -0,0 +1,19 @@
1
+ import type { MessageToken } from '@overlive/core';
2
+ /**
3
+ * Render parsed chat message tokens to an XSS-safe HTML string.
4
+ *
5
+ * The output is suitable for direct insertion into a chat overlay via
6
+ * `innerHTML` or React's `dangerouslySetInnerHTML`. All user-supplied
7
+ * strings are escaped; emote URLs and mention/url tokens go through the
8
+ * same escape pipeline so even a maliciously crafted emote name can't
9
+ * inject markup.
10
+ *
11
+ * Emit shape (mostly stable CSS classes you can style):
12
+ * text: plain escaped text run
13
+ * mention: <span class="overlive-mention">@user</span>
14
+ * url: <a class="overlive-url" href="..." rel="noreferrer">display</a>
15
+ * cheer: <span class="overlive-cheer">123</span>
16
+ * emote: <img class="overlive-emote" src="..." alt="name" title="name" />
17
+ */
18
+ export declare function tokensToHtml(tokens: MessageToken[], fallbackText?: string): string;
19
+ //# sourceMappingURL=tokensToHtml.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokensToHtml.d.ts","sourceRoot":"","sources":["../src/tokensToHtml.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAElD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,YAAY,EAAE,EAAE,YAAY,SAAK,GAAG,MAAM,CA+B9E"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Render parsed chat message tokens to an XSS-safe HTML string.
3
+ *
4
+ * The output is suitable for direct insertion into a chat overlay via
5
+ * `innerHTML` or React's `dangerouslySetInnerHTML`. All user-supplied
6
+ * strings are escaped; emote URLs and mention/url tokens go through the
7
+ * same escape pipeline so even a maliciously crafted emote name can't
8
+ * inject markup.
9
+ *
10
+ * Emit shape (mostly stable CSS classes you can style):
11
+ * text: plain escaped text run
12
+ * mention: <span class="overlive-mention">@user</span>
13
+ * url: <a class="overlive-url" href="..." rel="noreferrer">display</a>
14
+ * cheer: <span class="overlive-cheer">123</span>
15
+ * emote: <img class="overlive-emote" src="..." alt="name" title="name" />
16
+ */
17
+ export function tokensToHtml(tokens, fallbackText = '') {
18
+ if (!tokens || tokens.length === 0)
19
+ return esc(fallbackText);
20
+ const parts = [];
21
+ for (const t of tokens) {
22
+ switch (t.type) {
23
+ case 'text':
24
+ parts.push(esc(t.value));
25
+ break;
26
+ case 'mention':
27
+ parts.push(`<span class="overlive-mention">@${esc(t.username)}</span>`);
28
+ break;
29
+ case 'url':
30
+ parts.push(`<a class="overlive-url" href="${esc(t.href)}" rel="noreferrer">${esc(t.display)}</a>`);
31
+ break;
32
+ case 'cheer':
33
+ parts.push(`<span class="overlive-cheer">${Number(t.amount) || 0}</span>`);
34
+ break;
35
+ case 'emote': {
36
+ const e = t.emote;
37
+ const url = e.urls?.x2 ?? e.urls?.x1 ?? '';
38
+ if (!url) {
39
+ parts.push(esc(e.name));
40
+ break;
41
+ }
42
+ parts.push(`<img class="overlive-emote" src="${esc(url)}" alt="${esc(e.name)}" title="${esc(e.name)}" />`);
43
+ break;
44
+ }
45
+ }
46
+ }
47
+ return parts.join(' ');
48
+ }
49
+ function esc(s) {
50
+ return s.replace(/[&<>"']/g, (c) => HTML_ESCAPES[c]);
51
+ }
52
+ const HTML_ESCAPES = {
53
+ '&': '&amp;',
54
+ '<': '&lt;',
55
+ '>': '&gt;',
56
+ '"': '&quot;',
57
+ "'": '&#39;',
58
+ };
59
+ //# sourceMappingURL=tokensToHtml.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokensToHtml.js","sourceRoot":"","sources":["../src/tokensToHtml.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,YAAY,CAAC,MAAsB,EAAE,YAAY,GAAG,EAAE;IACpE,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,GAAG,CAAC,YAAY,CAAC,CAAA;IAC5D,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACvB,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC;YACf,KAAK,MAAM;gBACT,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAA;gBACxB,MAAK;YACP,KAAK,SAAS;gBACZ,KAAK,CAAC,IAAI,CAAC,mCAAmC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAA;gBACvE,MAAK;YACP,KAAK,KAAK;gBACR,KAAK,CAAC,IAAI,CACR,iCAAiC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,sBAAsB,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CACvF,CAAA;gBACD,MAAK;YACP,KAAK,OAAO;gBACV,KAAK,CAAC,IAAI,CAAC,gCAAgC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;gBAC1E,MAAK;YACP,KAAK,OAAO,CAAC,CAAC,CAAC;gBACb,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,CAAA;gBACjB,MAAM,GAAG,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,CAAA;gBAC1C,IAAI,CAAC,GAAG,EAAE,CAAC;oBAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;oBAAC,MAAK;gBAAC,CAAC;gBAC5C,KAAK,CAAC,IAAI,CACR,oCAAoC,GAAG,CAAC,GAAG,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAC/F,CAAA;gBACD,MAAK;YACP,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AACxB,CAAC;AAED,SAAS,GAAG,CAAC,CAAS;IACpB,OAAO,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,CAAC,CAAE,CAAC,CAAA;AACvD,CAAC;AAED,MAAM,YAAY,GAA2B;IAC3C,GAAG,EAAE,OAAO;IACZ,GAAG,EAAE,MAAM;IACX,GAAG,EAAE,MAAM;IACX,GAAG,EAAE,QAAQ;IACb,GAAG,EAAE,OAAO;CACb,CAAA"}
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@overlive/emotes",
3
+ "version": "0.1.0",
4
+ "description": "Emote resolution for Twitch, 7TV, BTTV, and FFZ",
5
+ "license": "UNLICENSED",
6
+ "author": "fennsorenn",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/fennsorenn/overlive.git",
10
+ "directory": "packages/emotes"
11
+ },
12
+ "type": "module",
13
+ "main": "./dist/index.cjs",
14
+ "module": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "import": "./dist/index.js",
19
+ "require": "./dist/index.cjs",
20
+ "types": "./dist/index.d.ts"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "README.md"
26
+ ],
27
+ "dependencies": {
28
+ "@overlive/core": "0.1.0"
29
+ },
30
+ "devDependencies": {
31
+ "typescript": "^5.4.0",
32
+ "vitest": "^1.6.0"
33
+ },
34
+ "scripts": {
35
+ "build": "tsc -p tsconfig.json",
36
+ "dev": "tsc -p tsconfig.json --watch",
37
+ "test": "vitest run",
38
+ "test:watch": "vitest",
39
+ "typecheck": "tsc --noEmit",
40
+ "clean": "rm -rf dist"
41
+ }
42
+ }