@brika/blocks-builtin 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,351 @@
1
+ # @brika/blocks-builtin
2
+
3
+ Core reactive blocks for BRIKA workflow automations. This plugin provides essential building blocks for creating visual workflows.
4
+
5
+ ## Overview
6
+
7
+ The built-in blocks plugin is automatically loaded by the BRIKA hub and provides fundamental workflow control and data manipulation blocks using the reactive stream architecture.
8
+
9
+ ## Available Blocks
10
+
11
+ ### Triggers
12
+
13
+ #### Clock
14
+ Emit periodic ticks on an interval.
15
+
16
+ - **Inputs**: None (source block)
17
+ - **Outputs**: `tick` — `{ count: number, ts: number }`
18
+ - **Config**:
19
+ - `interval` (duration) — Interval between ticks
20
+
21
+ ```yaml
22
+ - id: clock
23
+ type: "@brika/blocks-builtin:clock"
24
+ config:
25
+ interval: 5000 # 5 seconds
26
+ ```
27
+
28
+ ### Flow Control
29
+
30
+ #### Condition
31
+ Branch based on a boolean condition.
32
+
33
+ - **Inputs**: `in` (generic)
34
+ - **Outputs**: `then`, `else` (passthrough)
35
+ - **Config**:
36
+ - `field` — Field path to check (e.g., `"value"`, `"data.status"`)
37
+ - `operator` — `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `contains`, `exists`
38
+ - `value` — Value to compare against
39
+
40
+ ```yaml
41
+ - id: check
42
+ type: "@brika/blocks-builtin:condition"
43
+ config:
44
+ field: "temperature"
45
+ operator: "gt"
46
+ value: 25
47
+ ```
48
+
49
+ #### Switch
50
+ Multi-way branch based on a value.
51
+
52
+ - **Inputs**: `in` (generic)
53
+ - **Outputs**: `case1`, `case2`, `case3`, `default` (passthrough)
54
+ - **Config**:
55
+ - `field` — Field path to check
56
+ - `case1`, `case2`, `case3` — Values to match
57
+
58
+ ```yaml
59
+ - id: switch
60
+ type: "@brika/blocks-builtin:switch"
61
+ config:
62
+ field: "status"
63
+ case1: "active"
64
+ case2: "pending"
65
+ case3: "error"
66
+ ```
67
+
68
+ #### Delay
69
+ Wait for a duration before continuing.
70
+
71
+ - **Inputs**: `in` (generic)
72
+ - **Outputs**: `out` (passthrough)
73
+ - **Config**:
74
+ - `duration` (duration) — Duration to wait
75
+
76
+ ```yaml
77
+ - id: wait
78
+ type: "@brika/blocks-builtin:delay"
79
+ config:
80
+ duration: 5000 # 5 seconds
81
+ ```
82
+
83
+ #### Merge
84
+ Wait for multiple inputs before continuing.
85
+
86
+ - **Inputs**: `a`, `b` (generic)
87
+ - **Outputs**: `out` — `{ a: any, b: any }`
88
+ - **Config**: None
89
+
90
+ ```yaml
91
+ - id: merge
92
+ type: "@brika/blocks-builtin:merge"
93
+ config: {}
94
+ ```
95
+
96
+ #### Split
97
+ Send data to multiple branches.
98
+
99
+ - **Inputs**: `in` (generic)
100
+ - **Outputs**: `a`, `b` (passthrough)
101
+ - **Config**: None
102
+
103
+ ```yaml
104
+ - id: split
105
+ type: "@brika/blocks-builtin:split"
106
+ config: {}
107
+ ```
108
+
109
+ #### End
110
+ Terminate a workflow branch.
111
+
112
+ - **Inputs**: `in` (generic)
113
+ - **Outputs**: None
114
+ - **Config**:
115
+ - `status` — `"success"` or `"failure"`
116
+
117
+ ```yaml
118
+ - id: end
119
+ type: "@brika/blocks-builtin:end"
120
+ config:
121
+ status: success
122
+ ```
123
+
124
+ ### Actions
125
+
126
+ #### HTTP Request
127
+ Make HTTP requests to external APIs.
128
+
129
+ - **Inputs**: `trigger` (generic)
130
+ - **Outputs**: `response`, `error`
131
+ - **Config**:
132
+ - `url` — Request URL
133
+ - `method` — `GET`, `POST`, `PUT`, `PATCH`, `DELETE`
134
+ - `headers` — Request headers object
135
+ - `body` — Request body (for POST/PUT/PATCH)
136
+
137
+ ```yaml
138
+ - id: api-call
139
+ type: "@brika/blocks-builtin:http-request"
140
+ config:
141
+ url: "https://api.example.com/data"
142
+ method: GET
143
+ headers:
144
+ Authorization: "Bearer token"
145
+ ```
146
+
147
+ #### Log
148
+ Log a message with variable interpolation.
149
+
150
+ - **Inputs**: `in` (generic)
151
+ - **Outputs**: `out` (passthrough)
152
+ - **Config**:
153
+ - `message` — Message template with `{{inputs.in.field}}` expressions
154
+ - `level` — `debug`, `info`, `warn`, `error`
155
+
156
+ ```yaml
157
+ - id: log
158
+ type: "@brika/blocks-builtin:log"
159
+ config:
160
+ message: "Received: {{inputs.in.value}}"
161
+ level: info
162
+ ```
163
+
164
+ ### Data Manipulation
165
+
166
+ #### Transform
167
+ Transform or extract data.
168
+
169
+ - **Inputs**: `in` (generic)
170
+ - **Outputs**: `out` (any)
171
+ - **Config**:
172
+ - `field` — Field to extract (empty for passthrough)
173
+ - `template` — Template to build output object
174
+
175
+ ```yaml
176
+ # Extract a field
177
+ - id: extract
178
+ type: "@brika/blocks-builtin:transform"
179
+ config:
180
+ field: "data.temperature"
181
+
182
+ # Build new object
183
+ - id: reshape
184
+ type: "@brika/blocks-builtin:transform"
185
+ config:
186
+ template:
187
+ temp: "data.temperature"
188
+ hum: "data.humidity"
189
+ ```
190
+
191
+ ## Usage Examples
192
+
193
+ ### Simple Clock + Log
194
+
195
+ ```yaml
196
+ id: clock-demo
197
+ name: Clock Demo
198
+ enabled: true
199
+
200
+ blocks:
201
+ - id: clock
202
+ type: "@brika/blocks-builtin:clock"
203
+ config:
204
+ interval: 5000
205
+ position: { x: 100, y: 100 }
206
+
207
+ - id: log
208
+ type: "@brika/blocks-builtin:log"
209
+ config:
210
+ message: "Tick #{{inputs.in.count}}"
211
+ level: info
212
+ position: { x: 300, y: 100 }
213
+
214
+ connections:
215
+ - from: clock
216
+ fromPort: tick
217
+ to: log
218
+ toPort: in
219
+ ```
220
+
221
+ ### Conditional Flow
222
+
223
+ ```yaml
224
+ id: condition-demo
225
+ name: Condition Demo
226
+ enabled: true
227
+
228
+ blocks:
229
+ - id: clock
230
+ type: "@brika/blocks-builtin:clock"
231
+ config:
232
+ interval: 10000
233
+
234
+ - id: condition
235
+ type: "@brika/blocks-builtin:condition"
236
+ config:
237
+ field: "count"
238
+ operator: "gt"
239
+ value: 5
240
+
241
+ - id: high
242
+ type: "@brika/blocks-builtin:log"
243
+ config:
244
+ message: "Count is high: {{inputs.in.count}}"
245
+ level: warn
246
+
247
+ - id: low
248
+ type: "@brika/blocks-builtin:log"
249
+ config:
250
+ message: "Count is low: {{inputs.in.count}}"
251
+ level: debug
252
+
253
+ connections:
254
+ - from: clock
255
+ fromPort: tick
256
+ to: condition
257
+ toPort: in
258
+ - from: condition
259
+ fromPort: then
260
+ to: high
261
+ toPort: in
262
+ - from: condition
263
+ fromPort: else
264
+ to: low
265
+ toPort: in
266
+ ```
267
+
268
+ ### Parallel Branches
269
+
270
+ ```yaml
271
+ id: parallel-demo
272
+ name: Parallel Demo
273
+ enabled: true
274
+
275
+ blocks:
276
+ - id: clock
277
+ type: "@brika/blocks-builtin:clock"
278
+ config:
279
+ interval: 5000
280
+
281
+ - id: split
282
+ type: "@brika/blocks-builtin:split"
283
+ config: {}
284
+
285
+ - id: fast
286
+ type: "@brika/blocks-builtin:log"
287
+ config:
288
+ message: "Fast path"
289
+
290
+ - id: slow
291
+ type: "@brika/blocks-builtin:delay"
292
+ config:
293
+ duration: 2000
294
+
295
+ - id: slow-log
296
+ type: "@brika/blocks-builtin:log"
297
+ config:
298
+ message: "Slow path (after 2s)"
299
+
300
+ - id: merge
301
+ type: "@brika/blocks-builtin:merge"
302
+ config: {}
303
+
304
+ - id: done
305
+ type: "@brika/blocks-builtin:log"
306
+ config:
307
+ message: "Both paths completed"
308
+
309
+ connections:
310
+ - from: clock
311
+ fromPort: tick
312
+ to: split
313
+ toPort: in
314
+ - from: split
315
+ fromPort: a
316
+ to: fast
317
+ toPort: in
318
+ - from: split
319
+ fromPort: b
320
+ to: slow
321
+ toPort: in
322
+ - from: slow
323
+ fromPort: out
324
+ to: slow-log
325
+ toPort: in
326
+ - from: fast
327
+ fromPort: out
328
+ to: merge
329
+ toPort: a
330
+ - from: slow-log
331
+ fromPort: out
332
+ to: merge
333
+ toPort: b
334
+ - from: merge
335
+ fromPort: out
336
+ to: done
337
+ toPort: in
338
+ ```
339
+
340
+ ## Expression Syntax
341
+
342
+ Log blocks support `{{...}}` expressions for variable interpolation:
343
+
344
+ - `{{inputs.in}}` — Raw input data
345
+ - `{{inputs.in.field}}` — Access nested fields
346
+ - `{{config.value}}` — Access config values
347
+ - `{{JSON.stringify(inputs.in)}}` — Serialize to JSON
348
+
349
+ ## Installation
350
+
351
+ This plugin is included by default with BRIKA and does not need to be installed separately.
package/icon.svg ADDED
@@ -0,0 +1,25 @@
1
+ <svg xmlns:xlink="http://www.w3.org/1999/xlink" width="512" height="512" viewBox="0 0 512 512" fill="none"
2
+ xmlns="http://www.w3.org/2000/svg" class="">
3
+ <rect id="_r_9_" width="512" height="512" x="0" y="0" rx="0" fill="url(#_r_a_)" stroke="#FFFFFF" stroke-width="0"
4
+ stroke-opacity="100%" paint-order="stroke"></rect>
5
+ <clipPath id="clip">
6
+ <use xlink:href="#_r_9_"></use>
7
+ </clipPath>
8
+ <defs>
9
+ <radialGradient id="_r_a_" cx="50%" cy="50%" r="100%" fx="50%" fy="50%" gradientUnits="objectBoundingBox">
10
+ <stop stop-color="#f95356"></stop>
11
+ <stop offset="1" stop-color="#7e0000"></stop>
12
+ </radialGradient>
13
+ <radialGradient id="_r_b_" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse"
14
+ gradientTransform="translate(256) rotate(90) scale(512)">
15
+ <stop stop-color="white"></stop>
16
+ <stop offset="1" stop-color="white" stop-opacity="0"></stop>
17
+ </radialGradient>
18
+ </defs>
19
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16" width="352" height="352" x="80" y="80"
20
+ alignment-baseline="middle" style="color: rgb(255, 213, 213); width: 352px; height: 352px;">
21
+ <path fill="currentColor" fill-rule="evenodd"
22
+ d="M4.136 1H4.865c.401 0 .748 0 1.034.024.301.025.6.08.888.23.411.213.746.548.96.959.149.287.204.587.23.888C8 3.387 8 3.734 8 4.136v7.729c0 .401 0 .748-.024 1.034-.025.301-.08.6-.23.888a2.25 2.25 0 0 1-.959.96c-.287.149-.587.204-.888.23C5.613 15 5.266 15 4.864 15H4.135c-.401 0-.748 0-1.034-.024-.301-.025-.6-.08-.888-.23a2.25 2.25 0 0 1-.96-.959c-.149-.287-.204-.587-.23-.888A13.435 13.435 0 0 1 1 11.864V4.135c0-.401 0-.748.024-1.034.025-.301.08-.6.23-.888a2.25 2.25 0 0 1 .959-.96c.287-.149.587-.204.888-.23C3.387 1 3.734 1 4.136 1Zm-.91 1.519c-.208.017-.284.046-.322.065a.75.75 0 0 0-.32.32c-.02.038-.048.114-.065.321-.018.216-.019.5-.019.94v7.67c0 .44 0 .724.019.94.017.207.046.283.065.32a.75.75 0 0 0 .32.32c.038.02.114.05.321.067.216.017.5.018.94.018h.67c.44 0 .724 0 .94-.018.207-.018.283-.047.32-.066a.75.75 0 0 0 .32-.32c.02-.038.05-.114.066-.321.018-.216.019-.5.019-.94v-7.67c0-.44 0-.724-.019-.94-.017-.207-.046-.283-.065-.32a.75.75 0 0 0-.32-.32c-.038-.02-.114-.05-.321-.066-.216-.018-.5-.019-.94-.019h-.67c-.44 0-.724 0-.94.019ZM11.975 8h.048c.329 0 .613 0 .848.016.247.017.495.054.739.155.551.229.99.667 1.218 1.218.101.244.138.492.155.74.016.234.016.518.016.847v1.048c0 .329 0 .613-.016.848a2.3 2.3 0 0 1-.155.739 2.25 2.25 0 0 1-1.218 1.218 2.3 2.3 0 0 1-.74.155c-.234.016-.518.016-.847.016h-.048c-.329 0-.613 0-.848-.016a2.3 2.3 0 0 1-.739-.155 2.25 2.25 0 0 1-1.218-1.218 2.301 2.301 0 0 1-.155-.74C9 12.638 9 12.354 9 12.025v-1.048c0-.329 0-.613.016-.848.017-.247.054-.495.155-.739a2.25 2.25 0 0 1 1.218-1.218c.244-.101.492-.138.74-.155.234-.016.518-.016.847-.016Zm-.746 1.513a.87.87 0 0 0-.267.044.75.75 0 0 0-.406.406.871.871 0 0 0-.045.267c-.012.178-.012.41-.012.77v1c0 .36 0 .592.012.77a.871.871 0 0 0 .045.267.75.75 0 0 0 .406.406.871.871 0 0 0 .267.045c.178.012.41.012.77.012.36 0 .592 0 .77-.012a.871.871 0 0 0 .267-.045.75.75 0 0 0 .406-.406.871.871 0 0 0 .045-.267c.012-.178.012-.41.012-.77v-1c0-.36 0-.592-.012-.77a.871.871 0 0 0-.045-.267.75.75 0 0 0-.406-.406.87.87 0 0 0-.267-.044C12.592 9.5 12.36 9.5 12 9.5c-.36 0-.592 0-.77.013ZM12 1h-.024c-.329 0-.613 0-.848.016a2.301 2.301 0 0 0-.739.155A2.25 2.25 0 0 0 9.171 2.39a2.302 2.302 0 0 0-.155.74C9 3.362 9 3.646 9 3.975v.048c0 .329 0 .613.016.848.017.247.054.495.155.739.229.551.667.99 1.218 1.218.244.101.492.138.74.155.234.016.518.016.847.016h.048c.329 0 .613 0 .848-.016.247-.017.495-.054.739-.155a2.25 2.25 0 0 0 1.218-1.218c.101-.244.138-.492.155-.74.016-.234.016-.518.016-.847v-.048c0-.329 0-.613-.016-.848a2.301 2.301 0 0 0-.155-.739 2.25 2.25 0 0 0-1.218-1.218 2.301 2.301 0 0 0-.74-.155C12.638 1 12.354 1 12.025 1H12Zm-1.037 1.557a.87.87 0 0 1 .267-.044c.178-.013.41-.013.77-.013.36 0 .592 0 .77.013a.87.87 0 0 1 .267.044.75.75 0 0 1 .406.406.871.871 0 0 1 .045.267c.012.178.012.41.012.77 0 .36 0 .592-.012.77a.871.871 0 0 1-.045.267.75.75 0 0 1-.406.406.87.87 0 0 1-.267.044c-.178.013-.41.013-.77.013-.36 0-.592 0-.77-.013a.87.87 0 0 1-.267-.044.75.75 0 0 1-.406-.406.871.871 0 0 1-.045-.267C10.5 4.592 10.5 4.36 10.5 4c0-.36 0-.592.012-.77a.871.871 0 0 1 .045-.267.75.75 0 0 1 .406-.406Z"
23
+ clip-rule="evenodd"></path>
24
+ </svg>
25
+ </svg>
@@ -0,0 +1,183 @@
1
+ {
2
+ "name": "Built-in Blocks",
3
+ "description": "Core workflow blocks for BRIKA automations",
4
+ "blocks": {
5
+ "clock": {
6
+ "name": "Clock",
7
+ "description": "Emit periodic ticks at a configurable interval",
8
+ "ports": {
9
+ "tick": {
10
+ "name": "Tick",
11
+ "description": "Emitted on each interval with count and timestamp"
12
+ }
13
+ }
14
+ },
15
+ "http-request": {
16
+ "name": "HTTP Request",
17
+ "description": "Make HTTP requests to external APIs",
18
+ "ports": {
19
+ "trigger": {
20
+ "name": "Trigger",
21
+ "description": "Trigger the HTTP request"
22
+ },
23
+ "response": {
24
+ "name": "Response",
25
+ "description": "HTTP response with status, headers, and body"
26
+ },
27
+ "error": {
28
+ "name": "Error",
29
+ "description": "Error details if request fails"
30
+ }
31
+ }
32
+ },
33
+ "condition": {
34
+ "name": "Condition",
35
+ "description": "Branch based on a condition",
36
+ "ports": {
37
+ "in": {
38
+ "name": "Input",
39
+ "description": "Value to check"
40
+ },
41
+ "then": {
42
+ "name": "Then",
43
+ "description": "Output when condition is true"
44
+ },
45
+ "else": {
46
+ "name": "Else",
47
+ "description": "Output when condition is false"
48
+ }
49
+ }
50
+ },
51
+ "switch": {
52
+ "name": "Switch",
53
+ "description": "Multi-way branch based on value",
54
+ "ports": {
55
+ "in": {
56
+ "name": "Input",
57
+ "description": "Value to check"
58
+ },
59
+ "case1": {
60
+ "name": "Case 1",
61
+ "description": "First case output"
62
+ },
63
+ "case2": {
64
+ "name": "Case 2",
65
+ "description": "Second case output"
66
+ },
67
+ "case3": {
68
+ "name": "Case 3",
69
+ "description": "Third case output"
70
+ },
71
+ "default": {
72
+ "name": "Default",
73
+ "description": "Output when no case matches"
74
+ }
75
+ }
76
+ },
77
+ "delay": {
78
+ "name": "Delay",
79
+ "description": "Wait for a specified duration",
80
+ "ports": {
81
+ "in": {
82
+ "name": "Input",
83
+ "description": "Data to delay"
84
+ },
85
+ "out": {
86
+ "name": "Output",
87
+ "description": "Delayed data"
88
+ }
89
+ }
90
+ },
91
+ "transform": {
92
+ "name": "Transform",
93
+ "description": "Transform or extract data",
94
+ "ports": {
95
+ "in": {
96
+ "name": "Input",
97
+ "description": "Data to transform"
98
+ },
99
+ "out": {
100
+ "name": "Output",
101
+ "description": "Transformed data"
102
+ }
103
+ }
104
+ },
105
+ "log": {
106
+ "name": "Log",
107
+ "description": "Log a message",
108
+ "ports": {
109
+ "in": {
110
+ "name": "Input",
111
+ "description": "Data to log"
112
+ },
113
+ "out": {
114
+ "name": "Output",
115
+ "description": "Passthrough data"
116
+ }
117
+ }
118
+ },
119
+ "merge": {
120
+ "name": "Merge",
121
+ "description": "Wait for and merge multiple inputs",
122
+ "ports": {
123
+ "a": {
124
+ "name": "Input A",
125
+ "description": "First input"
126
+ },
127
+ "b": {
128
+ "name": "Input B",
129
+ "description": "Second input"
130
+ },
131
+ "out": {
132
+ "name": "Output",
133
+ "description": "Combined inputs"
134
+ }
135
+ }
136
+ },
137
+ "split": {
138
+ "name": "Split",
139
+ "description": "Split execution to parallel branches",
140
+ "ports": {
141
+ "in": {
142
+ "name": "Input",
143
+ "description": "Data to split"
144
+ },
145
+ "a": {
146
+ "name": "Branch A",
147
+ "description": "First branch"
148
+ },
149
+ "b": {
150
+ "name": "Branch B",
151
+ "description": "Second branch"
152
+ }
153
+ }
154
+ },
155
+ "end": {
156
+ "name": "End",
157
+ "description": "Terminate the workflow",
158
+ "ports": {
159
+ "in": {
160
+ "name": "Input",
161
+ "description": "Final data"
162
+ }
163
+ }
164
+ }
165
+ },
166
+ "fields": {
167
+ "field": "Field path to check",
168
+ "operator": "Comparison operator",
169
+ "value": "Value to compare against",
170
+ "duration": "Wait duration",
171
+ "message": "Message to log",
172
+ "level": "Log level",
173
+ "status": "End status",
174
+ "template": "Output template",
175
+ "interval": "Tick interval in milliseconds",
176
+ "emitOnStart": "Emit an initial tick immediately",
177
+ "url": "Request URL",
178
+ "method": "HTTP method",
179
+ "headers": "Request headers",
180
+ "body": "Request body",
181
+ "timeout": "Request timeout in milliseconds"
182
+ }
183
+ }
@@ -0,0 +1,183 @@
1
+ {
2
+ "name": "Blocs intégrés",
3
+ "description": "Blocs de workflow de base pour les automatisations BRIKA",
4
+ "blocks": {
5
+ "clock": {
6
+ "name": "Horloge",
7
+ "description": "Émettre des ticks périodiques à un intervalle configurable",
8
+ "ports": {
9
+ "tick": {
10
+ "name": "Tick",
11
+ "description": "Émis à chaque intervalle avec compteur et horodatage"
12
+ }
13
+ }
14
+ },
15
+ "http-request": {
16
+ "name": "Requête HTTP",
17
+ "description": "Effectuer des requêtes HTTP vers des APIs externes",
18
+ "ports": {
19
+ "trigger": {
20
+ "name": "Déclencheur",
21
+ "description": "Déclencher la requête HTTP"
22
+ },
23
+ "response": {
24
+ "name": "Réponse",
25
+ "description": "Réponse HTTP avec statut, en-têtes et corps"
26
+ },
27
+ "error": {
28
+ "name": "Erreur",
29
+ "description": "Détails de l'erreur si la requête échoue"
30
+ }
31
+ }
32
+ },
33
+ "condition": {
34
+ "name": "Condition",
35
+ "description": "Brancher selon une condition",
36
+ "ports": {
37
+ "in": {
38
+ "name": "Entrée",
39
+ "description": "Valeur à vérifier"
40
+ },
41
+ "then": {
42
+ "name": "Alors",
43
+ "description": "Sortie si la condition est vraie"
44
+ },
45
+ "else": {
46
+ "name": "Sinon",
47
+ "description": "Sortie si la condition est fausse"
48
+ }
49
+ }
50
+ },
51
+ "switch": {
52
+ "name": "Aiguillage",
53
+ "description": "Branchement multiple selon une valeur",
54
+ "ports": {
55
+ "in": {
56
+ "name": "Entrée",
57
+ "description": "Valeur à vérifier"
58
+ },
59
+ "case1": {
60
+ "name": "Cas 1",
61
+ "description": "Première sortie"
62
+ },
63
+ "case2": {
64
+ "name": "Cas 2",
65
+ "description": "Deuxième sortie"
66
+ },
67
+ "case3": {
68
+ "name": "Cas 3",
69
+ "description": "Troisième sortie"
70
+ },
71
+ "default": {
72
+ "name": "Défaut",
73
+ "description": "Sortie si aucun cas ne correspond"
74
+ }
75
+ }
76
+ },
77
+ "delay": {
78
+ "name": "Délai",
79
+ "description": "Attendre une durée spécifiée",
80
+ "ports": {
81
+ "in": {
82
+ "name": "Entrée",
83
+ "description": "Données à retarder"
84
+ },
85
+ "out": {
86
+ "name": "Sortie",
87
+ "description": "Données retardées"
88
+ }
89
+ }
90
+ },
91
+ "transform": {
92
+ "name": "Transformer",
93
+ "description": "Transformer ou extraire des données",
94
+ "ports": {
95
+ "in": {
96
+ "name": "Entrée",
97
+ "description": "Données à transformer"
98
+ },
99
+ "out": {
100
+ "name": "Sortie",
101
+ "description": "Données transformées"
102
+ }
103
+ }
104
+ },
105
+ "log": {
106
+ "name": "Journal",
107
+ "description": "Enregistrer un message",
108
+ "ports": {
109
+ "in": {
110
+ "name": "Entrée",
111
+ "description": "Données à enregistrer"
112
+ },
113
+ "out": {
114
+ "name": "Sortie",
115
+ "description": "Données passantes"
116
+ }
117
+ }
118
+ },
119
+ "merge": {
120
+ "name": "Fusionner",
121
+ "description": "Attendre et fusionner plusieurs entrées",
122
+ "ports": {
123
+ "a": {
124
+ "name": "Entrée A",
125
+ "description": "Première entrée"
126
+ },
127
+ "b": {
128
+ "name": "Entrée B",
129
+ "description": "Deuxième entrée"
130
+ },
131
+ "out": {
132
+ "name": "Sortie",
133
+ "description": "Entrées combinées"
134
+ }
135
+ }
136
+ },
137
+ "split": {
138
+ "name": "Diviser",
139
+ "description": "Diviser l'exécution en branches parallèles",
140
+ "ports": {
141
+ "in": {
142
+ "name": "Entrée",
143
+ "description": "Données à diviser"
144
+ },
145
+ "a": {
146
+ "name": "Branche A",
147
+ "description": "Première branche"
148
+ },
149
+ "b": {
150
+ "name": "Branche B",
151
+ "description": "Deuxième branche"
152
+ }
153
+ }
154
+ },
155
+ "end": {
156
+ "name": "Fin",
157
+ "description": "Terminer le workflow",
158
+ "ports": {
159
+ "in": {
160
+ "name": "Entrée",
161
+ "description": "Données finales"
162
+ }
163
+ }
164
+ }
165
+ },
166
+ "fields": {
167
+ "field": "Chemin du champ à vérifier",
168
+ "operator": "Opérateur de comparaison",
169
+ "value": "Valeur à comparer",
170
+ "duration": "Durée d'attente",
171
+ "message": "Message à enregistrer",
172
+ "level": "Niveau de journal",
173
+ "status": "Statut de fin",
174
+ "template": "Modèle de sortie",
175
+ "interval": "Intervalle en millisecondes",
176
+ "emitOnStart": "Émettre un tick initial immédiatement",
177
+ "url": "URL de la requête",
178
+ "method": "Méthode HTTP",
179
+ "headers": "En-têtes de la requête",
180
+ "body": "Corps de la requête",
181
+ "timeout": "Délai d'expiration en millisecondes"
182
+ }
183
+ }
package/package.json ADDED
@@ -0,0 +1,133 @@
1
+ {
2
+ "$schema": "https://schema.brika.dev/plugin.schema.json",
3
+ "name": "@brika/blocks-builtin",
4
+ "version": "0.2.0",
5
+ "description": "Core workflow blocks for BRIKA automations",
6
+ "author": "BRIKA Team",
7
+ "license": "MIT",
8
+ "homepage": "https://github.com/maxscharwath/brika/tree/main/plugins/blocks-builtin#readme",
9
+ "bugs": {
10
+ "url": "https://github.com/maxscharwath/brika/issues"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/maxscharwath/brika.git",
15
+ "directory": "plugins/blocks-builtin"
16
+ },
17
+ "icon": "./icon.svg",
18
+ "keywords": [
19
+ "brika",
20
+ "blocks",
21
+ "workflow",
22
+ "automation",
23
+ "condition",
24
+ "delay",
25
+ "home-automation",
26
+ "visual-programming"
27
+ ],
28
+ "engines": {
29
+ "brika": "^0.2.0"
30
+ },
31
+ "type": "module",
32
+ "main": "./src/main.ts",
33
+ "exports": {
34
+ ".": "./src/main.ts"
35
+ },
36
+ "files": [
37
+ "src",
38
+ "locales",
39
+ "icon.svg",
40
+ "README.md"
41
+ ],
42
+ "scripts": {
43
+ "link": "bun link",
44
+ "tsc": "bunx --biome tsc --noEmit",
45
+ "prepublishOnly": "bun run tsc"
46
+ },
47
+ "blocks": [
48
+ {
49
+ "id": "clock",
50
+ "name": "Clock",
51
+ "description": "Emit periodic ticks on an interval",
52
+ "category": "trigger",
53
+ "icon": "clock",
54
+ "color": "#22c55e"
55
+ },
56
+ {
57
+ "id": "http-request",
58
+ "name": "HTTP Request",
59
+ "description": "Make HTTP requests to external APIs",
60
+ "category": "action",
61
+ "icon": "globe",
62
+ "color": "#3b82f6"
63
+ },
64
+ {
65
+ "id": "condition",
66
+ "name": "Condition",
67
+ "description": "Branch based on a condition",
68
+ "category": "flow",
69
+ "icon": "git-branch",
70
+ "color": "#f59e0b"
71
+ },
72
+ {
73
+ "id": "switch",
74
+ "name": "Switch",
75
+ "description": "Multi-way branch based on a value",
76
+ "category": "flow",
77
+ "icon": "shuffle",
78
+ "color": "#8b5cf6"
79
+ },
80
+ {
81
+ "id": "delay",
82
+ "name": "Delay",
83
+ "description": "Wait for a duration before continuing",
84
+ "category": "flow",
85
+ "icon": "timer",
86
+ "color": "#6b7280"
87
+ },
88
+ {
89
+ "id": "transform",
90
+ "name": "Transform",
91
+ "description": "Transform or extract data fields",
92
+ "category": "transform",
93
+ "icon": "edit",
94
+ "color": "#ec4899"
95
+ },
96
+ {
97
+ "id": "log",
98
+ "name": "Log",
99
+ "description": "Log a message to the console",
100
+ "category": "action",
101
+ "icon": "file-text",
102
+ "color": "#78716c"
103
+ },
104
+ {
105
+ "id": "merge",
106
+ "name": "Merge",
107
+ "description": "Wait for multiple inputs before continuing",
108
+ "category": "flow",
109
+ "icon": "git-merge",
110
+ "color": "#06b6d4"
111
+ },
112
+ {
113
+ "id": "split",
114
+ "name": "Split",
115
+ "description": "Split into parallel branches",
116
+ "category": "flow",
117
+ "icon": "git-fork",
118
+ "color": "#a855f7"
119
+ },
120
+ {
121
+ "id": "end",
122
+ "name": "End",
123
+ "description": "End the workflow branch",
124
+ "category": "action",
125
+ "icon": "square",
126
+ "color": "#dc2626"
127
+ }
128
+ ],
129
+ "dependencies": {
130
+ "@brika/sdk": "0.1.1",
131
+ "zod": "^4.3.4"
132
+ }
133
+ }
package/src/main.ts ADDED
@@ -0,0 +1,486 @@
1
+ /**
2
+ * Built-in Blocks Plugin
3
+ *
4
+ * Provides core workflow blocks for BRIKA automations.
5
+ * All blocks use the reactive defineReactiveBlock API.
6
+ */
7
+
8
+ import {
9
+ combine,
10
+ defineReactiveBlock,
11
+ delay as delayOp,
12
+ input,
13
+ interval,
14
+ log,
15
+ map,
16
+ output,
17
+ type Serializable,
18
+ z,
19
+ } from '@brika/sdk';
20
+
21
+ // ─────────────────────────────────────────────────────────────────────────────
22
+ // Action Block - Call a tool with arguments
23
+ // ─────────────────────────────────────────────────────────────────────────────
24
+
25
+ // HTTP Response schema
26
+ const HttpResponseSchema = z.object({
27
+ status: z.number(),
28
+ statusText: z.string(),
29
+ headers: z.record(z.string(), z.string()),
30
+ body: z.any(),
31
+ });
32
+
33
+ export const httpRequest = defineReactiveBlock(
34
+ {
35
+ id: 'http-request',
36
+ name: 'HTTP Request',
37
+ description: 'Make HTTP requests to external APIs',
38
+ category: 'action',
39
+ icon: 'globe',
40
+ color: '#3b82f6',
41
+ inputs: {
42
+ trigger: input(z.generic(), { name: 'Trigger' }),
43
+ },
44
+ outputs: {
45
+ response: output(HttpResponseSchema, { name: 'Response' }),
46
+ error: output(z.object({ message: z.string() }), { name: 'Error' }),
47
+ },
48
+ config: z.object({
49
+ url: z.string().describe('Request URL'),
50
+ method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']).optional().describe('HTTP method'),
51
+ headers: z.record(z.string(), z.string()).optional().describe('Request headers'),
52
+ body: z.string().optional().describe('Request body (for POST/PUT/PATCH)'),
53
+ }),
54
+ },
55
+ ({ inputs, outputs, config, log }) => {
56
+ inputs.trigger.on(async () => {
57
+ log('debug', `HTTP ${config.method ?? 'GET'} ${config.url}`);
58
+ try {
59
+ const res = await fetch(config.url, {
60
+ method: config.method ?? 'GET',
61
+ headers: config.headers,
62
+ body: config.body,
63
+ });
64
+ const body = await res.json().catch(() => res.text());
65
+ outputs.response.emit({
66
+ status: res.status,
67
+ statusText: res.statusText,
68
+ headers: Object.fromEntries(res.headers.entries()),
69
+ body,
70
+ });
71
+ } catch (err) {
72
+ log('error', `HTTP request failed: ${err}`);
73
+ outputs.error.emit({ message: String(err) });
74
+ }
75
+ });
76
+ }
77
+ );
78
+
79
+ // ─────────────────────────────────────────────────────────────────────────────
80
+ // Condition Block - Branch based on a condition
81
+ // ─────────────────────────────────────────────────────────────────────────────
82
+
83
+ export const condition = defineReactiveBlock(
84
+ {
85
+ id: 'condition',
86
+ name: 'Condition',
87
+ description: 'Branch based on a condition',
88
+ category: 'flow',
89
+ icon: 'git-branch',
90
+ color: '#f59e0b',
91
+ inputs: {
92
+ in: input(z.generic(), { name: 'Input' }),
93
+ },
94
+ outputs: {
95
+ then: output(z.passthrough('in'), { name: 'Then' }),
96
+ else: output(z.passthrough('in'), { name: 'Else' }),
97
+ },
98
+ config: z.object({
99
+ field: z.string().describe('Field path to check (e.g., "value", "data.status")'),
100
+ operator: z
101
+ .enum(['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'contains', 'exists'])
102
+ .describe('Comparison operator'),
103
+ value: z.any().optional().describe('Value to compare against'),
104
+ }),
105
+ },
106
+ ({ inputs, outputs, config, log }) => {
107
+ inputs.in.on((data) => {
108
+ const fieldValue = getFieldValue(data, config.field);
109
+ const result = evaluate(fieldValue, config.operator, config.value);
110
+ log('debug', `Condition: ${config.field} ${config.operator} ${config.value} = ${result}`);
111
+
112
+ if (result) {
113
+ outputs.then.emit(data);
114
+ } else {
115
+ outputs.else.emit(data);
116
+ }
117
+ });
118
+ }
119
+ );
120
+
121
+ function getFieldValue(data: unknown, path: string): unknown {
122
+ if (data === null || data === undefined) return undefined;
123
+ const parts = path.split('.');
124
+ let current: unknown = data;
125
+ for (const part of parts) {
126
+ if (current === null || current === undefined) return undefined;
127
+ if (typeof current !== 'object') return undefined;
128
+ current = (current as Record<string, unknown>)[part];
129
+ }
130
+ return current;
131
+ }
132
+
133
+ function evaluate(fieldValue: unknown, operator: string, compareValue: unknown): boolean {
134
+ switch (operator) {
135
+ case 'eq':
136
+ return fieldValue === compareValue;
137
+ case 'neq':
138
+ return fieldValue !== compareValue;
139
+ case 'gt':
140
+ return Number(fieldValue) > Number(compareValue);
141
+ case 'gte':
142
+ return Number(fieldValue) >= Number(compareValue);
143
+ case 'lt':
144
+ return Number(fieldValue) < Number(compareValue);
145
+ case 'lte':
146
+ return Number(fieldValue) <= Number(compareValue);
147
+ case 'contains':
148
+ return String(fieldValue).includes(String(compareValue));
149
+ case 'exists':
150
+ return fieldValue !== undefined && fieldValue !== null;
151
+ default:
152
+ return false;
153
+ }
154
+ }
155
+
156
+ // ─────────────────────────────────────────────────────────────────────────────
157
+ // Switch Block - Multi-way branch
158
+ // ─────────────────────────────────────────────────────────────────────────────
159
+
160
+ export const switchBlock = defineReactiveBlock(
161
+ {
162
+ id: 'switch',
163
+ name: 'Switch',
164
+ description: 'Multi-way branch based on a value',
165
+ category: 'flow',
166
+ icon: 'shuffle',
167
+ color: '#8b5cf6',
168
+ inputs: {
169
+ in: input(z.generic(), { name: 'Input' }),
170
+ },
171
+ outputs: {
172
+ case1: output(z.passthrough('in'), { name: 'Case 1' }),
173
+ case2: output(z.passthrough('in'), { name: 'Case 2' }),
174
+ case3: output(z.passthrough('in'), { name: 'Case 3' }),
175
+ default: output(z.passthrough('in'), { name: 'Default' }),
176
+ },
177
+ config: z.object({
178
+ field: z.string().describe('Field path to check'),
179
+ case1: z.any().optional().describe('Value for case 1'),
180
+ case2: z.any().optional().describe('Value for case 2'),
181
+ case3: z.any().optional().describe('Value for case 3'),
182
+ }),
183
+ },
184
+ ({ inputs, outputs, config, log }) => {
185
+ inputs.in.on((data) => {
186
+ const value = getFieldValue(data, config.field);
187
+ log('debug', `Switch value: ${JSON.stringify(value)}`);
188
+
189
+ if (value === config.case1) {
190
+ outputs.case1.emit(data);
191
+ } else if (value === config.case2) {
192
+ outputs.case2.emit(data);
193
+ } else if (value === config.case3) {
194
+ outputs.case3.emit(data);
195
+ } else {
196
+ outputs.default.emit(data);
197
+ }
198
+ });
199
+ }
200
+ );
201
+
202
+ // ─────────────────────────────────────────────────────────────────────────────
203
+ // Delay Block - Wait for a duration
204
+ // ─────────────────────────────────────────────────────────────────────────────
205
+
206
+ export const delay = defineReactiveBlock(
207
+ {
208
+ id: 'delay',
209
+ name: 'Delay',
210
+ description: 'Wait for a duration before continuing',
211
+ category: 'flow',
212
+ icon: 'timer',
213
+ color: '#6b7280',
214
+ inputs: {
215
+ in: input(z.generic(), { name: 'Input' }),
216
+ },
217
+ outputs: {
218
+ out: output(z.passthrough('in'), { name: 'Output' }),
219
+ },
220
+ config: z.object({
221
+ duration: z.duration(undefined, 'Duration to wait'),
222
+ }),
223
+ },
224
+ ({ inputs, outputs, config, log }) => {
225
+ log('debug', `Delay configured: ${config.duration}ms`);
226
+
227
+ // Use delay operator to wait before emitting
228
+ inputs.in.pipe(delayOp(config.duration)).to(outputs.out);
229
+ }
230
+ );
231
+
232
+ // ─────────────────────────────────────────────────────────────────────────────
233
+ // Emit Block - Emit an event
234
+ // ─────────────────────────────────────────────────────────────────────────────
235
+
236
+ export const clock = defineReactiveBlock(
237
+ {
238
+ id: 'clock',
239
+ name: 'Clock',
240
+ description: 'Emit periodic ticks on an interval',
241
+ category: 'trigger',
242
+ icon: 'clock',
243
+ color: '#22c55e',
244
+ inputs: {},
245
+ outputs: {
246
+ tick: output(z.object({ count: z.number(), ts: z.number() }), { name: 'Tick' }),
247
+ },
248
+ config: z.object({
249
+ interval: z.duration(undefined, 'Interval between ticks'),
250
+ }),
251
+ },
252
+ ({ outputs, config, log, start }) => {
253
+ start(interval(config.interval))
254
+ .pipe(
255
+ map((count) => {
256
+ return { count: count + 1, ts: Date.now() };
257
+ })
258
+ )
259
+ .to(outputs.tick);
260
+ log('info', `Clock started with interval: ${config.interval}ms`);
261
+ }
262
+ );
263
+
264
+ // ─────────────────────────────────────────────────────────────────────────────
265
+ // Transform Block - Transform data
266
+ // ─────────────────────────────────────────────────────────────────────────────
267
+
268
+ export const transform = defineReactiveBlock(
269
+ {
270
+ id: 'transform',
271
+ name: 'Transform',
272
+ description: 'Transform or extract data',
273
+ category: 'transform',
274
+ icon: 'edit',
275
+ color: '#ec4899',
276
+ inputs: {
277
+ in: input(z.generic(), { name: 'Input' }),
278
+ },
279
+ outputs: {
280
+ out: output(z.any(), { name: 'Output' }),
281
+ },
282
+ config: z.object({
283
+ field: z.string().optional().describe('Field to extract (empty for passthrough)'),
284
+ template: z.record(z.string(), z.string()).optional().describe('Template to build output'),
285
+ }),
286
+ },
287
+ ({ inputs, outputs, config, log }) => {
288
+ inputs.in
289
+ .pipe(
290
+ map((data) => {
291
+ // If template is provided, build object
292
+ if (config.template) {
293
+ const result: Record<string, unknown> = {};
294
+ for (const [key, path] of Object.entries(config.template)) {
295
+ result[key] = getFieldValue(data, path);
296
+ }
297
+ log('debug', `Transformed with template: ${JSON.stringify(result)}`);
298
+ return result;
299
+ }
300
+
301
+ // If field is provided, extract it
302
+ if (config.field) {
303
+ const value = getFieldValue(data, config.field);
304
+ log('debug', `Extracted field ${config.field}: ${JSON.stringify(value)}`);
305
+ return value;
306
+ }
307
+
308
+ // Passthrough
309
+ return data;
310
+ })
311
+ )
312
+ .to(outputs.out);
313
+ }
314
+ );
315
+
316
+ // ─────────────────────────────────────────────────────────────────────────────
317
+ // Expression Interpolation
318
+ // ─────────────────────────────────────────────────────────────────────────────
319
+
320
+ /**
321
+ * Interpolate {{inputs.portId.field}} expressions in a template string.
322
+ *
323
+ * @param template Template string with {{...}} placeholders
324
+ * @param context Object containing available data: { inputs: { portId: data }, config: {...} }
325
+ */
326
+ function interpolate(
327
+ template: string,
328
+ context: { inputs: Record<string, unknown>; config: Record<string, unknown> }
329
+ ): string {
330
+ return template.replace(/\{\{([^}]+)\}\}/g, (_, expr: string) => {
331
+ const path = expr.trim().split('.');
332
+ let value: unknown = context;
333
+
334
+ for (const key of path) {
335
+ if (value === null || value === undefined) return '';
336
+ if (typeof value !== 'object') return '';
337
+ value = (value as Record<string, unknown>)[key];
338
+ }
339
+
340
+ if (value === undefined || value === null) return '';
341
+ if (typeof value === 'object') return JSON.stringify(value);
342
+ return String(value);
343
+ });
344
+ }
345
+
346
+ // ─────────────────────────────────────────────────────────────────────────────
347
+ // Log Block - Log a message
348
+ // ─────────────────────────────────────────────────────────────────────────────
349
+
350
+ export const logBlock = defineReactiveBlock(
351
+ {
352
+ id: 'log',
353
+ name: 'Log',
354
+ description: 'Log a message with variable interpolation',
355
+ category: 'action',
356
+ icon: 'file-text',
357
+ color: '#78716c',
358
+ inputs: {
359
+ in: input(z.generic(), { name: 'Input' }),
360
+ },
361
+ outputs: {
362
+ out: output(z.passthrough('in'), { name: 'Output' }),
363
+ },
364
+ config: z.object({
365
+ message: z.string().optional().describe('Message template with {{inputs.in.field}} expressions'),
366
+ level: z.enum(['debug', 'info', 'warn', 'error']).default('info').describe('Log level'),
367
+ }),
368
+ },
369
+ ({ inputs, outputs, config, log }) => {
370
+ inputs.in.on((data) => {
371
+ const context = {
372
+ inputs: { in: data },
373
+ config: config as Record<string, unknown>,
374
+ };
375
+
376
+ const message = config.message
377
+ ? interpolate(config.message, context)
378
+ : JSON.stringify(data);
379
+
380
+ log(config.level, message);
381
+ outputs.out.emit(data);
382
+ });
383
+ }
384
+ );
385
+
386
+ // ─────────────────────────────────────────────────────────────────────────────
387
+ // Merge Block - Wait for multiple inputs (combine)
388
+ // ─────────────────────────────────────────────────────────────────────────────
389
+
390
+ export const merge = defineReactiveBlock(
391
+ {
392
+ id: 'merge',
393
+ name: 'Merge',
394
+ description: 'Wait for both inputs before continuing',
395
+ category: 'flow',
396
+ icon: 'git-merge',
397
+ color: '#06b6d4',
398
+ inputs: {
399
+ a: input(z.generic(), { name: 'Input A' }),
400
+ b: input(z.generic(), { name: 'Input B' }),
401
+ },
402
+ outputs: {
403
+ out: output(
404
+ z.object({
405
+ a: z.generic(),
406
+ b: z.generic(),
407
+ }),
408
+ { name: 'Output' }
409
+ ),
410
+ },
411
+ config: z.object({}),
412
+ },
413
+ ({ inputs, outputs, log }) => {
414
+ // Combine waits for both inputs to have values
415
+ combine(inputs.a, inputs.b)
416
+ .pipe(
417
+ map(([a, b]) => {
418
+ log('debug', 'Merged inputs');
419
+ return { a, b };
420
+ })
421
+ )
422
+ .to(outputs.out);
423
+ }
424
+ );
425
+
426
+ // ─────────────────────────────────────────────────────────────────────────────
427
+ // Split Block - Send to multiple outputs
428
+ // ─────────────────────────────────────────────────────────────────────────────
429
+
430
+ export const split = defineReactiveBlock(
431
+ {
432
+ id: 'split',
433
+ name: 'Split',
434
+ description: 'Send data to multiple branches',
435
+ category: 'flow',
436
+ icon: 'git-fork',
437
+ color: '#a855f7',
438
+ inputs: {
439
+ in: input(z.generic(), { name: 'Input' }),
440
+ },
441
+ outputs: {
442
+ a: output(z.passthrough('in'), { name: 'Branch A' }),
443
+ b: output(z.passthrough('in'), { name: 'Branch B' }),
444
+ },
445
+ config: z.object({}),
446
+ },
447
+ ({ inputs, outputs, log }) => {
448
+ inputs.in.on((data) => {
449
+ log('debug', 'Splitting to parallel branches');
450
+ outputs.a.emit(data);
451
+ outputs.b.emit(data);
452
+ });
453
+ }
454
+ );
455
+
456
+ // ─────────────────────────────────────────────────────────────────────────────
457
+ // End Block - Terminal block
458
+ // ─────────────────────────────────────────────────────────────────────────────
459
+
460
+ export const end = defineReactiveBlock(
461
+ {
462
+ id: 'end',
463
+ name: 'End',
464
+ description: 'End the workflow branch',
465
+ category: 'flow',
466
+ icon: 'square',
467
+ color: '#dc2626',
468
+ inputs: {
469
+ in: input(z.generic(), { name: 'Input' }),
470
+ },
471
+ outputs: {},
472
+ config: z.object({
473
+ status: z.enum(['success', 'failure']).default('success').describe('End status'),
474
+ }),
475
+ },
476
+ ({ inputs, config, log }) => {
477
+ inputs.in.on((data) => {
478
+ log('info', `Workflow ended with status: ${config.status}`);
479
+ log('debug', `Final data: ${JSON.stringify(data)}`);
480
+ });
481
+ }
482
+ );
483
+
484
+ // ─────────────────────────────────────────────────────────────────────────────
485
+
486
+ log('info', 'Built-in blocks plugin loaded');