@fugood/bricks-project 2.23.4 → 2.23.5

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 (90) hide show
  1. package/compile/action-name-map.ts +14 -0
  2. package/compile/index.ts +29 -0
  3. package/package.json +2 -2
  4. package/skills/bricks-project/rules/architecture-patterns.md +7 -0
  5. package/skills/bricks-project/rules/buttress.md +9 -6
  6. package/tools/mcp-tools/huggingface.ts +113 -3
  7. package/tools/postinstall.ts +113 -28
  8. package/types/animation.ts +4 -0
  9. package/types/automation.ts +3 -0
  10. package/types/bricks/Camera.ts +33 -6
  11. package/types/bricks/GenerativeMedia.ts +5 -5
  12. package/types/bricks/Icon.ts +2 -2
  13. package/types/bricks/Image.ts +3 -3
  14. package/types/bricks/Items.ts +6 -6
  15. package/types/bricks/Lottie.ts +3 -3
  16. package/types/bricks/Maps.ts +3 -3
  17. package/types/bricks/QrCode.ts +3 -3
  18. package/types/bricks/Rect.ts +3 -3
  19. package/types/bricks/RichText.ts +2 -2
  20. package/types/bricks/Slideshow.ts +1 -1
  21. package/types/bricks/Svg.ts +2 -2
  22. package/types/bricks/Text.ts +3 -3
  23. package/types/bricks/TextInput.ts +10 -6
  24. package/types/bricks/Video.ts +3 -3
  25. package/types/bricks/VideoStreaming.ts +2 -2
  26. package/types/bricks/WebView.ts +3 -3
  27. package/types/canvas.ts +2 -0
  28. package/types/common.ts +5 -0
  29. package/types/data-calc-command.ts +2 -0
  30. package/types/data-calc.ts +1 -0
  31. package/types/data.ts +2 -0
  32. package/types/generators/AlarmClock.ts +4 -4
  33. package/types/generators/Assistant.ts +53 -8
  34. package/types/generators/BleCentral.ts +11 -3
  35. package/types/generators/BlePeripheral.ts +3 -3
  36. package/types/generators/CanvasMap.ts +3 -3
  37. package/types/generators/CastlesPay.ts +2 -2
  38. package/types/generators/DataBank.ts +29 -2
  39. package/types/generators/File.ts +62 -13
  40. package/types/generators/GraphQl.ts +2 -2
  41. package/types/generators/Http.ts +25 -6
  42. package/types/generators/HttpServer.ts +4 -4
  43. package/types/generators/Information.ts +1 -1
  44. package/types/generators/Intent.ts +7 -1
  45. package/types/generators/Iterator.ts +5 -5
  46. package/types/generators/Keyboard.ts +15 -5
  47. package/types/generators/LlmAnthropicCompat.ts +9 -3
  48. package/types/generators/LlmAppleBuiltin.ts +4 -4
  49. package/types/generators/LlmGgml.ts +63 -13
  50. package/types/generators/LlmMlx.ts +210 -0
  51. package/types/generators/LlmOnnx.ts +13 -4
  52. package/types/generators/LlmOpenAiCompat.ts +19 -3
  53. package/types/generators/LlmQualcommAiEngine.ts +29 -5
  54. package/types/generators/Mcp.ts +331 -16
  55. package/types/generators/McpServer.ts +34 -7
  56. package/types/generators/MediaFlow.ts +24 -6
  57. package/types/generators/MqttBroker.ts +9 -3
  58. package/types/generators/MqttClient.ts +10 -4
  59. package/types/generators/Question.ts +4 -4
  60. package/types/generators/RealtimeTranscription.ts +69 -10
  61. package/types/generators/RerankerGgml.ts +19 -5
  62. package/types/generators/SerialPort.ts +5 -5
  63. package/types/generators/SoundPlayer.ts +1 -1
  64. package/types/generators/SoundRecorder.ts +4 -4
  65. package/types/generators/SpeechToTextGgml.ts +27 -7
  66. package/types/generators/SpeechToTextOnnx.ts +3 -3
  67. package/types/generators/SpeechToTextPlatform.ts +3 -3
  68. package/types/generators/SqLite.ts +9 -5
  69. package/types/generators/Step.ts +2 -2
  70. package/types/generators/SttAppleBuiltin.ts +4 -4
  71. package/types/generators/Tcp.ts +3 -3
  72. package/types/generators/TcpServer.ts +5 -5
  73. package/types/generators/TextToSpeechAppleBuiltin.ts +3 -3
  74. package/types/generators/TextToSpeechGgml.ts +3 -3
  75. package/types/generators/TextToSpeechOnnx.ts +3 -3
  76. package/types/generators/TextToSpeechOpenAiLike.ts +3 -3
  77. package/types/generators/ThermalPrinter.ts +4 -4
  78. package/types/generators/Tick.ts +2 -2
  79. package/types/generators/Udp.ts +8 -3
  80. package/types/generators/VadGgml.ts +34 -5
  81. package/types/generators/VadOnnx.ts +27 -4
  82. package/types/generators/VadTraditional.ts +13 -7
  83. package/types/generators/VectorStore.ts +22 -5
  84. package/types/generators/Watchdog.ts +10 -5
  85. package/types/generators/WebCrawler.ts +3 -3
  86. package/types/generators/WebRtc.ts +14 -8
  87. package/types/generators/WebSocket.ts +4 -4
  88. package/types/generators/index.ts +1 -0
  89. package/types/subspace.ts +1 -0
  90. package/utils/event-props.ts +104 -87
@@ -786,6 +786,20 @@ export const templateActionNameMap = {
786
786
  documents: 'GENERATOR_RERANKER_DOCUMENTS',
787
787
  },
788
788
  },
789
+ GENERATOR_MLX_LLM: {
790
+ GENERATOR_MLX_LLM_LOAD_MODEL: {
791
+ modelId: 'GENERATOR_MLX_LLM_MODEL_ID',
792
+ vlm: 'GENERATOR_MLX_LLM_VLM',
793
+ },
794
+ GENERATOR_MLX_LLM_COMPLETION: {
795
+ messages: 'GENERATOR_MLX_LLM_MESSAGES',
796
+ tools: 'GENERATOR_MLX_LLM_TOOLS',
797
+ maxTokens: 'GENERATOR_MLX_LLM_MAX_TOKENS',
798
+ temperature: 'GENERATOR_MLX_LLM_TEMPERATURE',
799
+ topP: 'GENERATOR_MLX_LLM_TOP_P',
800
+ repetitionPenalty: 'GENERATOR_MLX_LLM_REPETITION_PENALTY',
801
+ },
802
+ },
789
803
  GENERATOR_QNN_LLM: {
790
804
  GENERATOR_QNN_LLM_PROCESS: {
791
805
  prompt: 'GENERATOR_QNN_LLM_PROMPT',
package/compile/index.ts CHANGED
@@ -266,6 +266,7 @@ const compileApplicationSettings = (settings: Application['settings']) => ({
266
266
  use_gemini_api_key_system_data: settings.ai.useGeminiApiKeySystemData,
267
267
  }
268
268
  : undefined,
269
+ hide_short_refs: settings?.hideShortRefs,
269
270
  })
270
271
 
271
272
  const animationTypeMap = {
@@ -385,6 +386,7 @@ function compileRunArray(run: unknown[]): unknown[] {
385
386
  const compileTestCase = (testCase: TestCase) => ({
386
387
  id: testCase.id,
387
388
  name: testCase.name,
389
+ hide_short_ref: testCase.hideShortRef,
388
390
  run: compileRunArray(testCase.run),
389
391
  exit_on_failed: testCase.exit_on_failed,
390
392
  commented: testCase.commented,
@@ -449,6 +451,7 @@ const compileAutomationTest = (
449
451
  return {
450
452
  id: testId,
451
453
  title: test.title,
454
+ hide_short_ref: test.hideShortRef,
452
455
  timeout: test.timeout,
453
456
  trigger_type: test.trigger_type,
454
457
  cron: test.cron,
@@ -476,6 +479,7 @@ const compileAutomationTestMap = (testMap: AutomationTestMap, mapId: string) =>
476
479
 
477
480
  return {
478
481
  title: testMap.title,
482
+ hide_short_ref: testMap.hideShortRef,
479
483
  createdAt: testMap.createdAt,
480
484
  map: arrayToIdMap(
481
485
  testMap.tests,
@@ -519,6 +523,7 @@ export const compile = async (app: Application) => {
519
523
  subspaceMap[subspaceId] = {
520
524
  title: subspace.title,
521
525
  description: subspace.description,
526
+ hide_short_ref: subspace.hideShortRef,
522
527
  _expanded: subspace.unexpanded
523
528
  ? {
524
529
  brick: !subspace.unexpanded.brick,
@@ -556,8 +561,10 @@ export const compile = async (app: Application) => {
556
561
  if (animation.__typename === 'Animation') {
557
562
  const animationDef = animation as AnimationDef
558
563
  map[animationId] = {
564
+ alias: animationDef.alias,
559
565
  title: animationDef.title,
560
566
  description: animationDef.description,
567
+ hide_short_ref: animationDef.hideShortRef,
561
568
  animationRunType: animationDef.runType,
562
569
  property: animationDef.property,
563
570
  type: animationTypeMap[animationDef.config.__type],
@@ -569,8 +576,10 @@ export const compile = async (app: Application) => {
569
576
  } else if (animation.__typename === 'AnimationCompose') {
570
577
  const animationDef = animation as AnimationComposeDef
571
578
  map[animationId] = {
579
+ alias: animationDef.alias,
572
580
  title: animationDef.title,
573
581
  description: animationDef.description,
582
+ hide_short_ref: animationDef.hideShortRef,
574
583
  animationRunType: animationDef.runType,
575
584
  compose_type: animationDef.composeType,
576
585
  item_list: animationDef.items.map((item, index) => {
@@ -670,6 +679,8 @@ export const compile = async (app: Application) => {
670
679
  buildList(item, index, 'brickList'),
671
680
  )
672
681
  property.brickList = brickList
682
+ } else if (!brickItems.brickList) {
683
+ property.brickList = []
673
684
  } else {
674
685
  // Not supported Data for brickList
675
686
  throw new TypeError('Not supported Data for brickList directly')
@@ -679,6 +690,8 @@ export const compile = async (app: Application) => {
679
690
  buildList(item, index, 'brickDetails'),
680
691
  )
681
692
  property.brickDetails = brickDetails
693
+ } else if (!brickItems.brickDetails) {
694
+ property.brickDetails = []
682
695
  } else {
683
696
  // Not supported Data for brickList
684
697
  throw new TypeError('Not supported Data for brickList directly')
@@ -686,8 +699,10 @@ export const compile = async (app: Application) => {
686
699
  }
687
700
  map[brickId] = {
688
701
  template_key: brick.templateKey,
702
+ alias: brick.alias,
689
703
  title: brick.title,
690
704
  description: brick.description,
705
+ hide_short_ref: brick.hideShortRef,
691
706
  property,
692
707
  animation: compileAnimations(
693
708
  brick.templateKey,
@@ -754,8 +769,10 @@ export const compile = async (app: Application) => {
754
769
  )
755
770
 
756
771
  map[canvasId] = {
772
+ alias: canvas.alias,
757
773
  title: canvas.title,
758
774
  description: canvas.description,
775
+ hide_short_ref: canvas.hideShortRef,
759
776
  property: compileProperty(
760
777
  canvas.property,
761
778
  `(canvas: ${canvasId}, subspace ${subspaceId})`,
@@ -834,8 +851,10 @@ export const compile = async (app: Application) => {
834
851
 
835
852
  map[generatorId] = {
836
853
  template_key: generator.templateKey,
854
+ alias: generator.alias,
837
855
  title: generator.title,
838
856
  description: generator.description,
857
+ hide_short_ref: generator.hideShortRef,
839
858
  local_sync: generator.localSyncRunMode
840
859
  ? {
841
860
  run_mode: generator.localSyncRunMode,
@@ -904,8 +923,10 @@ export const compile = async (app: Application) => {
904
923
  )
905
924
 
906
925
  map[dataId] = {
926
+ alias: data.alias,
907
927
  title: data.title,
908
928
  description: data.description,
929
+ hide_short_ref: data.hideShortRef,
909
930
  linked: data.metadata?.linked,
910
931
  linkedFrom: data.metadata?.linkedFrom,
911
932
  local_sync: data.localSyncUpdateMode
@@ -939,6 +960,7 @@ export const compile = async (app: Application) => {
939
960
  const calc: any = {
940
961
  title: dataCalc.title,
941
962
  description: dataCalc.description,
963
+ hide_short_ref: dataCalc.hideShortRef,
942
964
  }
943
965
  if (dataCalc.triggerMode) calc.trigger_type = dataCalc.triggerMode
944
966
  if (dataCalc.__typename === 'DataCalculationMap') {
@@ -1025,6 +1047,7 @@ export const compile = async (app: Application) => {
1025
1047
  acc[getNodeId(dataNode, 'data node', nodeIndex)] = {
1026
1048
  title: dataNode.title,
1027
1049
  description: dataNode.description,
1050
+ hide_short_ref: dataNode.hideShortRef,
1028
1051
  type: 'data-node',
1029
1052
  properties: {},
1030
1053
  in: generateInputPorts(dataNode.inputs),
@@ -1050,6 +1073,7 @@ export const compile = async (app: Application) => {
1050
1073
  acc[getNodeId(commandNode, 'command node', nodeIndex)] = {
1051
1074
  title: commandNode.title,
1052
1075
  description: commandNode.description,
1076
+ hide_short_ref: commandNode.hideShortRef,
1053
1077
  type: `command-node-${type}`,
1054
1078
  properties: {
1055
1079
  command: commandNode.__commandName,
@@ -1193,3 +1217,8 @@ export const compile = async (app: Application) => {
1193
1217
  }
1194
1218
  return config
1195
1219
  }
1220
+
1221
+ export const checkConfig = async (configPath: string) => {
1222
+ const { $ } = await import('bun')
1223
+ await $`bricks app check-config ${configPath}`
1224
+ }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@fugood/bricks-project",
3
- "version": "2.23.4",
3
+ "version": "2.23.5",
4
4
  "main": "index.ts",
5
5
  "scripts": {
6
6
  "typecheck": "tsc --noEmit",
7
7
  "build": "bun scripts/build.js"
8
8
  },
9
9
  "dependencies": {
10
- "@fugood/bricks-cli": "^2.23.4",
10
+ "@fugood/bricks-cli": "^2.23.5",
11
11
  "@huggingface/gguf": "^0.3.2",
12
12
  "@iarna/toml": "^3.0.0",
13
13
  "@modelcontextprotocol/sdk": "^1.15.0",
@@ -37,6 +37,13 @@ Built-in commands for direct state and UI changes.
37
37
  ### Data Calculation (Priority 4)
38
38
  ONLY for deriving, formatting, or aggregating values from other data. Not for orchestration, side effects, or flow control.
39
39
 
40
+ ## Entity Aliases
41
+
42
+ Set `alias` on entities to give them a stable, human-readable name:
43
+ - Code generation uses alias as the variable name (e.g., `alias: 'submitBtn'` → `export const submitBtn`)
44
+ - At runtime, devtools MCP tools accept alias instead of short ID and selectors can match by alias. Note: bricks-project does not directly interact with devtools MCP — aliases here primarily affect code generation
45
+ - Aliases must be unique across the application — duplicates are excluded from resolution
46
+
40
47
  ## Flow Decomposition
41
48
 
42
49
  When the user describes a complex flow, decompose it BEFORE writing code:
@@ -14,8 +14,9 @@ When mobile devices or embedded systems lack hardware for local AI inference (LL
14
14
 
15
15
  ## Supported Generators
16
16
 
17
- - [LLM (GGML)](generator_LLM) - Local Large Language Model inference
18
- - [Speech-to-Text (GGML)](generator_SpeechInference) - Local Speech-to-Text inference
17
+ - LLM (GGML) (LlmMlx.ts) - Local Large Language Model inference with GGML
18
+ - LLM (MLX) (LlmGgml.ts) - Local Large Language Model inference with MLX
19
+ - Speech-to-Text (GGML) (SpeechToTextGgml.ts) - Local Speech-to-Text inference with GGML
19
20
 
20
21
  ## Client Configuration
21
22
 
@@ -50,10 +51,12 @@ const llmGenerator: GeneratorLLM = {
50
51
  property: {
51
52
  modelUrl: 'https://huggingface.co/ggml-org/gemma-3-12b-it-qat-GGUF/resolve/main/gemma-3-12b-it-qat-q4_0.gguf',
52
53
  contextSize: 8192,
53
- buttressEnabled: true,
54
- buttressUrl: 'http://192.168.1.100:2080',
55
- buttressFallbackType: 'use-local',
56
- buttressStrategy: 'prefer-best',
54
+ buttressConnectionSettings: {
55
+ enabled: true,
56
+ url: 'http://192.168.1.100:2080',
57
+ fallbackType: 'use-local',
58
+ strategy: 'prefer-best',
59
+ },
57
60
  },
58
61
  events: {},
59
62
  switches: [],
@@ -102,6 +102,7 @@ type HFSibling = {
102
102
  rfilename: string
103
103
  size?: number
104
104
  lfs?: { sha256?: string }
105
+ blobId?: string
105
106
  }
106
107
 
107
108
  type HFModel = {
@@ -132,8 +133,9 @@ type GeneratorType =
132
133
  | 'GeneratorOnnxLLM'
133
134
  | 'GeneratorOnnxSTT'
134
135
  | 'GeneratorTTS'
136
+ | 'GeneratorMlxLLM'
135
137
 
136
- type ModelKind = 'gguf' | 'onnx'
138
+ type ModelKind = 'gguf' | 'onnx' | 'mlx'
137
139
 
138
140
  interface GeneratorConfig {
139
141
  modelKind: ModelKind
@@ -219,6 +221,11 @@ const generatorConfigs: Record<GeneratorType, GeneratorConfig> = {
219
221
  filter: 'onnx,text-to-speech',
220
222
  hasValidStructure: hasValidOnnxStructure,
221
223
  },
224
+ GeneratorMlxLLM: {
225
+ modelKind: 'mlx',
226
+ filter: 'mlx',
227
+ taskFilter: supportedLlmTasks,
228
+ },
222
229
  }
223
230
 
224
231
  const searchHFModels = async (filter: string, search?: string, limit = 50): Promise<HFModel[]> => {
@@ -276,6 +283,7 @@ export function register(server: McpServer) {
276
283
  'GeneratorOnnxLLM',
277
284
  'GeneratorOnnxSTT',
278
285
  'GeneratorTTS',
286
+ 'GeneratorMlxLLM',
279
287
  ])
280
288
  .describe('Generator type to search models for')
281
289
  .default('GeneratorLLM'),
@@ -407,6 +415,7 @@ export function register(server: McpServer) {
407
415
  'GeneratorOnnxLLM',
408
416
  'GeneratorOnnxSTT',
409
417
  'GeneratorTTS',
418
+ 'GeneratorMlxLLM',
410
419
  ])
411
420
  .describe('Generator type for model selection')
412
421
  .default('GeneratorLLM'),
@@ -489,7 +498,108 @@ export function register(server: McpServer) {
489
498
 
490
499
  // Return JSON for huggingface_select to allow direct parsing by consumers
491
500
  return {
492
- content: [{ type: 'text', text: JSON5.stringify(result, null, 2) }],
501
+ content: [
502
+ {
503
+ type: 'text',
504
+ text: `${JSON5.stringify(result, null, 2)} // Please use _hfRepoInfo undocumented field with @ts-ignore`,
505
+ },
506
+ ],
507
+ }
508
+ }
509
+
510
+ // Handle MLX models (repo-level, like ONNX)
511
+ if (config.modelKind === 'mlx') {
512
+ // Fetch config.json for model architecture metadata
513
+ const headers: Record<string, string> = {}
514
+ if (HF_TOKEN) headers['Authorization'] = `Bearer ${HF_TOKEN}`
515
+ let mlxModelConfig: Record<string, any> | null = null
516
+ try {
517
+ const configRes = await fetch(
518
+ `https://huggingface.co/${modelId}/resolve/main/config.json`,
519
+ { headers },
520
+ )
521
+ if (configRes.ok) mlxModelConfig = await configRes.json()
522
+ } catch {
523
+ // Non-critical
524
+ }
525
+
526
+ const modelType =
527
+ mlxModelConfig?.model_type || details.config?.model_type || details.cardData?.model_type
528
+
529
+ // Build MLX model metadata from config.json (for hardware guardrails)
530
+ const cfg = mlxModelConfig || ({} as Record<string, any>)
531
+ const textCfg = cfg.text_config || cfg
532
+ const numHeads = textCfg.num_attention_heads || textCfg.n_heads || 0
533
+ const hiddenSize = textCfg.hidden_size || textCfg.dim || 0
534
+ const kvLoraRank = textCfg.kv_lora_rank || 0
535
+ const quant = cfg.quantization || cfg.quantization_config || null
536
+
537
+ // Sum safetensors/npz file sizes for model weight bytes
538
+ const modelBytes = siblings
539
+ .filter((f) => /\.(safetensors|npz)$/.test(f.rfilename))
540
+ .reduce((sum, f) => sum + (f.size ?? 0), 0)
541
+
542
+ // Build _mlxDownloadFiles list (safetensors, json, jinja, tokenizer.model)
543
+ const mlxDownloadFiles = siblings
544
+ .filter(
545
+ (f) =>
546
+ f.rfilename.endsWith('.safetensors') ||
547
+ f.rfilename.endsWith('.json') ||
548
+ f.rfilename.endsWith('.jinja') ||
549
+ f.rfilename === 'tokenizer.model',
550
+ )
551
+ .map((f) => ({
552
+ url: `https://huggingface.co/${modelId}/resolve/main/${f.rfilename}?download=true`,
553
+ filename: `${modelId.replace('/', '-')}/${f.rfilename}`,
554
+ // eslint-disable-next-line no-nested-ternary
555
+ hash_type: f.lfs ? 'sha256' : f.blobId ? 'sha1' : undefined,
556
+ sha256: f.lfs?.sha256,
557
+ sha1: f.lfs ? undefined : f.blobId,
558
+ }))
559
+
560
+ const result = {
561
+ modelId,
562
+ modelType,
563
+ _mlxDownloadFiles: mlxDownloadFiles,
564
+ _hfRepoInfo: {
565
+ repo: modelId,
566
+ model: {
567
+ id: details.id,
568
+ downloads: details.downloads,
569
+ likes: details.likes,
570
+ author: details.author,
571
+ },
572
+ generatorType,
573
+ modelType,
574
+ ...(modelBytes > 0 && { size: modelBytes }),
575
+ ...(mlxModelConfig && {
576
+ mlxModelMetadata: {
577
+ hiddenSize,
578
+ numLayers: textCfg.num_hidden_layers || textCfg.n_layers || 0,
579
+ numHeads,
580
+ numKvHeads: textCfg.num_key_value_heads ?? numHeads,
581
+ headDim:
582
+ textCfg.head_dim ||
583
+ textCfg.v_head_dim ||
584
+ (numHeads > 0 && hiddenSize > 0 ? hiddenSize / numHeads : 0),
585
+ maxCtx: textCfg.max_position_embeddings || 0,
586
+ kvLoraRank,
587
+ qkRopeHeadDim: textCfg.qk_rope_head_dim || 0,
588
+ isMLA: kvLoraRank > 0,
589
+ quantBits: quant?.bits || null,
590
+ quantGroupSize: quant?.group_size || null,
591
+ },
592
+ }),
593
+ },
594
+ }
595
+
596
+ return {
597
+ content: [
598
+ {
599
+ type: 'text',
600
+ text: `${JSON5.stringify(result, null, 2)} // IMPORTANT: Use _mlxDownloadFiles / _hfRepoInfo undocumented fields with @ts-ignore`,
601
+ },
602
+ ],
493
603
  }
494
604
  }
495
605
 
@@ -638,7 +748,7 @@ export function register(server: McpServer) {
638
748
  content: [
639
749
  {
640
750
  type: 'text',
641
- text: `${JSON5.stringify(result, null, 2)} // Please use _hfRepoInfo undocumented field with @ts-ignore`,
751
+ text: `${JSON5.stringify(result, null, 2)} // IMPORTANT: Use _hfRepoInfo undocumented field with @ts-ignore`,
642
752
  },
643
753
  ],
644
754
  }
@@ -1,8 +1,25 @@
1
1
  import { $ } from 'bun'
2
- import { stat, readFile, writeFile, readdir } from 'fs/promises'
2
+ import {
3
+ cp,
4
+ lstat,
5
+ mkdir,
6
+ readFile,
7
+ readdir,
8
+ readlink,
9
+ rm,
10
+ stat,
11
+ symlink,
12
+ writeFile,
13
+ } from 'fs/promises'
14
+ import * as path from 'path'
3
15
  import TOML from '@iarna/toml'
4
16
 
5
17
  const cwd = process.cwd()
18
+ const projectSkillsDir = path.join(cwd, '.bricks', 'skills')
19
+ const compatibilitySkillLinks = [
20
+ path.join(cwd, '.claude', 'skills'),
21
+ path.join(cwd, '.codex', 'skills'),
22
+ ]
6
23
 
7
24
  async function exists(f: string) {
8
25
  try {
@@ -13,6 +30,15 @@ async function exists(f: string) {
13
30
  }
14
31
  }
15
32
 
33
+ async function pathExists(f: string) {
34
+ try {
35
+ await lstat(f)
36
+ return true
37
+ } catch {
38
+ return false
39
+ }
40
+ }
41
+
16
42
  // handle flag --skip-copy
17
43
  const skipCopyProject = process.argv.includes('--skip-copy-project')
18
44
  if (skipCopyProject) {
@@ -34,6 +60,7 @@ type CodexMcpConfig = {
34
60
  mcp_servers: Record<string, typeof projectMcpServer>
35
61
  }
36
62
 
63
+ // Claude Code and AGENTS.md projects both use the shared project .mcp.json file.
37
64
  const defaultMcpConfig = {
38
65
  mcpServers: {
39
66
  'bricks-project': projectMcpServer,
@@ -64,43 +91,101 @@ const hasClaudeCode = await exists(`${cwd}/CLAUDE.md`)
64
91
  const hasAgentsMd = await exists(`${cwd}/AGENTS.md`)
65
92
 
66
93
  if (hasClaudeCode || hasAgentsMd) {
94
+ // Keep the workspace-level JSON MCP config aligned for tools that read .mcp.json.
67
95
  const mcpConfigPath = `${cwd}/.mcp.json`
68
96
  await handleMcpConfigOverride(mcpConfigPath)
69
97
  }
70
98
 
71
- const setupSkills = async (skillsDir) => {
72
- const packageSkillsDir = `${__dirname}/../skills`
99
+ const copyMissingSkills = async (sourceDir: string, targetDir: string) => {
100
+ if (!(await exists(sourceDir))) return
101
+
102
+ const packageSkills = await readdir(sourceDir, { withFileTypes: true })
103
+ const skillsToInstall = packageSkills.filter(
104
+ (entry) => entry.isDirectory() && !entry.name.startsWith('.'),
105
+ )
106
+
107
+ await mkdir(targetDir, { recursive: true })
108
+
109
+ await Promise.all(
110
+ skillsToInstall.map(async (entry) => {
111
+ const targetSkillDir = path.join(targetDir, entry.name)
112
+ if (await exists(targetSkillDir)) {
113
+ console.log(`Skill '${entry.name}' already exists, skipping`)
114
+ } else {
115
+ await cp(path.join(sourceDir, entry.name), targetSkillDir, { recursive: true })
116
+ console.log(`Installed skill '${entry.name}' to ${targetDir}/`)
117
+ }
118
+ }),
119
+ )
120
+ }
121
+
122
+ const migrateSkillsDir = async (legacySkillsDir: string, canonicalSkillsDir: string) => {
123
+ if (!(await pathExists(legacySkillsDir))) return
124
+
125
+ const legacyStats = await lstat(legacySkillsDir)
126
+
127
+ if (legacyStats.isSymbolicLink()) {
128
+ const linkTarget = await readlink(legacySkillsDir)
129
+ const resolvedTarget = path.resolve(path.dirname(legacySkillsDir), linkTarget)
130
+ if (resolvedTarget === canonicalSkillsDir) return
73
131
 
74
- if (await exists(packageSkillsDir)) {
75
- const packageSkills = await readdir(packageSkillsDir)
76
- const skillsToInstall = packageSkills.filter((skill) => !skill.startsWith('.'))
77
-
78
- await $`mkdir -p ${skillsDir}`
79
-
80
- await Promise.all(
81
- skillsToInstall.map(async (skill) => {
82
- const targetSkillDir = `${skillsDir}/${skill}`
83
- if (await exists(targetSkillDir)) {
84
- console.log(`Skill '${skill}' already exists, skipping`)
85
- } else {
86
- await $`cp -r ${packageSkillsDir}/${skill} ${targetSkillDir}`
87
- console.log(`Installed skill '${skill}' to ${skillsDir}/`)
88
- }
89
- }),
90
- )
132
+ await copyMissingSkills(resolvedTarget, canonicalSkillsDir)
133
+ await rm(legacySkillsDir, { force: true, recursive: true })
134
+ return
91
135
  }
136
+
137
+ if (legacyStats.isDirectory()) {
138
+ await copyMissingSkills(legacySkillsDir, canonicalSkillsDir)
139
+ await rm(legacySkillsDir, { force: true, recursive: true })
140
+ return
141
+ }
142
+
143
+ console.warn(`Skipping skills migration for ${legacySkillsDir}; expected a directory or symlink`)
92
144
  }
93
145
 
94
- if (hasClaudeCode) {
95
- // Install skills that don't already exist in the project
96
- await setupSkills(`${cwd}/.claude/skills`)
146
+ const ensureCompatibilitySkillLink = async (linkPath: string, targetDir: string) => {
147
+ await mkdir(path.dirname(linkPath), { recursive: true })
148
+
149
+ if (await pathExists(linkPath)) {
150
+ const linkStats = await lstat(linkPath)
151
+ if (linkStats.isSymbolicLink()) {
152
+ const linkTarget = await readlink(linkPath)
153
+ const resolvedTarget = path.resolve(path.dirname(linkPath), linkTarget)
154
+ if (resolvedTarget === targetDir) return
155
+ } else {
156
+ console.warn(
157
+ `Skipping skills symlink at ${linkPath}; path already exists and is not a symlink`,
158
+ )
159
+ return
160
+ }
161
+ }
162
+
163
+ const relativeTarget = path.relative(path.dirname(linkPath), targetDir)
164
+ const symlinkType = process.platform === 'win32' ? 'junction' : 'dir'
165
+ await symlink(relativeTarget, linkPath, symlinkType)
166
+ console.log(`Linked ${linkPath} -> ${relativeTarget}`)
97
167
  }
98
168
 
99
- if (hasAgentsMd) {
100
- // Handle codex skills
101
- // Currently no signal file for codex skills, so we just check if AGENTS.md exists
102
- await setupSkills(`${cwd}/.codex/skills`)
169
+ const setupSkills = async () => {
170
+ const packageSkillsDir = `${__dirname}/../skills`
171
+ await mkdir(projectSkillsDir, { recursive: true })
172
+ await copyMissingSkills(packageSkillsDir, projectSkillsDir)
173
+
174
+ await Promise.all(
175
+ compatibilitySkillLinks.map(async (linkPath) => {
176
+ await migrateSkillsDir(linkPath, projectSkillsDir)
177
+ await ensureCompatibilitySkillLink(linkPath, projectSkillsDir)
178
+ }),
179
+ )
180
+ }
103
181
 
182
+ if (hasClaudeCode || hasAgentsMd) {
183
+ // Install project skills once and expose them through compatibility symlinks.
184
+ await setupSkills()
185
+ }
186
+
187
+ if (hasAgentsMd) {
188
+ // Codex stores its project-local MCP config in .codex/config.toml.
104
189
  const defaultCodexMcpConfig = {
105
190
  mcp_servers: {
106
191
  'bricks-project': projectMcpServer,
@@ -128,7 +213,7 @@ if (hasAgentsMd) {
128
213
  console.log(`Updated ${mcpConfigPath}`)
129
214
  }
130
215
 
131
- // Setup MCP config (.codex/config.toml)
216
+ // Keep the Codex TOML MCP config aligned with the same bricks-project server entry.
132
217
  const codexConfigPath = `${cwd}/.codex/config.toml`
133
218
  await handleCodexMcpConfigOverride(codexConfigPath)
134
219
  }
@@ -61,8 +61,10 @@ export interface AnimationDecayConfig {
61
61
  export interface AnimationDef {
62
62
  __typename: 'Animation'
63
63
  id: string
64
+ alias?: string
64
65
  title: string
65
66
  description?: string
67
+ hideShortRef?: boolean
66
68
  runType?: 'once' | 'loop'
67
69
  property:
68
70
  | 'transform.translateX'
@@ -80,8 +82,10 @@ export interface AnimationDef {
80
82
  export interface AnimationComposeDef {
81
83
  __typename: 'AnimationCompose'
82
84
  id: string
85
+ alias?: string
83
86
  title: string
84
87
  description?: string
88
+ hideShortRef?: boolean
85
89
  runType?: 'once' | 'loop'
86
90
  composeType: 'parallel' | 'sequence'
87
91
  items: Array<() => Animation>
@@ -182,6 +182,7 @@ export interface TestCase {
182
182
  __typename: 'TestCase'
183
183
  id: string
184
184
  name: string
185
+ hideShortRef?: boolean
185
186
  run: TestMethodRun
186
187
  exit_on_failed: boolean
187
188
  commented: boolean
@@ -203,6 +204,7 @@ export interface AutomationTest {
203
204
  __typename: 'AutomationTest'
204
205
  id: string
205
206
  title: string
207
+ hideShortRef?: boolean
206
208
  timeout: number
207
209
  trigger_type?: TestTriggerType
208
210
  cron?: string // Cron expression when trigger_type is 'cron'
@@ -220,6 +222,7 @@ export interface AutomationTestMap {
220
222
  __typename: 'AutomationTestMap'
221
223
  id: string
222
224
  title: string
225
+ hideShortRef?: boolean
223
226
  createdAt: number
224
227
  tests: AutomationTest[]
225
228
  }
@@ -167,15 +167,42 @@ Default property:
167
167
  }
168
168
  outlets?: {
169
169
  /* Camera device and format information */
170
- info?: () => Data
170
+ info?: () => Data<{ [key: string]: any }>
171
171
  /* Picture taken result */
172
- pictureTaken?: () => Data
172
+ pictureTaken?: () => Data<{
173
+ width?: number
174
+ height?: number
175
+ uri?: string
176
+ base64?: string
177
+ [key: string]: any
178
+ }>
173
179
  /* Record video result */
174
- recordVideo?: () => Data
180
+ recordVideo?: () => Data<{
181
+ uri?: string
182
+ [key: string]: any
183
+ }>
175
184
  /* Barcode read result */
176
- barcodeRead?: () => Data
185
+ barcodeRead?: () => Data<{
186
+ type?: string
187
+ data?: string
188
+ rawData?: string
189
+ bounds?: {
190
+ origin?: {
191
+ x?: number
192
+ y?: number
193
+ [key: string]: any
194
+ }
195
+ size?: {
196
+ width?: number
197
+ height?: number
198
+ [key: string]: any
199
+ }
200
+ [key: string]: any
201
+ }
202
+ [key: string]: any
203
+ }>
177
204
  /* Faces detected result */
178
- faceDetected?: () => Data
205
+ faceDetected?: () => Data<Array<{ [key: string]: any }>>
179
206
  }
180
207
  animation?: AnimationBasicEvents & {
181
208
  stateChange?: Animation
@@ -188,7 +215,7 @@ Default property:
188
215
  }
189
216
  }
190
217
 
191
- /* Camera view brick ([Tutorial](https://intercom.help/bricks-dag-inc/articles/5378589-camera)) */
218
+ /* Camera view brick */
192
219
  export type BrickCamera = Brick &
193
220
  BrickCameraDef & {
194
221
  templateKey: 'BRICK_CAMERA'