@fugood/bricks-project 2.24.0-beta.2 → 2.24.0-beta.21

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.
Files changed (110) hide show
  1. package/compile/action-name-map.ts +14 -0
  2. package/compile/index.ts +377 -129
  3. package/package.json +8 -3
  4. package/skills/bricks-project/rules/architecture-patterns.md +7 -0
  5. package/skills/bricks-project/rules/automations.md +74 -28
  6. package/skills/bricks-project/rules/buttress.md +9 -6
  7. package/tools/deploy.ts +39 -10
  8. package/tools/mcp-server.ts +10 -877
  9. package/tools/mcp-tools/compile.ts +91 -0
  10. package/tools/mcp-tools/huggingface.ts +762 -0
  11. package/tools/mcp-tools/icons.ts +60 -0
  12. package/tools/mcp-tools/lottie.ts +102 -0
  13. package/tools/mcp-tools/media.ts +110 -0
  14. package/tools/postinstall.ts +121 -33
  15. package/tools/preview-main.mjs +12 -8
  16. package/tools/pull.ts +37 -19
  17. package/tsconfig.json +16 -0
  18. package/types/animation.ts +4 -0
  19. package/types/automation.ts +3 -0
  20. package/types/brick-base.ts +1 -1
  21. package/types/bricks/Camera.ts +34 -7
  22. package/types/bricks/Chart.ts +1 -1
  23. package/types/bricks/GenerativeMedia.ts +6 -6
  24. package/types/bricks/Icon.ts +3 -3
  25. package/types/bricks/Image.ts +4 -4
  26. package/types/bricks/Items.ts +7 -7
  27. package/types/bricks/Lottie.ts +4 -4
  28. package/types/bricks/Maps.ts +4 -4
  29. package/types/bricks/QrCode.ts +4 -4
  30. package/types/bricks/Rect.ts +4 -4
  31. package/types/bricks/RichText.ts +3 -3
  32. package/types/bricks/Rive.ts +1 -1
  33. package/types/bricks/Slideshow.ts +4 -4
  34. package/types/bricks/Svg.ts +3 -3
  35. package/types/bricks/Text.ts +4 -4
  36. package/types/bricks/TextInput.ts +11 -7
  37. package/types/bricks/Video.ts +4 -4
  38. package/types/bricks/VideoStreaming.ts +3 -3
  39. package/types/bricks/WebRtcStream.ts +1 -1
  40. package/types/bricks/WebView.ts +4 -4
  41. package/types/canvas.ts +4 -2
  42. package/types/common.ts +9 -4
  43. package/types/data-calc-command.ts +2 -0
  44. package/types/data-calc.ts +1 -0
  45. package/types/data.ts +2 -0
  46. package/types/generators/AlarmClock.ts +5 -5
  47. package/types/generators/Assistant.ts +57 -12
  48. package/types/generators/BleCentral.ts +12 -4
  49. package/types/generators/BlePeripheral.ts +5 -5
  50. package/types/generators/CanvasMap.ts +4 -4
  51. package/types/generators/CastlesPay.ts +3 -3
  52. package/types/generators/DataBank.ts +31 -4
  53. package/types/generators/File.ts +63 -14
  54. package/types/generators/GraphQl.ts +3 -3
  55. package/types/generators/Http.ts +27 -8
  56. package/types/generators/HttpServer.ts +9 -9
  57. package/types/generators/Information.ts +2 -2
  58. package/types/generators/Intent.ts +8 -2
  59. package/types/generators/Iterator.ts +6 -6
  60. package/types/generators/Keyboard.ts +18 -8
  61. package/types/generators/LlmAnthropicCompat.ts +12 -6
  62. package/types/generators/LlmAppleBuiltin.ts +6 -6
  63. package/types/generators/LlmGgml.ts +75 -25
  64. package/types/generators/LlmMlx.ts +210 -0
  65. package/types/generators/LlmOnnx.ts +18 -9
  66. package/types/generators/LlmOpenAiCompat.ts +22 -6
  67. package/types/generators/LlmQualcommAiEngine.ts +32 -8
  68. package/types/generators/Mcp.ts +332 -17
  69. package/types/generators/McpServer.ts +38 -11
  70. package/types/generators/MediaFlow.ts +26 -8
  71. package/types/generators/MqttBroker.ts +10 -4
  72. package/types/generators/MqttClient.ts +11 -5
  73. package/types/generators/Question.ts +6 -6
  74. package/types/generators/RealtimeTranscription.ts +70 -11
  75. package/types/generators/RerankerGgml.ts +23 -9
  76. package/types/generators/SerialPort.ts +6 -6
  77. package/types/generators/SoundPlayer.ts +2 -2
  78. package/types/generators/SoundRecorder.ts +5 -5
  79. package/types/generators/SpeechToTextGgml.ts +34 -14
  80. package/types/generators/SpeechToTextOnnx.ts +8 -8
  81. package/types/generators/SpeechToTextPlatform.ts +4 -4
  82. package/types/generators/SqLite.ts +10 -6
  83. package/types/generators/Step.ts +3 -3
  84. package/types/generators/SttAppleBuiltin.ts +6 -6
  85. package/types/generators/Tcp.ts +5 -5
  86. package/types/generators/TcpServer.ts +7 -7
  87. package/types/generators/TextToSpeechApple.ts +1 -1
  88. package/types/generators/TextToSpeechAppleBuiltin.ts +5 -5
  89. package/types/generators/TextToSpeechGgml.ts +8 -8
  90. package/types/generators/TextToSpeechOnnx.ts +9 -9
  91. package/types/generators/TextToSpeechOpenAiLike.ts +5 -5
  92. package/types/generators/ThermalPrinter.ts +6 -6
  93. package/types/generators/Tick.ts +3 -3
  94. package/types/generators/Udp.ts +9 -4
  95. package/types/generators/VadGgml.ts +39 -10
  96. package/types/generators/VadOnnx.ts +31 -8
  97. package/types/generators/VadTraditional.ts +15 -9
  98. package/types/generators/VectorStore.ts +26 -9
  99. package/types/generators/Watchdog.ts +11 -6
  100. package/types/generators/WebCrawler.ts +5 -5
  101. package/types/generators/WebRtc.ts +17 -11
  102. package/types/generators/WebSocket.ts +5 -5
  103. package/types/generators/index.ts +1 -0
  104. package/types/subspace.ts +1 -0
  105. package/types/system.ts +1 -1
  106. package/utils/calc.ts +12 -8
  107. package/utils/event-props.ts +104 -87
  108. package/utils/id.ts +4 -0
  109. package/api/index.ts +0 -1
  110. package/api/instance.ts +0 -213
@@ -1,106 +1,11 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
3
- import { z } from 'zod'
4
- import { $, JSON5 } from 'bun'
5
- import * as TOON from '@toon-format/toon'
6
- import Fuse from 'fuse.js'
7
- import { gguf } from '@huggingface/gguf'
8
- import glyphmap from './icons/fa6pro-glyphmap.json'
9
- import glyphmapMeta from './icons/fa6pro-meta.json'
10
3
 
11
- // Hugging Face API configuration
12
- const HF_API_URL = 'https://huggingface.co/api'
13
- const { HF_TOKEN } = process.env
14
-
15
- // Helper function to convert BigInt to number for JSON serialization
16
- const convertBigIntToNumber = (value: unknown): number | unknown => {
17
- if (typeof value === 'bigint') {
18
- return Number(value)
19
- }
20
- return value
21
- }
22
-
23
- // Extract GGUF metadata from a GGUF file URL with 10s timeout
24
- const extractGGUFMetadata = async (url: string) => {
25
- const timeoutPromise = new Promise<never>((_, reject) =>
26
- setTimeout(() => reject(new Error('GGUF metadata extraction timeout')), 10000),
27
- )
28
-
29
- try {
30
- const { metadata } = (await Promise.race([gguf(url), timeoutPromise])) as { metadata: any }
31
- const architecture = metadata['general.architecture']
32
-
33
- const ggufSimplifiedMetadata: Record<string, unknown> = {
34
- name: metadata['general.name'],
35
- size_label: metadata['general.size_label'],
36
- basename: metadata['general.basename'],
37
- architecture: metadata['general.architecture'],
38
- file_type: convertBigIntToNumber(metadata['general.file_type']),
39
- quantization_version: convertBigIntToNumber(metadata['general.quantization_version']),
40
- }
41
-
42
- if (!architecture) return ggufSimplifiedMetadata
43
-
44
- // Helper to add converted value if defined
45
- const addIfDefined = (target: Record<string, unknown>, key: string, value: unknown) => {
46
- if (value !== undefined) target[key] = convertBigIntToNumber(value)
47
- }
48
-
49
- // Architecture-specific transformer parameters
50
- addIfDefined(ggufSimplifiedMetadata, 'n_ctx_train', metadata[`${architecture}.context_length`])
51
- addIfDefined(ggufSimplifiedMetadata, 'n_layer', metadata[`${architecture}.block_count`])
52
- addIfDefined(ggufSimplifiedMetadata, 'n_embd', metadata[`${architecture}.embedding_length`])
53
- addIfDefined(ggufSimplifiedMetadata, 'n_head', metadata[`${architecture}.attention.head_count`])
54
- addIfDefined(
55
- ggufSimplifiedMetadata,
56
- 'n_head_kv',
57
- metadata[`${architecture}.attention.head_count_kv`],
58
- )
59
- addIfDefined(
60
- ggufSimplifiedMetadata,
61
- 'n_embd_head_k',
62
- metadata[`${architecture}.attention.key_length`],
63
- )
64
- addIfDefined(
65
- ggufSimplifiedMetadata,
66
- 'n_embd_head_v',
67
- metadata[`${architecture}.attention.value_length`],
68
- )
69
- addIfDefined(
70
- ggufSimplifiedMetadata,
71
- 'n_swa',
72
- metadata[`${architecture}.attention.sliding_window`],
73
- )
74
-
75
- // SSM (Mamba) parameters for recurrent/hybrid models
76
- addIfDefined(ggufSimplifiedMetadata, 'ssm_d_conv', metadata[`${architecture}.ssm.conv_kernel`])
77
- addIfDefined(ggufSimplifiedMetadata, 'ssm_d_state', metadata[`${architecture}.ssm.state_size`])
78
- addIfDefined(ggufSimplifiedMetadata, 'ssm_d_inner', metadata[`${architecture}.ssm.inner_size`])
79
- addIfDefined(ggufSimplifiedMetadata, 'ssm_n_group', metadata[`${architecture}.ssm.group_count`])
80
- addIfDefined(
81
- ggufSimplifiedMetadata,
82
- 'ssm_dt_rank',
83
- metadata[`${architecture}.ssm.time_step_rank`],
84
- )
85
-
86
- // RWKV parameters
87
- addIfDefined(
88
- ggufSimplifiedMetadata,
89
- 'rwkv_head_size',
90
- metadata[`${architecture}.rwkv.head_size`],
91
- )
92
- addIfDefined(
93
- ggufSimplifiedMetadata,
94
- 'rwkv_token_shift_count',
95
- metadata[`${architecture}.rwkv.token_shift_count`],
96
- )
97
-
98
- return ggufSimplifiedMetadata
99
- } catch (error) {
100
- console.error('Failed to extract GGUF metadata:', error)
101
- return null
102
- }
103
- }
4
+ import { register as registerCompile } from './mcp-tools/compile'
5
+ import { register as registerLottie } from './mcp-tools/lottie'
6
+ import { register as registerIcons } from './mcp-tools/icons'
7
+ import { register as registerHuggingface } from './mcp-tools/huggingface'
8
+ import { register as registerMedia } from './mcp-tools/media'
104
9
 
105
10
  const server = new McpServer({
106
11
  name: 'bricks-project',
@@ -110,786 +15,14 @@ const server = new McpServer({
110
15
  const { dirname } = import.meta
111
16
  const projectDir = String(dirname).split('/node_modules/')[0]
112
17
 
113
- server.tool('compile', {}, async () => {
114
- let log = ''
115
- try {
116
- log += 'Type checking & Compiling...'
117
- log += await $`bun compile`.cwd(projectDir).text()
118
- } catch (err) {
119
- log += `${err.stdout.toString()}\n${err.stderr.toString()}`
120
- }
121
- return {
122
- content: [{ type: 'text', text: log }],
123
- }
124
- })
125
-
126
18
  // NOTE: Cursor (Or VSCode) seems set ELECTRON_RUN_AS_NODE to 1, so we need to unset it
127
19
  process.env.ELECTRON_RUN_AS_NODE = ''
128
20
 
129
- server.tool(
130
- 'preview',
131
- {
132
- delay: z
133
- .number()
134
- .describe('Delay in milliseconds before taking screenshot')
135
- .optional()
136
- .default(3000),
137
- width: z.number().describe('Width of the screenshot').optional().default(600),
138
- height: z.number().optional().default(480),
139
- responseImage: z
140
- .boolean()
141
- .describe(
142
- 'Whether to response image content (base64 encoded jpeg). If false, only saved path will be responded as text.',
143
- )
144
- .optional()
145
- .default(false),
146
- testId: z.string().describe('Automation test ID to trigger').optional(),
147
- testTitleLike: z
148
- .string()
149
- .describe('Find automation test by partial title match (case-insensitive)')
150
- .optional(),
151
- } as any,
152
- async ({ delay, width, height, responseImage, testId, testTitleLike }: any) => {
153
- let log = ''
154
- let error = false
155
- try {
156
- const args = [
157
- '--no-keep-open',
158
- '--take-screenshot',
159
- JSON.stringify({
160
- delay,
161
- width,
162
- height,
163
- path: `${dirname}/screenshot.jpg`,
164
- closeAfter: true,
165
- headless: true,
166
- }),
167
- ]
168
- if (testId) args.push('--test-id', testId)
169
- if (testTitleLike) args.push('--test-title-like', testTitleLike)
170
- log = await $`bunx --bun electron ${dirname}/preview-main.mjs ${args}`.cwd(projectDir).text()
171
- } catch (err) {
172
- log = `${err.stdout.toString()}\n${err.stderr.toString()}`
173
- error = true
174
- }
175
- let screenshotBase64: any = null
176
- if (!error && responseImage) {
177
- const screenshot = await Bun.file(`${dirname}/screenshot.jpg`).arrayBuffer()
178
- screenshotBase64 = Buffer.from(screenshot).toString('base64')
179
- }
180
- return {
181
- content: [
182
- { type: 'text', text: log },
183
- screenshotBase64 && {
184
- type: 'image',
185
- data: screenshotBase64,
186
- mimeType: 'image/jpeg',
187
- },
188
- ].filter(Boolean),
189
- }
190
- },
191
- )
192
-
193
- // LottieFiles API tools
194
- const LOTTIEFILES_API_URL = 'https://lottiefiles.com/api'
195
-
196
- server.tool(
197
- 'lottie_search',
198
- {
199
- query: z.string().describe('Search keywords for animations'),
200
- page: z.number().min(1).optional().default(1),
201
- limit: z.number().min(1).max(100).optional().default(10),
202
- },
203
- async ({ query, page, limit }) => {
204
- try {
205
- const url = new URL(`${LOTTIEFILES_API_URL}/search/get-animations`)
206
- url.searchParams.set('query', query)
207
- url.searchParams.set('page', String(page))
208
- url.searchParams.set('limit', String(limit))
209
- url.searchParams.set('format', 'json')
210
-
211
- const response = await fetch(url.toString())
212
- const data = await response.json()
213
- const animations = data?.data?.data ?? []
214
-
215
- return {
216
- content: [
217
- {
218
- type: 'text',
219
- text: TOON.encode({ count: animations.length, animations }),
220
- },
221
- ],
222
- }
223
- } catch (err: any) {
224
- return {
225
- content: [{ type: 'text', text: `Failed to search animations: ${err.message}` }],
226
- }
227
- }
228
- },
229
- )
230
-
231
- server.tool(
232
- 'lottie_get_details',
233
- {
234
- id: z.string().describe('Animation file ID'),
235
- },
236
- async ({ id }) => {
237
- try {
238
- const url = new URL(`${LOTTIEFILES_API_URL}/animations/get-animation-data`)
239
- url.searchParams.set('fileId', id)
240
- url.searchParams.set('format', 'json')
241
-
242
- const response = await fetch(url.toString())
243
- const data = await response.json()
244
-
245
- return {
246
- content: [{ type: 'text', text: TOON.encode(data) }],
247
- }
248
- } catch (err: any) {
249
- return {
250
- content: [{ type: 'text', text: `Failed to get animation: ${err.message}` }],
251
- }
252
- }
253
- },
254
- )
255
-
256
- server.tool(
257
- 'lottie_popular',
258
- {
259
- page: z.number().min(1).optional().default(1),
260
- limit: z.number().min(1).max(100).optional().default(10),
261
- },
262
- async ({ page, limit }) => {
263
- try {
264
- const url = new URL(
265
- `${LOTTIEFILES_API_URL}/iconscout/popular-animations-weekly?api=%26sort%3Dpopular`,
266
- )
267
- url.searchParams.set('page', String(page))
268
- url.searchParams.set('limit', String(limit))
269
- url.searchParams.set('format', 'json')
270
-
271
- const response = await fetch(url.toString())
272
- const data = await response.json()
273
- const animations = data?.popularWeeklyData?.data ?? []
274
-
275
- return {
276
- content: [
277
- {
278
- type: 'text',
279
- text: TOON.encode({ count: animations.length, animations }),
280
- },
281
- ],
282
- }
283
- } catch (err: any) {
284
- return {
285
- content: [{ type: 'text', text: `Failed to get popular animations: ${err.message}` }],
286
- }
287
- }
288
- },
289
- )
290
-
291
- // FontAwesome 6 Pro icon search
292
- type IconStyle = 'brands' | 'duotone' | 'light' | 'regular' | 'solid' | 'thin'
293
- const iconMeta = glyphmapMeta as Record<IconStyle, string[]>
294
-
295
- const iconList = Object.entries(glyphmap as Record<string, number>).map(([name, code]) => {
296
- const styles = (Object.keys(iconMeta) as IconStyle[]).filter((style) =>
297
- iconMeta[style].includes(name),
298
- )
299
- return { name, code, styles }
300
- })
301
-
302
- const iconFuse = new Fuse(iconList, {
303
- keys: ['name'],
304
- threshold: 0.3,
305
- includeScore: true,
306
- })
307
-
308
- server.tool(
309
- 'icon_search',
310
- {
311
- query: z.string().describe('Search keywords for FontAwesome 6 Pro icons'),
312
- limit: z.number().min(1).max(100).optional().default(10),
313
- style: z
314
- .enum(['brands', 'duotone', 'light', 'regular', 'solid', 'thin'])
315
- .optional()
316
- .describe('Filter by icon style'),
317
- },
318
- async ({ query, limit, style }) => {
319
- let results = iconFuse.search(query, { limit: style ? limit * 3 : limit })
320
-
321
- if (style) {
322
- results = results.filter((r) => r.item.styles.includes(style)).slice(0, limit)
323
- }
324
-
325
- const icons = results.map((r) => ({
326
- name: r.item.name,
327
- code: r.item.code,
328
- unicode: `U+${r.item.code.toString(16).toUpperCase()}`,
329
- styles: r.item.styles,
330
- score: r.score,
331
- }))
332
-
333
- return {
334
- content: [
335
- {
336
- type: 'text',
337
- text: TOON.encode({ count: icons.length, icons }),
338
- },
339
- ],
340
- }
341
- },
342
- )
343
-
344
- // Hugging Face model search
345
- type HFSibling = {
346
- rfilename: string
347
- size?: number
348
- lfs?: { sha256?: string }
349
- }
350
-
351
- type HFModel = {
352
- id: string
353
- author?: string
354
- downloads?: number
355
- likes?: number
356
- tags?: string[]
357
- pipeline_tag?: string
358
- siblings?: HFSibling[]
359
- config?: { model_type?: string }
360
- cardData?: { model_type?: string }
361
- }
362
-
363
- const supportedLlmTasks = [
364
- 'text-generation',
365
- 'image-text-to-text',
366
- 'text2text-generation',
367
- 'conversational',
368
- ]
369
-
370
- // Generator type configurations for Hugging Face model search
371
- type GeneratorType =
372
- | 'GeneratorLLM'
373
- | 'GeneratorVectorStore'
374
- | 'GeneratorReranker'
375
- | 'GeneratorGGMLTTS'
376
- | 'GeneratorGGMLTTSVocoder'
377
- | 'GeneratorOnnxLLM'
378
- | 'GeneratorOnnxSTT'
379
- | 'GeneratorTTS'
380
-
381
- type ModelKind = 'gguf' | 'onnx'
382
-
383
- interface GeneratorConfig {
384
- modelKind: ModelKind
385
- filter: string
386
- taskFilter?: string[]
387
- filePattern?: RegExp
388
- hasValidStructure?: (siblings: HFSibling[]) => boolean
389
- }
390
-
391
- // Helper to check valid ONNX structure
392
- const hasValidOnnxStructure = (siblings: HFSibling[]): boolean => {
393
- const hasConfigJson = siblings.some((file) => file.rfilename === 'config.json')
394
- const hasOnnxModel = siblings.some(
395
- (file) => file.rfilename.includes('onnx/') && file.rfilename.endsWith('.onnx'),
396
- )
397
- return hasConfigJson && hasOnnxModel
398
- }
399
-
400
- // Detect quantization types from ONNX files
401
- const detectOnnxQuantizationTypes = (siblings: HFSibling[]): string[] => {
402
- const onnxFiles = siblings.filter((file) => file.rfilename.endsWith('.onnx'))
403
- const quantTypes = new Set<string>()
404
-
405
- onnxFiles.forEach((file) => {
406
- const filename = file.rfilename
407
- if (!filename.endsWith('.onnx')) return
408
- const postfix = /_(q8|q4|q4f16|fp16|int8|int4|uint8|bnb4|quantized)\.onnx$/.exec(filename)?.[1]
409
- if (!postfix) {
410
- quantTypes.add('auto')
411
- quantTypes.add('none')
412
- } else {
413
- quantTypes.add(postfix === 'quantized' ? 'q8' : postfix)
414
- }
415
- })
416
-
417
- return Array.from(quantTypes)
418
- }
419
-
420
- // Find speaker embedding files for TTS models
421
- const findSpeakerEmbedFiles = (siblings: HFSibling[]): HFSibling[] =>
422
- siblings.filter((file) => file.rfilename.startsWith('voices/') && file.rfilename.endsWith('.bin'))
423
-
424
- const generatorConfigs: Record<GeneratorType, GeneratorConfig> = {
425
- GeneratorLLM: {
426
- modelKind: 'gguf',
427
- filter: 'gguf',
428
- taskFilter: supportedLlmTasks,
429
- filePattern: /\.gguf$/,
430
- },
431
- GeneratorVectorStore: {
432
- modelKind: 'gguf',
433
- filter: 'gguf',
434
- filePattern: /\.gguf$/,
435
- },
436
- GeneratorReranker: {
437
- modelKind: 'gguf',
438
- filter: 'gguf,reranker',
439
- filePattern: /\.gguf$/,
440
- },
441
- GeneratorGGMLTTS: {
442
- modelKind: 'gguf',
443
- filter: 'gguf,text-to-speech',
444
- filePattern: /\.gguf$/,
445
- },
446
- GeneratorGGMLTTSVocoder: {
447
- modelKind: 'gguf',
448
- filter: 'gguf,feature-extraction',
449
- filePattern: /\.gguf$/,
450
- },
451
- GeneratorOnnxLLM: {
452
- modelKind: 'onnx',
453
- filter: 'onnx',
454
- taskFilter: supportedLlmTasks,
455
- hasValidStructure: hasValidOnnxStructure,
456
- },
457
- GeneratorOnnxSTT: {
458
- modelKind: 'onnx',
459
- filter: 'onnx,automatic-speech-recognition',
460
- hasValidStructure: hasValidOnnxStructure,
461
- },
462
- GeneratorTTS: {
463
- modelKind: 'onnx',
464
- filter: 'onnx,text-to-speech',
465
- hasValidStructure: hasValidOnnxStructure,
466
- },
467
- }
468
-
469
- const searchHFModels = async (filter: string, search?: string, limit = 50): Promise<HFModel[]> => {
470
- const params = new URLSearchParams({
471
- limit: String(limit),
472
- full: 'true',
473
- config: 'true',
474
- sort: 'likes',
475
- direction: '-1',
476
- })
477
- if (filter) params.set('filter', filter)
478
- if (search) params.set('search', search)
479
-
480
- const headers: Record<string, string> = {}
481
- if (HF_TOKEN) {
482
- headers['Authorization'] = `Bearer ${HF_TOKEN}`
483
- }
484
-
485
- const response = await fetch(`${HF_API_URL}/models?${params.toString()}`, { headers })
486
- if (!response.ok) {
487
- throw new Error(`Hugging Face API error: ${response.status} ${response.statusText}`)
488
- }
489
- return response.json()
490
- }
491
-
492
- const fetchHFModelDetails = async (modelId: string): Promise<HFModel> => {
493
- const params = new URLSearchParams({ blobs: 'true' })
494
-
495
- const headers: Record<string, string> = {}
496
- if (HF_TOKEN) {
497
- headers['Authorization'] = `Bearer ${HF_TOKEN}`
498
- }
499
-
500
- const response = await fetch(`${HF_API_URL}/models/${modelId}?${params.toString()}`, { headers })
501
- if (!response.ok) {
502
- throw new Error(`Hugging Face API error: ${response.status} ${response.statusText}`)
503
- }
504
- return response.json()
505
- }
506
-
507
- server.tool(
508
- 'huggingface_search',
509
- {
510
- generatorType: z
511
- .enum([
512
- 'GeneratorLLM',
513
- 'GeneratorVectorStore',
514
- 'GeneratorReranker',
515
- 'GeneratorGGMLTTS',
516
- 'GeneratorGGMLTTSVocoder',
517
- 'GeneratorOnnxLLM',
518
- 'GeneratorOnnxSTT',
519
- 'GeneratorTTS',
520
- ])
521
- .describe('Generator type to search models for')
522
- .default('GeneratorLLM'),
523
- query: z.string().describe('Search keywords for models on Hugging Face').optional(),
524
- limit: z.number().min(1).max(100).optional().default(20),
525
- includeFiles: z
526
- .boolean()
527
- .optional()
528
- .default(false)
529
- .describe('Include list of model files (requires additional API calls)'),
530
- },
531
- async ({ generatorType, query, limit, includeFiles }) => {
532
- try {
533
- const config = generatorConfigs[generatorType]
534
- const models = await searchHFModels(config.filter, query, limit)
535
-
536
- // Filter models based on generator configuration
537
- const filteredModels = models.filter((model) => {
538
- const modelTags = model.tags || []
539
-
540
- // Check task filter if configured
541
- if (config.taskFilter && !config.taskFilter.some((t) => modelTags.includes(t))) {
542
- return false
543
- }
544
-
545
- // Check structure validation for ONNX models
546
- if (config.hasValidStructure && model.siblings) {
547
- if (!config.hasValidStructure(model.siblings)) {
548
- return false
549
- }
550
- }
551
-
552
- return true
553
- })
554
-
555
- // Build result models
556
- let results: Array<{
557
- id: string
558
- author?: string
559
- downloads?: number
560
- likes?: number
561
- pipeline_tag?: string
562
- model_type?: string
563
- model_kind: ModelKind
564
- files?: Array<{ filename: string; size?: number }>
565
- quantization_types?: string[]
566
- speaker_embed_files?: Array<{ filename: string; size?: number }>
567
- }> = filteredModels.map((model) => ({
568
- id: model.id,
569
- author: model.author,
570
- downloads: model.downloads,
571
- likes: model.likes,
572
- pipeline_tag: model.pipeline_tag,
573
- model_type: model.config?.model_type || model.cardData?.model_type,
574
- model_kind: config.modelKind,
575
- }))
576
-
577
- if (includeFiles) {
578
- results = await Promise.all(
579
- results.map(async (model) => {
580
- try {
581
- const details = await fetchHFModelDetails(model.id)
582
- const siblings = details.siblings || []
583
-
584
- if (config.modelKind === 'gguf') {
585
- const ggufFiles = siblings
586
- .filter((file) => config.filePattern?.test(file.rfilename))
587
- .map((file) => ({
588
- filename: file.rfilename,
589
- size: file.size,
590
- }))
591
- return { ...model, files: ggufFiles }
592
- } else {
593
- // ONNX models
594
- const quantTypes = detectOnnxQuantizationTypes(siblings)
595
- const speakerFiles =
596
- generatorType === 'GeneratorTTS' ? findSpeakerEmbedFiles(siblings) : []
597
-
598
- return {
599
- ...model,
600
- quantization_types: quantTypes,
601
- ...(speakerFiles.length > 0 && {
602
- speaker_embed_files: speakerFiles.map((f) => ({
603
- filename: f.rfilename,
604
- size: f.size,
605
- })),
606
- }),
607
- }
608
- }
609
- } catch {
610
- return model
611
- }
612
- }),
613
- )
614
- }
615
-
616
- return {
617
- content: [
618
- {
619
- type: 'text',
620
- text: TOON.encode({
621
- count: results.length,
622
- generatorType,
623
- modelKind: config.modelKind,
624
- models: results,
625
- hf_token_configured: !!HF_TOKEN,
626
- }),
627
- },
628
- ],
629
- }
630
- } catch (err: any) {
631
- return {
632
- content: [{ type: 'text', text: `Failed to search models: ${err.message}` }],
633
- }
634
- }
635
- },
636
- )
637
-
638
- // Example: Mixtral-8x22B-v0.1.IQ3_XS-00001-of-00005.gguf
639
- const ggufSplitPattern = /-(\d{5})-of-(\d{5})\.gguf$/
640
-
641
- server.tool(
642
- 'huggingface_select',
643
- {
644
- generatorType: z
645
- .enum([
646
- 'GeneratorLLM',
647
- 'GeneratorVectorStore',
648
- 'GeneratorReranker',
649
- 'GeneratorGGMLTTS',
650
- 'GeneratorGGMLTTSVocoder',
651
- 'GeneratorOnnxLLM',
652
- 'GeneratorOnnxSTT',
653
- 'GeneratorTTS',
654
- ])
655
- .describe('Generator type for model selection')
656
- .default('GeneratorLLM'),
657
- // eslint-disable-next-line camelcase
658
- model_id: z
659
- .string()
660
- .describe('Hugging Face model ID (e.g., "unsloth/Llama-3.2-1B-Instruct-GGUF")'),
661
- filename: z.string().describe('Model filename to select (required for GGUF models)').optional(),
662
- quantize_type: z
663
- .string()
664
- .describe('Quantization type for ONNX models (e.g., "q8", "fp16", "auto")')
665
- .optional()
666
- .default('auto'),
667
- speaker_embed_file: z.string().describe('Speaker embedding file for TTS models').optional(),
668
- },
669
- // eslint-disable-next-line camelcase
670
- async ({
671
- generatorType,
672
- model_id: modelId,
673
- filename,
674
- quantize_type: quantizeType,
675
- speaker_embed_file: speakerEmbedFile,
676
- }) => {
677
- try {
678
- const config = generatorConfigs[generatorType]
679
- const details = await fetchHFModelDetails(modelId)
680
- const siblings = details.siblings || []
681
-
682
- // Handle ONNX models
683
- // ONNX generators expect: model (HF model ID), modelType, quantizeType
684
- if (config.modelKind === 'onnx') {
685
- const quantTypes = detectOnnxQuantizationTypes(siblings)
686
- const speakerFiles = generatorType === 'GeneratorTTS' ? findSpeakerEmbedFiles(siblings) : []
687
- const selectedSpeakerFile = speakerEmbedFile
688
- ? siblings.find((f) => f.rfilename === speakerEmbedFile)
689
- : undefined
690
-
691
- const selectedQuantType = quantTypes.includes(quantizeType || 'auto')
692
- ? quantizeType
693
- : 'auto'
694
- const modelType = details.config?.model_type || details.cardData?.model_type
695
-
696
- // Result format matches ONNX generator property names (camelCase)
697
- const result = {
698
- // Primary model fields for generator
699
- model: modelId,
700
- modelType,
701
- quantizeType: selectedQuantType,
702
- // Speaker embedding for TTS generators
703
- ...(selectedSpeakerFile && {
704
- speakerEmbedUrl: `https://huggingface.co/${modelId}/resolve/main/${selectedSpeakerFile.rfilename}?download=true`,
705
- speakerEmbedHash: selectedSpeakerFile.lfs?.sha256,
706
- speakerEmbedHashType: 'sha256',
707
- }),
708
- // Additional info
709
- availableQuantizeTypes: quantTypes,
710
- ...(speakerFiles.length > 0 && {
711
- availableSpeakerEmbedFiles: speakerFiles.map((f) => f.rfilename),
712
- }),
713
- _hfRepoInfo: {
714
- repo: modelId,
715
- model: {
716
- id: details.id,
717
- downloads: details.downloads,
718
- likes: details.likes,
719
- author: details.author,
720
- },
721
- generatorType,
722
- modelType,
723
- quantizeType: selectedQuantType,
724
- ...(selectedSpeakerFile && {
725
- speakerEmbedFile: selectedSpeakerFile.rfilename,
726
- }),
727
- },
728
- }
729
-
730
- // Return JSON for huggingface_select to allow direct parsing by consumers
731
- return {
732
- content: [{ type: 'text', text: JSON5.stringify(result, null, 2) }],
733
- }
734
- }
735
-
736
- // Handle GGUF models
737
- if (!filename) {
738
- // List available GGUF files
739
- const ggufFiles = siblings
740
- .filter((file) => config.filePattern?.test(file.rfilename))
741
- .map((file) => ({
742
- filename: file.rfilename,
743
- size: file.size,
744
- }))
745
-
746
- // Return JSON for huggingface_select to allow direct parsing by consumers
747
- return {
748
- content: [
749
- {
750
- type: 'text',
751
- text: JSON.stringify(
752
- {
753
- error: 'filename is required for GGUF models',
754
- available_files: ggufFiles,
755
- },
756
- null,
757
- 2,
758
- ),
759
- },
760
- ],
761
- }
762
- }
763
-
764
- // Find the selected file
765
- const selectedFile = siblings.find((f) => f.rfilename === filename)
766
- if (!selectedFile) {
767
- return {
768
- content: [{ type: 'text', text: `File "${filename}" not found in model ${modelId}` }],
769
- }
770
- }
771
-
772
- // Check if it's a split file
773
- const matched = filename.match(ggufSplitPattern)
774
- const isSplit = !!matched
775
-
776
- // Find mmproj file if available (only for LLM generators)
777
- const mmprojFile =
778
- generatorType === 'GeneratorLLM'
779
- ? siblings.find((f) => /^mmproj-/i.test(f.rfilename))
780
- : undefined
781
-
782
- // Extract GGUF metadata (for split files, metadata is in the first split)
783
- const metadataFilename = isSplit
784
- ? filename.replace(ggufSplitPattern, '-00001-of-$2.gguf')
785
- : filename
786
- const ggufUrl = `https://huggingface.co/${modelId}/resolve/main/${metadataFilename}`
787
- const ggufSimplifiedMetadata = await extractGGUFMetadata(ggufUrl)
788
-
789
- if (isSplit) {
790
- const [, , splitTotal] = matched!
791
- const splitFiles = Array.from({ length: Number(splitTotal) }, (_, i) => {
792
- const split = String(i + 1).padStart(5, '0')
793
- const splitRFilename = filename.replace(
794
- ggufSplitPattern,
795
- `-${split}-of-${splitTotal}.gguf`,
796
- )
797
- const sibling = siblings.find((sb) => sb.rfilename === splitRFilename)
798
- return {
799
- rfilename: splitRFilename,
800
- size: sibling?.size,
801
- lfs: sibling?.lfs,
802
- }
803
- })
804
-
805
- const first = splitFiles[0]
806
- const rest = splitFiles.slice(1)
807
-
808
- const result = {
809
- url: `https://huggingface.co/${modelId}/resolve/main/${first.rfilename}?download=true`,
810
- hash: first.lfs?.sha256,
811
- hash_type: 'sha256',
812
- _ggufSplitFiles: rest.map((split) => ({
813
- url: `https://huggingface.co/${modelId}/resolve/main/${split.rfilename}?download=true`,
814
- hash: split.lfs?.sha256,
815
- hash_type: 'sha256',
816
- })),
817
- ...(mmprojFile && {
818
- mmproj_url: `https://huggingface.co/${modelId}/resolve/main/${mmprojFile.rfilename}?download=true`,
819
- mmproj_hash: mmprojFile.lfs?.sha256,
820
- mmproj_hash_type: 'sha256',
821
- }),
822
- _hfRepoInfo: {
823
- repo: modelId,
824
- model: {
825
- id: details.id,
826
- downloads: details.downloads,
827
- likes: details.likes,
828
- author: details.author,
829
- },
830
- generatorType,
831
- isSplit: true,
832
- files: splitFiles.map((f) => f.rfilename),
833
- sizes: splitFiles.map((f) => f.size),
834
- size: splitFiles.reduce((acc, f) => acc + (f.size ?? 0), 0),
835
- ...(mmprojFile && {
836
- mmprojFile: mmprojFile.rfilename,
837
- mmprojSize: mmprojFile.size,
838
- }),
839
- ...(ggufSimplifiedMetadata && { ggufSimplifiedMetadata }),
840
- },
841
- }
842
-
843
- // Return JSON for huggingface_select to allow direct parsing by consumers
844
- return {
845
- content: [{ type: 'text', text: JSON5.stringify(result, null, 2) }],
846
- }
847
- } else {
848
- const result = {
849
- url: `https://huggingface.co/${modelId}/resolve/main/${filename}?download=true`,
850
- hash: selectedFile.lfs?.sha256,
851
- hash_type: 'sha256',
852
- ...(mmprojFile && {
853
- mmproj_url: `https://huggingface.co/${modelId}/resolve/main/${mmprojFile.rfilename}?download=true`,
854
- mmproj_hash: mmprojFile.lfs?.sha256,
855
- mmproj_hash_type: 'sha256',
856
- }),
857
- _hfRepoInfo: {
858
- repo: modelId,
859
- model: {
860
- id: details.id,
861
- downloads: details.downloads,
862
- likes: details.likes,
863
- author: details.author,
864
- },
865
- generatorType,
866
- file: filename,
867
- size: selectedFile.size,
868
- ...(mmprojFile && {
869
- mmprojFile: mmprojFile.rfilename,
870
- mmprojSize: mmprojFile.size,
871
- }),
872
- ...(ggufSimplifiedMetadata && { ggufSimplifiedMetadata }),
873
- },
874
- }
875
-
876
- // Return JSON for huggingface_select to allow direct parsing by consumers
877
- return {
878
- content: [
879
- {
880
- type: 'text',
881
- text: `${JSON5.stringify(result, null, 2)} // Please use _hfRepoInfo undocumented field with @ts-ignore`,
882
- },
883
- ],
884
- }
885
- }
886
- } catch (err: any) {
887
- return {
888
- content: [{ type: 'text', text: `Failed to select model: ${err.message}` }],
889
- }
890
- }
891
- },
892
- )
21
+ registerCompile(server, projectDir)
22
+ registerLottie(server)
23
+ registerIcons(server)
24
+ registerHuggingface(server)
25
+ registerMedia(server, projectDir)
893
26
 
894
27
  const transport = new StdioServerTransport()
895
28
  await server.connect(transport)