blokctl 0.6.21 → 1.0.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.
Files changed (81) hide show
  1. package/dist/__tests__/modular-observability.capstone.e2e.test.js +72 -0
  2. package/dist/commands/create/node.js +46 -66
  3. package/dist/commands/create/project.js +60 -9
  4. package/dist/commands/create/utils/Examples.d.ts +11 -23
  5. package/dist/commands/create/utils/Examples.js +206 -421
  6. package/dist/commands/dev/index.js +40 -1
  7. package/dist/commands/generate/NodeGenerator.d.ts +0 -2
  8. package/dist/commands/generate/NodeGenerator.js +0 -20
  9. package/dist/commands/generate/RuntimeGenerator.d.ts +0 -2
  10. package/dist/commands/generate/RuntimeGenerator.js +0 -19
  11. package/dist/commands/generate/RuntimeGenerator.test.js +0 -29
  12. package/dist/commands/generate/TriggerGenerator.d.ts +0 -2
  13. package/dist/commands/generate/TriggerGenerator.js +0 -19
  14. package/dist/commands/generate/WorkflowGenerator.d.ts +0 -2
  15. package/dist/commands/generate/WorkflowGenerator.js +4 -23
  16. package/dist/commands/generate/WorkflowGenerator.test.js +2 -2
  17. package/dist/commands/generate/e2e/NodeGenerator.e2e.test.js +0 -12
  18. package/dist/commands/generate/e2e/RuntimeGenerator.e2e.test.js +0 -12
  19. package/dist/commands/generate/e2e/TriggerGenerator.e2e.test.js +0 -14
  20. package/dist/commands/generate/prompts/create-fn-node.system.js +10 -17
  21. package/dist/commands/generate/prompts/create-workflow.system.js +114 -375
  22. package/dist/commands/generate/validators/WorkflowValidator.js +80 -5
  23. package/dist/commands/generate/validators/WorkflowValidator.test.js +66 -0
  24. package/dist/commands/monitor/monitor-component.js +5 -5
  25. package/dist/commands/observability/add.d.ts +2 -0
  26. package/dist/commands/observability/add.js +113 -0
  27. package/dist/commands/observability/alerting-module.test.js +43 -0
  28. package/dist/commands/observability/apply.d.ts +10 -0
  29. package/dist/commands/observability/apply.js +11 -0
  30. package/dist/commands/observability/descriptor.d.ts +37 -0
  31. package/dist/commands/observability/descriptor.js +203 -0
  32. package/dist/commands/observability/descriptor.test.d.ts +1 -0
  33. package/dist/commands/observability/descriptor.test.js +40 -0
  34. package/dist/commands/observability/index.d.ts +1 -0
  35. package/dist/commands/observability/index.js +53 -0
  36. package/dist/commands/observability/list.d.ts +2 -0
  37. package/dist/commands/observability/list.js +45 -0
  38. package/dist/commands/observability/logging-module.test.d.ts +1 -0
  39. package/dist/commands/observability/logging-module.test.js +43 -0
  40. package/dist/commands/observability/obs-stack-module.test.d.ts +1 -0
  41. package/dist/commands/observability/obs-stack-module.test.js +33 -0
  42. package/dist/commands/observability/remove.d.ts +2 -0
  43. package/dist/commands/observability/remove.js +62 -0
  44. package/dist/commands/observability/shared.d.ts +6 -0
  45. package/dist/commands/observability/shared.js +23 -0
  46. package/dist/commands/observability/status.d.ts +2 -0
  47. package/dist/commands/observability/status.js +36 -0
  48. package/dist/commands/observability/tracing-module.test.d.ts +1 -0
  49. package/dist/commands/observability/tracing-module.test.js +42 -0
  50. package/dist/commands/profile/index.js +7 -10
  51. package/dist/commands/watch/format.d.ts +23 -0
  52. package/dist/commands/watch/format.js +60 -0
  53. package/dist/commands/watch/index.d.ts +1 -0
  54. package/dist/commands/watch/index.js +53 -0
  55. package/dist/commands/watch/sse.d.ts +16 -0
  56. package/dist/commands/watch/sse.js +82 -0
  57. package/dist/index.d.ts +2 -0
  58. package/dist/index.js +4 -0
  59. package/dist/services/obs-setup.d.ts +5 -0
  60. package/dist/services/obs-setup.js +68 -0
  61. package/dist/services/obs-setup.test.d.ts +1 -0
  62. package/dist/services/obs-setup.test.js +71 -0
  63. package/dist/services/obs-tiers.d.ts +9 -0
  64. package/dist/services/obs-tiers.js +16 -0
  65. package/dist/services/observability-mutations.d.ts +4 -0
  66. package/dist/services/observability-mutations.js +46 -0
  67. package/dist/services/observability-mutations.test.d.ts +1 -0
  68. package/dist/services/observability-mutations.test.js +57 -0
  69. package/dist/services/runtime-setup.d.ts +12 -1
  70. package/dist/services/runtime-setup.js +274 -14
  71. package/dist/studio-dist/assets/{index-BD8_9YPN.js → index-CnFqCRQe.js} +17 -17
  72. package/dist/studio-dist/index.html +1 -1
  73. package/package.json +3 -3
  74. package/dist/commands/generate/GenerationAnalytics.d.ts +0 -61
  75. package/dist/commands/generate/GenerationAnalytics.js +0 -163
  76. package/dist/commands/generate/GenerationAnalytics.test.js +0 -407
  77. package/dist/commands/generate/PromptVersioning.d.ts +0 -25
  78. package/dist/commands/generate/PromptVersioning.js +0 -71
  79. package/dist/commands/generate/PromptVersioning.test.js +0 -120
  80. /package/dist/{commands/generate/GenerationAnalytics.test.d.ts → __tests__/modular-observability.capstone.e2e.test.d.ts} +0 -0
  81. /package/dist/commands/{generate/PromptVersioning.test.d.ts → observability/alerting-module.test.d.ts} +0 -0
@@ -32,36 +32,27 @@ const package_dev_dependencies = {
32
32
  "@types/ejs": "^3.1.5",
33
33
  "@types/pg": "^8.11.11",
34
34
  };
35
- const python3_file = `
36
- from core.blok import BlokService
37
- from core.types.context import Context
38
- from core.types.blok_response import BlokResponse
39
- from core.types.global_error import GlobalError
40
- from typing import Any, Dict
41
- import traceback
42
-
43
- class Node(BlokService):
44
- def __init__(self):
45
- BlokService.__init__(self)
46
- self.input_schema = {}
47
- self.output_schema = {}
48
-
49
- async def handle(self, ctx: Context, inputs: Dict[str, Any]) -> BlokResponse:
50
- response = BlokResponse()
51
-
52
- try:
53
- response.setSuccess({ "message": "Hello World from Python3!" })
54
- except Exception as error:
55
- err = GlobalError(error)
56
- err.setCode(500)
57
- err.setName(self.name)
58
-
59
- stack_trace = traceback.format_exc()
60
- err.setStack(stack_trace)
61
- response.success = False
62
- response.setError(err)
63
-
64
- return response
35
+ const python3_file = `from pydantic import BaseModel
36
+
37
+ from blok import node
38
+ from blok.types.context import Context
39
+
40
+
41
+ class Input(BaseModel):
42
+ """Validated inputs for the {{NODE_NAME}} node."""
43
+
44
+ name: str = "world"
45
+
46
+
47
+ class Output(BaseModel):
48
+ message: str
49
+
50
+
51
+ @node("{{NODE_NAME}}", "Describe what {{NODE_NAME}} does")
52
+ def run(ctx: Context, input: Input) -> Output:
53
+ # \`input\` is already validated against Input; the return is validated
54
+ # against Output and serialized for you.
55
+ return Output(message=f"Hello, {input.name}!")
65
56
  `;
66
57
  const examples_url = `
67
58
  Examples:
@@ -121,316 +112,151 @@ autorestart=true
121
112
  stderr_logfile=/var/log/python.err.log
122
113
  stdout_logfile=/var/log/python.out.log
123
114
  `;
124
- const go_node_file = `package main
115
+ const go_node_file = `// {{NODE_NAME}} — a Blok node for the Go runtime.
116
+ package {{NODE_PKG}}
125
117
 
126
118
  import (
127
- "github.com/blok/sdk"
119
+ blok "github.com/nickincloud/blok-go"
128
120
  )
129
121
 
130
- type HelloWorldNode struct{}
122
+ // {{NODE_NAME_PASCAL}}Node implements blok.NodeHandler.
123
+ type {{NODE_NAME_PASCAL}}Node struct{}
131
124
 
132
- func (n *HelloWorldNode) Execute(ctx *sdk.Context, config map[string]interface{}) (*sdk.ExecutionResult, error) {
133
- // Access request body
125
+ // Execute runs the node. Input arrives on ctx.Request; config holds the step's
126
+ // inputs from the workflow. Return any JSON-serialisable value (or an error).
127
+ func (n *{{NODE_NAME_PASCAL}}Node) Execute(ctx *blok.Context, config map[string]interface{}) (interface{}, error) {
134
128
  name := "World"
135
- if body, ok := ctx.Request.Body.(map[string]interface{}); ok {
136
- if nameVal, ok := body["name"].(string); ok {
137
- name = nameVal
129
+ if body := ctx.Request.BodyMap(); body != nil {
130
+ if v, ok := body["name"].(string); ok {
131
+ name = v
138
132
  }
139
133
  }
140
134
 
141
- // Access configuration
142
135
  prefix := "Hello"
143
- if prefixVal, ok := config["prefix"].(string); ok {
144
- prefix = prefixVal
136
+ if v, ok := config["prefix"].(string); ok {
137
+ prefix = v
145
138
  }
146
139
 
147
- // Store result in context for downstream nodes
148
- ctx.Vars["greeting"] = prefix + ", " + name + "!"
149
-
150
- // Return successful result
151
- return &sdk.ExecutionResult{
152
- Success: true,
153
- Data: map[string]interface{}{
154
- "message": prefix + ", " + name + "!",
155
- "timestamp": sdk.GetCurrentTimestamp(),
156
- "language": "Go",
157
- },
158
- Errors: nil,
140
+ return map[string]interface{}{
141
+ "message": prefix + ", " + name + "!",
142
+ "language": "Go",
159
143
  }, nil
160
144
  }
161
145
 
162
- func main() {
163
- // Register node
164
- registry := sdk.NewNodeRegistry()
165
- registry.Register("{{NODE_NAME}}", &HelloWorldNode{})
166
-
167
- // Start HTTP server
168
- server := sdk.NewHTTPServer(registry, ":8080")
169
- if err := server.Start(); err != nil {
170
- panic(err)
171
- }
146
+ // Register wires this node into the runtime registry. The generated
147
+ // register_user_nodes.go calls it for every node under runtimes/go/nodes.
148
+ func Register(registry *blok.NodeRegistry) {
149
+ registry.Register("{{NODE_NAME}}", &{{NODE_NAME_PASCAL}}Node{})
172
150
  }
173
151
  `;
174
- const go_mod_file = `module github.com/blok/nodes/{{NODE_NAME}}
175
-
176
- go 1.21
177
-
178
- require github.com/blok/sdk v1.0.0
179
- `;
180
- const go_dockerfile = `FROM golang:1.21-alpine AS builder
152
+ const java_node_file = `package com.blok.blok.nodes;
181
153
 
182
- WORKDIR /app
183
- COPY go.mod go.sum ./
184
- RUN go mod download
185
-
186
- COPY . .
187
- RUN CGO_ENABLED=0 GOOS=linux go build -o /node main.go
188
-
189
- FROM alpine:latest
190
- RUN apk --no-cache add ca-certificates
191
- WORKDIR /root/
192
- COPY --from=builder /node .
193
-
194
- EXPOSE 8080
195
- HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
196
- CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
197
-
198
- CMD ["./node"]
199
- `;
200
- const java_node_file = `package com.blok.nodes;
154
+ import com.blok.blok.node.NodeHandler;
155
+ import com.blok.blok.types.Context;
201
156
 
202
- import com.blok.runtime.Blok;
203
- import com.blok.runtime.NodeRegistry;
204
- import com.blok.server.RuntimeServer;
157
+ import java.time.Instant;
205
158
  import java.util.HashMap;
206
159
  import java.util.Map;
207
160
 
208
- public class HelloWorldNode implements Blok.NodeHandler {
161
+ /**
162
+ * {{NODE_NAME}} — a Blok node for the Java runtime.
163
+ *
164
+ * <p>Input arrives on {@code ctx.getRequest().bodyMap()} (a Map, or null when
165
+ * the body isn't an object); {@code config} holds the step's inputs from the
166
+ * workflow. Return any JSON-serialisable value (a Map here), or throw to fail
167
+ * the step. {@code blokctl dev} discovers this class under
168
+ * {@code runtimes/java/nodes/} and registers it into the Java runtime.
169
+ */
170
+ public class {{NODE_NAME_PASCAL}}Node implements NodeHandler {
171
+
209
172
  @Override
210
- public Blok.ExecutionResult execute(Blok.Context ctx, Map<String, Object> config) {
211
- try {
212
- // Access request body
213
- String name = "World";
214
- if (ctx.request.body != null && ctx.request.body.containsKey("name")) {
215
- name = (String) ctx.request.body.get("name");
216
- }
217
-
218
- // Access configuration
219
- String prefix = "Hello";
220
- if (config != null && config.containsKey("prefix")) {
221
- prefix = (String) config.get("prefix");
222
- }
223
-
224
- // Store result in context for downstream nodes
225
- String greeting = prefix + ", " + name + "!";
226
- ctx.vars.put("greeting", greeting);
227
-
228
- // Build response data
229
- Map<String, Object> data = new HashMap<>();
230
- data.put("message", greeting);
231
- data.put("timestamp", System.currentTimeMillis());
232
- data.put("language", "Java");
233
-
234
- // Return successful result
235
- return new Blok.ExecutionResult(true, data, null, null, null);
236
- } catch (Exception e) {
237
- return new Blok.ExecutionResult(false, null, e.getMessage(), null, null);
173
+ public Object execute(Context ctx, Map<String, Object> config) throws Exception {
174
+ String name = "World";
175
+ Map<String, Object> body = ctx.getRequest().bodyMap();
176
+ if (body != null && body.get("name") instanceof String s && !s.isEmpty()) {
177
+ name = s;
238
178
  }
239
- }
240
179
 
241
- public static void main(String[] args) {
242
- try {
243
- // Register node
244
- NodeRegistry registry = new NodeRegistry();
245
- registry.register("{{NODE_NAME}}", new HelloWorldNode());
246
-
247
- // Start HTTP server
248
- RuntimeServer server = new RuntimeServer(registry, 8080);
249
- server.start();
250
- } catch (Exception e) {
251
- e.printStackTrace();
252
- System.exit(1);
180
+ String prefix = "Hello";
181
+ if (config != null && config.get("prefix") instanceof String s && !s.isEmpty()) {
182
+ prefix = s;
253
183
  }
254
- }
255
- }
256
- `;
257
- const java_pom_file = `<?xml version="1.0" encoding="UTF-8"?>
258
- <project xmlns="http://maven.apache.org/POM/4.0.0"
259
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
260
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
261
- http://maven.apache.org/xsd/maven-4.0.0.xsd">
262
- <modelVersion>4.0.0</modelVersion>
263
-
264
- <groupId>com.blok</groupId>
265
- <artifactId>{{NODE_NAME}}</artifactId>
266
- <version>1.0.0</version>
267
-
268
- <properties>
269
- <maven.compiler.source>17</maven.compiler.source>
270
- <maven.compiler.target>17</maven.compiler.target>
271
- <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
272
- </properties>
273
-
274
- <dependencies>
275
- <dependency>
276
- <groupId>com.google.code.gson</groupId>
277
- <artifactId>gson</artifactId>
278
- <version>2.10.1</version>
279
- </dependency>
280
- </dependencies>
281
-
282
- <build>
283
- <plugins>
284
- <plugin>
285
- <groupId>org.apache.maven.plugins</groupId>
286
- <artifactId>maven-shade-plugin</artifactId>
287
- <version>3.5.0</version>
288
- <executions>
289
- <execution>
290
- <phase>package</phase>
291
- <goals>
292
- <goal>shade</goal>
293
- </goals>
294
- <configuration>
295
- <transformers>
296
- <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
297
- <mainClass>com.blok.nodes.HelloWorldNode</mainClass>
298
- </transformer>
299
- </transformers>
300
- </configuration>
301
- </execution>
302
- </executions>
303
- </plugin>
304
- </plugins>
305
- </build>
306
- </project>
307
- `;
308
- const java_dockerfile = `FROM maven:3.9-eclipse-temurin-17 AS builder
309
-
310
- WORKDIR /app
311
- COPY pom.xml .
312
- RUN mvn dependency:go-offline
313
-
314
- COPY src ./src
315
- RUN mvn clean package -DskipTests
316
-
317
- FROM eclipse-temurin:17-jre-alpine
318
184
 
319
- WORKDIR /root/
320
- COPY --from=builder /app/target/{{NODE_NAME}}-1.0.0.jar ./app.jar
185
+ String message = prefix + ", " + name + "!";
321
186
 
322
- EXPOSE 8080
323
- HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
324
- CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
187
+ // Publish a value for downstream nodes via context vars.
188
+ ctx.setVar("greeting", message);
325
189
 
326
- CMD ["java", "-jar", "app.jar"]
190
+ Map<String, Object> result = new HashMap<>();
191
+ result.put("message", message);
192
+ result.put("timestamp", Instant.now().toString());
193
+ result.put("language", "Java");
194
+ return result;
195
+ }
196
+ }
327
197
  `;
328
198
  const rust_node_file = `use async_trait::async_trait;
329
- use blok::{NodeHandler, NodeRegistry, Context};
199
+ use blok::registry::NodeRegistry;
200
+ use blok::{Context, NodeHandler};
330
201
  use std::collections::HashMap;
331
202
 
332
- /// {{NODE_NAME}} - A Blok node implemented in Rust
333
- struct {{NODE_NAME_PASCAL}};
203
+ /// {{NODE_NAME_PASCAL}}Node implements blok::NodeHandler.
204
+ struct {{NODE_NAME_PASCAL}}Node;
334
205
 
335
206
  #[async_trait]
336
- impl NodeHandler for {{NODE_NAME_PASCAL}} {
207
+ impl NodeHandler for {{NODE_NAME_PASCAL}}Node {
208
+ /// Execute runs the node. Input arrives on ctx.request; config holds the
209
+ /// step's inputs from the workflow. Return any JSON value (or an error).
337
210
  async fn execute(
338
211
  &self,
339
212
  ctx: &mut Context,
340
213
  config: &HashMap<String, serde_json::Value>,
341
214
  ) -> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>> {
342
- // Access request body
343
- let name = ctx.request.body
215
+ let name = ctx
216
+ .request
217
+ .body
344
218
  .get("name")
345
219
  .and_then(|v| v.as_str())
346
220
  .unwrap_or("World");
347
221
 
348
- // Access configuration
349
222
  let prefix = config
350
223
  .get("prefix")
351
224
  .and_then(|v| v.as_str())
352
225
  .unwrap_or("Hello");
353
226
 
354
- let message = format!("{}, {}!", prefix, name);
355
-
356
- // Store in context vars for downstream nodes
357
- ctx.vars.insert(
358
- "greeting".to_string(),
359
- serde_json::Value::String(message.clone()),
360
- );
361
-
362
- // Return response
363
227
  Ok(serde_json::json!({
364
- "message": message,
228
+ "message": format!("{}, {}!", prefix, name),
365
229
  "language": "Rust"
366
230
  }))
367
231
  }
368
232
  }
369
233
 
370
- #[tokio::main]
371
- async fn main() {
372
- // Initialize tracing
373
- tracing_subscriber::fmt::init();
374
-
375
- // Register nodes
376
- let mut registry = NodeRegistry::new("1.0.0");
377
- registry.register("{{NODE_NAME}}", {{NODE_NAME_PASCAL}});
378
-
379
- // Start HTTP server
380
- blok::server::serve(registry, 8080).await.unwrap();
234
+ /// register wires this node into the runtime registry. The generated
235
+ /// user_nodes/mod.rs calls it for every node under runtimes/rust/nodes.
236
+ pub fn register(registry: &mut NodeRegistry) {
237
+ registry.register("{{NODE_NAME}}", {{NODE_NAME_PASCAL}}Node);
381
238
  }
382
239
  `;
383
- const rust_cargo_file = `[package]
384
- name = "{{NODE_NAME}}"
385
- version = "1.0.0"
386
- edition = "2021"
387
-
388
- [[bin]]
389
- name = "{{NODE_NAME}}"
390
- path = "src/main.rs"
391
-
392
- [dependencies]
393
- blok = { path = "../../sdk" }
394
- tokio = { version = "1", features = ["full"] }
395
- serde = { version = "1", features = ["derive"] }
396
- serde_json = "1"
397
- async-trait = "0.1"
398
- tracing = "0.1"
399
- tracing-subscriber = "0.3"
400
- `;
401
- const rust_dockerfile = `FROM rust:1.77-alpine AS builder
402
-
403
- RUN apk add --no-cache musl-dev
404
-
405
- WORKDIR /app
406
- COPY Cargo.toml Cargo.lock ./
407
- RUN mkdir -p src && echo 'fn main() {}' > src/main.rs && \\
408
- cargo build --release 2>/dev/null || true && rm -rf src
409
-
410
- COPY . .
411
- RUN cargo build --release
412
-
413
- FROM alpine:latest
414
- RUN apk --no-cache add ca-certificates
415
- WORKDIR /root/
416
- COPY --from=builder /app/target/release/{{NODE_NAME}} .
417
-
418
- EXPOSE 8080
419
- HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
420
- CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
421
-
422
- CMD ["./{{NODE_NAME}}"]
423
- `;
424
240
  const csharp_node_file = `using System.Text.Json;
425
- using Blok.Runtime;
241
+ using Blok.Core.Node;
242
+ using Blok.Core.Types;
426
243
 
427
- namespace Blok.Runtime.Nodes;
244
+ namespace Blok.Core.Nodes;
428
245
 
246
+ /// <summary>
247
+ /// {{NODE_NAME_PASCAL}}Node — a Blok node for the C# runtime. Discovered under
248
+ /// runtimes/csharp/nodes/ and registered into the shared gRPC server alongside
249
+ /// the built-in nodes (same model as the other SDKs). Run \`blokctl dev\`; no
250
+ /// per-node csproj/Dockerfile — it isn't a standalone service.
251
+ /// </summary>
429
252
  public class {{NODE_NAME_PASCAL}}Node : INodeHandler
430
253
  {
254
+ /// <summary>
255
+ /// Execute the node. Input arrives on ctx.Request; config holds the step's
256
+ /// inputs from the workflow. Return any JSON-serialisable value.
257
+ /// </summary>
431
258
  public Task<JsonElement> ExecuteAsync(Context ctx, Dictionary<string, JsonElement> config)
432
259
  {
433
- // Access request body
434
260
  var name = "World";
435
261
  if (ctx.Request.Body.ValueKind == JsonValueKind.Object &&
436
262
  ctx.Request.Body.TryGetProperty("name", out var nameEl) &&
@@ -439,7 +265,6 @@ public class {{NODE_NAME_PASCAL}}Node : INodeHandler
439
265
  name = nameEl.GetString() ?? "World";
440
266
  }
441
267
 
442
- // Access configuration
443
268
  var prefix = "Hello";
444
269
  if (config.TryGetValue("prefix", out var prefixEl) &&
445
270
  prefixEl.ValueKind == JsonValueKind.String)
@@ -448,11 +273,8 @@ public class {{NODE_NAME_PASCAL}}Node : INodeHandler
448
273
  }
449
274
 
450
275
  var message = $"{prefix}, {name}!";
276
+ ctx.SetVar("greeting", message);
451
277
 
452
- // Store in context for downstream nodes
453
- ctx.Vars["greeting"] = JsonSerializer.SerializeToElement(message);
454
-
455
- // Return response
456
278
  var result = JsonSerializer.SerializeToElement(new
457
279
  {
458
280
  message,
@@ -464,154 +286,58 @@ public class {{NODE_NAME_PASCAL}}Node : INodeHandler
464
286
  }
465
287
  }
466
288
  `;
467
- const csharp_csproj_file = `<Project Sdk="Microsoft.NET.Sdk.Web">
468
- <PropertyGroup>
469
- <TargetFramework>net8.0</TargetFramework>
470
- <RootNamespace>Blok.Runtime</RootNamespace>
471
- <ImplicitUsings>enable</ImplicitUsings>
472
- <Nullable>enable</Nullable>
473
- </PropertyGroup>
474
- </Project>
475
- `;
476
- const csharp_dockerfile = `FROM mcr.microsoft.com/dotnet/sdk:8.0 AS builder
477
- WORKDIR /app
478
- COPY *.csproj .
479
- RUN dotnet restore
480
- COPY . .
481
- RUN dotnet publish -c Release -o /out
482
-
483
- FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine
484
- WORKDIR /app
485
- COPY --from=builder /out .
486
-
487
- EXPOSE 8080
488
- ENV PORT=8080
489
- HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
490
- CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
491
-
492
- CMD ["dotnet", "BlokRuntime.dll"]
493
- `;
494
289
  const php_node_file = `<?php
495
290
 
496
- namespace Blok\\Nodes;
291
+ declare(strict_types=1);
497
292
 
498
- use Blok\\NodeHandler;
499
- use Blok\\Context;
293
+ namespace Blok\\Blok\\Nodes\\{{NODE_NAME_PASCAL}};
500
294
 
501
- class {{NODE_NAME_PASCAL}}Node implements NodeHandler
295
+ use Blok\\Blok\\Node\\NodeHandler;
296
+ use Blok\\Blok\\Types\\Context;
297
+
298
+ /**
299
+ * {{NODE_NAME}} — a Blok node for the PHP runtime.
300
+ *
301
+ * Input arrives on \$ctx->request; \$config holds the step's inputs from the
302
+ * workflow. Return any JSON-serialisable value (array, scalar, etc.).
303
+ */
304
+ final class {{NODE_NAME_PASCAL}}Node implements NodeHandler
502
305
  {
503
306
  public function execute(Context $ctx, array $config): mixed
504
307
  {
505
- // Access request body
506
- $name = $ctx->request->body['name'] ?? 'World';
507
-
508
- // Access configuration
509
- $prefix = $config['prefix'] ?? 'Hello';
510
-
511
- $message = "$prefix, $name!";
308
+ $name = $ctx->request->bodyStr('name') ?? 'World';
309
+ $prefix = isset($config['prefix']) && is_string($config['prefix'])
310
+ ? $config['prefix']
311
+ : 'Hello';
512
312
 
513
- // Store in context for downstream nodes
514
- $ctx->vars['greeting'] = $message;
515
-
516
- // Return response
517
313
  return [
518
- 'message' => $message,
519
- 'timestamp' => date('c'),
314
+ 'message' => sprintf('%s, %s!', $prefix, $name),
520
315
  'language' => 'PHP',
521
316
  ];
522
317
  }
523
318
  }
524
319
  `;
525
- const php_composer_file = `{
526
- "name": "blok/{{NODE_NAME}}",
527
- "type": "project",
528
- "require": {
529
- "php": ">=8.2",
530
- "react/http": "^1.9",
531
- "react/socket": "^1.15"
532
- },
533
- "autoload": {
534
- "psr-4": {
535
- "Blok\\\\": "src/"
536
- }
537
- }
538
- }
539
- `;
540
- const php_dockerfile = `FROM php:8.2-cli-alpine AS builder
541
- WORKDIR /app
542
- COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
543
- COPY composer.json .
544
- RUN composer install --no-dev --optimize-autoloader
545
- COPY . .
546
-
547
- FROM php:8.2-cli-alpine
548
- WORKDIR /app
549
- COPY --from=builder /app .
550
-
551
- EXPOSE 8080
552
- ENV PORT=8080
553
- HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
554
- CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
555
-
556
- CMD ["php", "index.php"]
557
- `;
558
- const ruby_node_file = `require_relative '../../lib/blok'
559
-
560
- module Blok
561
- module Nodes
562
- class {{NODE_NAME_PASCAL}}Node < Blok::NodeHandler
563
- def execute(ctx, config)
564
- # Access request body
565
- name = ctx.request.body.is_a?(Hash) ? ctx.request.body['name'] : nil
566
- name ||= 'World'
320
+ const ruby_node_file = `# frozen_string_literal: true
567
321
 
568
- # Access configuration
569
- prefix = config['prefix'] || 'Hello'
322
+ # {{NODE_NAME_PASCAL}}Node — registered as "{{NODE_NAME}}" by the Ruby runtime.
323
+ #
324
+ # Request body arrives on ctx.request (use body_str(key) for a String|nil);
325
+ # config holds the step's inputs from the workflow. Return any
326
+ # JSON-serialisable Hash. Publish to downstream steps with ctx.set_var(k, v).
327
+ class {{NODE_NAME_PASCAL}}Node < Blok::Node::NodeHandler
328
+ def execute(ctx, config)
329
+ name = ctx.request.body_str("name") || "World"
330
+ prefix = config["prefix"] || "Hello"
570
331
 
571
- message = "#{prefix}, #{name}!"
332
+ message = "#{prefix}, #{name}!"
333
+ ctx.set_var("greeting", message)
572
334
 
573
- # Store in context for downstream nodes
574
- ctx.vars['greeting'] = message
575
-
576
- # Return response
577
- {
578
- 'message' => message,
579
- 'timestamp' => Time.now.utc.iso8601,
580
- 'language' => 'Ruby'
581
- }
582
- end
583
- end
335
+ {
336
+ "message" => message,
337
+ "language" => "Ruby"
338
+ }
584
339
  end
585
- end
586
- `;
587
- const ruby_gemfile = `source 'https://rubygems.org'
588
-
589
- ruby '>= 3.1'
590
-
591
- gem 'sinatra', '~> 4.0'
592
- gem 'puma', '~> 6.4'
593
- gem 'rackup', '~> 2.1'
594
- `;
595
- const ruby_dockerfile = `FROM ruby:3.2-alpine AS builder
596
- RUN apk add --no-cache build-base
597
- WORKDIR /app
598
- COPY Gemfile Gemfile.lock ./
599
- RUN bundle install --without development test
600
-
601
- FROM ruby:3.2-alpine
602
- RUN apk --no-cache add ca-certificates wget
603
- WORKDIR /app
604
- COPY --from=builder /usr/local/bundle /usr/local/bundle
605
- COPY . .
606
-
607
- EXPOSE 8080
608
- ENV PORT=8080
609
- ENV RACK_ENV=production
610
- HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
611
- CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
612
-
613
- CMD ["bundle", "exec", "puma", "-b", "tcp://0.0.0.0:8080"]
614
- `;
340
+ end`;
615
341
  const agents_md = `
616
342
  # AGENTS.md — Blok Framework AI Context
617
343
 
@@ -623,7 +349,52 @@ Blok is a **multi-trigger, multi-runtime workflow framework**. A workflow is a d
623
349
  The 9 trigger types: \`http\`, \`worker\`, \`cron\`, \`pubsub\`, \`sse\`, \`websocket\`, \`webhook\`, \`mcp\`, \`grpc\`.
624
350
  The 8 runtimes: \`typescript\` (in-process), \`go\`, \`rust\`, \`java\`, \`csharp\`, \`php\`, \`ruby\`, \`python3\`.
625
351
 
626
- The canonical workflow form is \`workflow({ name, version, trigger, steps })\` from \`@blokjs/helper\`. The same shape works for all 9 triggers — only the \`trigger:\` block changes.
352
+ The canonical TypeScript form is the **typed-handle DSL** from \`@blokjs/core\`: \`workflow(name, { version, trigger }, (entry) => { ... })\`, where each \`step()\` returns a typed handle you reference directly — **no \`$\`, no \`js/\`, no raw \`ctx\` strings.** The object-style \`workflow({ name, version, trigger, steps: [...] })\` from \`@blokjs/helper\` and JSON workflows are equivalent (all three compile to the same IR) and remain fully supported. The same shape works for all 9 triggers — only the \`trigger:\` block changes.
353
+
354
+ ---
355
+
356
+ ## 0. THE TYPED-HANDLE DSL (\`@blokjs/core\`) — preferred for TypeScript
357
+
358
+ \`\`\`ts
359
+ import { workflow, step, branch, forEach, switchOn, tryCatch, http, tpl, gt } from "@blokjs/core";
360
+
361
+ export default workflow("order-intake", { version: "1.0.0", trigger: http.post("/orders") }, (req) => {
362
+ // step(id, node, inputs) → a TYPED handle shaped like the node's output.
363
+ const order = step("validate", validateOrder, { qty: req.body.qty });
364
+
365
+ // Reference a handle field anywhere downstream — it records a ref the runner resolves.
366
+ step("summary", summarize, { line: tpl\`order of \${order.qty} item(s)\` });
367
+
368
+ // Control flow: a comparator (gt/eq/lt/...) builds the condition; arms are callbacks.
369
+ branch("lane", gt(order.qty, 10), {
370
+ then: () => { step("bulk", routeOrder, { lane: "bulk" }); },
371
+ else: () => { step("standard", routeOrder, { lane: "standard" }); },
372
+ });
373
+ });
374
+ \`\`\`
375
+
376
+ **Trigger config:** \`http.{get,post,put,delete,patch,any}(path?, opts?)\` for HTTP (omit \`path\` for file-based routing). For any other trigger pass the raw block as \`trigger:\` — e.g. \`{ worker: { queue: "jobs" } }\`, \`{ cron: { schedule: "0 2 * * *" } }\`, \`{ webhook: { source: "stripe" } }\`.
377
+
378
+ **The entry handle** (the callback's argument) is the trigger payload — typed and conventionally named per trigger: \`http\`→\`req\`, \`webhook\`→\`event\`, \`cron\`→\`tick\`, \`worker\`→\`job\`, \`pubsub\`→\`msg\`, \`grpc\`→\`rpc\` (others get a loose handle). Read \`req.body\`, \`req.params.id\`, \`req.query.q\`, \`req.headers["x-…"]\`.
379
+
380
+ **Handles & persistence:**
381
+ - \`const h = step("id", node, inputs)\` — every step auto-persists to \`ctx.state["id"]\` on success; \`h\` / \`h.field\` is how you reference it later (never \`$.state.id\`).
382
+ - 4th arg \`opts\`: \`{ as: "name" }\` roots the handle at \`state["name"]\`; \`{ spread: true }\` flattens \`result.data\` into state (mutually exclusive with \`as\`); \`{ ephemeral: true }\` skips persistence (handle then unreadable downstream); also \`idempotencyKey\`, \`retry\`, \`maxDuration\`, \`type: "runtime.<lang>"\`.
383
+ - \`tpl\`…\${h.field}…\`\` builds a string with embedded handle refs.
384
+ - Comparators (typed): \`eq, ne, gt, gte, lt, lte, not\`. A bare boolean handle is a truthiness check.
385
+ - \`defineNode\` is re-exported from \`@blokjs/core\` — author nodes exactly as before; just RETURN output (never write \`ctx.state\`/\`ctx.vars\`).
386
+
387
+ **Control-flow primitives** (all from \`@blokjs/core\`):
388
+ - \`branch(id, cond, { then: () => {…}, else?: () => {…} })\` — \`cond\` is a comparator or a boolean handle.
389
+ - \`forEach(iterable, (item, index) => { step(…) }, { as?, mode?, concurrency? })\` — \`iterable\` is a handle; \`item\`/\`index\` are per-iteration handles; \`mode: "parallel"\` bounds with \`concurrency\` (default 10).
390
+ - \`switchOn(discriminant, { cases: [{ when: "push", do: () => {…} }], default?: () => {…} }, { id })\` — \`when\` labels are STATIC literals (a handle never matches).
391
+ - \`tryCatch(id, { try: () => {…}, catch: (error) => { step("log", logger, { msg: error.message }); }, finally?: () => {…} })\` — \`error\` is a typed handle (\`.message\`/\`.name\`/\`.stack\`/\`.code\`/\`.stepId\`).
392
+
393
+ **The four footguns** (full detail: \`docs/d/primitives/handles-and-footguns.mdx\`):
394
+ 1. **Arm-scoped handles don't escape their arm.** A handle minted inside a \`branch\`/\`switchOn\`/\`tryCatch\`/\`forEach\` arm is unreadable outside it. Return a value from the control-flow step, or have both arms write one \`as:\` key.
395
+ 2. **Ephemeral handles are unreadable.** Reading an \`{ ephemeral: true }\` step's handle resolves to \`undefined\`.
396
+ 3. **Never reuse a step \`id\`** — including across mutually-exclusive arms. Ids are a flat per-workflow map; the runner throws at load time. Use \`as:\` when two arms must write the same downstream key.
397
+ 4. **Never name a \`forEach\` \`as:\`/\`asIndex\` after an existing step id** (or another loop's \`as\`) — they share \`ctx.state\`; the runner throws at load time.
627
398
 
628
399
  ---
629
400
 
@@ -1132,7 +903,7 @@ Process-global middleware: \`WorkflowRegistry.getInstance().setGlobalMiddleware(
1132
903
  Always \`export default defineNode(...)\`. Never class-based \`BlokService\`. Zod input/output are mandatory.
1133
904
 
1134
905
  \`\`\`ts
1135
- import { defineNode } from "@blokjs/runner";
906
+ import { defineNode } from "@blokjs/core";
1136
907
  import { z } from "zod";
1137
908
 
1138
909
  export default defineNode({
@@ -1608,7 +1379,7 @@ Per-step reliability lives on the step: \`idempotencyKey\` (cache by \`(workflow
1608
1379
  Always \`export default defineNode(...)\` (TS) — never class-based \`BlokService\`. Zod input/output are mandatory. Never write \`ctx.state\` from a node — return your output and let the runner persist it (use \`ctx.publish(name, value)\` for a true side-channel). No \`any\` types — use \`z.unknown()\`.
1609
1380
 
1610
1381
  \`\`\`typescript
1611
- import { defineNode } from "@blokjs/runner";
1382
+ import { defineNode } from "@blokjs/core";
1612
1383
  import { z } from "zod";
1613
1384
 
1614
1385
  export default defineNode({
@@ -1662,7 +1433,21 @@ Registration is **manual** in non-TS runtimes — importing the module runs the
1662
1433
 
1663
1434
  ## 4. Generating Workflows
1664
1435
 
1665
- Canonical form: \`workflow({ name, version, trigger, steps })\` from \`@blokjs/helper\` one object literal, no chained builder, no separate \`nodes{}\` map. \`name\` 3 chars, \`version\` 5 chars (semver). Reference earlier outputs with \`$.state.<id>\` / \`$.req.body\`. Use \`branch\`, \`switchOn\`, \`forEach\`, \`loop\`, \`tryCatch\` (all from \`@blokjs/helper\`) for control flow.
1436
+ **Preferred (TypeScript): the typed-handle DSL from \`@blokjs/core\`.** \`workflow(name, { version, trigger }, (entry) => { ... })\` each \`step(id, node, inputs)\` returns a typed handle you reference directly; no \`$\`, no \`js/\`, no raw \`ctx\` strings. \`http.{get,post,…}(path?)\` builds the trigger; other triggers pass the raw block (\`{ worker: { queue } }\`, \`{ cron: { schedule } }\`, …). Entry name per trigger: \`http\`→\`req\`, \`worker\`→\`job\`, \`cron\`→\`tick\`, \`webhook\`→\`event\`, \`pubsub\`→\`msg\`, \`grpc\`→\`rpc\`.
1437
+
1438
+ \`\`\`typescript
1439
+ import { workflow, step, branch, gt, http } from "@blokjs/core";
1440
+
1441
+ export default workflow("Process Order", { version: "1.0.0", trigger: http.post("/orders") }, (req) => {
1442
+ const order = step("validate", orderValidator, { order: req.body }); // typed handle
1443
+ step("save", orderStore, { data: order }); // reference it directly
1444
+ branch("big", gt(order.total, 100), { then: () => { step("vip", flagVip, { id: order.id }); } });
1445
+ });
1446
+ \`\`\`
1447
+
1448
+ Control flow (all from \`@blokjs/core\`): \`branch(id, cond, {then,else?})\`, \`forEach(iterable, (item,i)=>{}, {as?,mode?})\`, \`switchOn(disc, {cases:[{when,do}],default?}, {id})\`, \`tryCatch(id, {try,catch:(err)=>{},finally?})\`; comparators \`eq/ne/gt/gte/lt/lte/not\`; \`tpl\`…\${h.x}…\`\` for strings. The **four footguns**: arm-scoped handles don't escape their arm; ephemeral handles read \`undefined\`; never reuse a step \`id\`; never name a \`forEach\` \`as:\` after an existing id. See \`docs/d/primitives/handles-and-footguns.mdx\`.
1449
+
1450
+ **Equivalent object-style form** (\`@blokjs/helper\`, also valid; JSON mirrors it): one object literal, \`steps: [...]\`, reference outputs with \`$.state.<id>\` / \`$.req.body\`.
1666
1451
 
1667
1452
  \`\`\`typescript
1668
1453
  import { workflow, $ } from "@blokjs/helper";
@@ -1735,7 +1520,7 @@ branch({ id: "route",
1735
1520
  - Do NOT generate class-based \`BlokService\` nodes or use \`any\` types — always \`defineNode()\` (TS) / \`@node\` (Python) with Zod/Pydantic schemas.
1736
1521
  - Do NOT use ESLint/Prettier — this project uses Biome. Do NOT edit auto-generated files in \`.blok/runtimes/\`.
1737
1522
  `;
1738
- const function_first_node_file = `import { defineNode } from "@blokjs/runner";
1523
+ const function_first_node_file = `import { defineNode } from "@blokjs/core";
1739
1524
  import { z } from "zod";
1740
1525
 
1741
1526
  /**
@@ -1760,13 +1545,13 @@ export default defineNode({
1760
1545
  // Execute function - type-safe with inferred types from Zod schemas
1761
1546
  async execute(ctx, input) {
1762
1547
  // Your business logic here
1763
- // - ctx.vars: Access workflow variables
1764
1548
  // - ctx.request: Access HTTP request data
1765
1549
  // - ctx.logger: Log messages
1766
1550
  // - ctx.env: Access environment variables
1767
-
1768
- // Example: Store data for downstream nodes
1769
- ctx.vars["processed-message"] = input.message;
1551
+ //
1552
+ // Just RETURN your output the runner auto-persists it to
1553
+ // ctx.state["{{NODE_NAME}}"], readable from any later step's handle.
1554
+ // Do NOT write to ctx.state / ctx.vars inside a node.
1770
1555
 
1771
1556
  // Return type-safe output (validated automatically)
1772
1557
  return {
@@ -1776,4 +1561,4 @@ export default defineNode({
1776
1561
  },
1777
1562
  });
1778
1563
  `;
1779
- export { node_file, package_dependencies, package_dev_dependencies, python3_file, examples_url, workflow_template, supervisord_nodejs, supervisord_python, go_node_file, go_mod_file, go_dockerfile, java_node_file, java_pom_file, java_dockerfile, rust_node_file, rust_cargo_file, rust_dockerfile, csharp_node_file, csharp_csproj_file, csharp_dockerfile, php_node_file, php_composer_file, php_dockerfile, ruby_node_file, ruby_gemfile, ruby_dockerfile, function_first_node_file, agents_md, claude_md, };
1564
+ export { node_file, package_dependencies, package_dev_dependencies, python3_file, examples_url, workflow_template, supervisord_nodejs, supervisord_python, go_node_file, java_node_file, rust_node_file, csharp_node_file, php_node_file, ruby_node_file, function_first_node_file, agents_md, claude_md, };