blokctl 0.2.11 → 0.3.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 (37) hide show
  1. package/dist/commands/create/project.js +54 -0
  2. package/dist/commands/create/utils/Examples.d.ts +3 -3
  3. package/dist/commands/create/utils/Examples.js +109 -13
  4. package/dist/commands/dev/index.js +50 -13
  5. package/dist/commands/generate/validators/WorkflowValidator.js +1 -1
  6. package/dist/commands/migrate/index.js +11 -0
  7. package/dist/commands/migrate/workflows.d.ts +3 -0
  8. package/dist/commands/migrate/workflows.js +333 -0
  9. package/dist/commands/trace/index.js +6 -2
  10. package/dist/commands/trace/startStudio.d.ts +2 -0
  11. package/dist/commands/trace/startStudio.js +76 -11
  12. package/dist/index.js +1 -0
  13. package/dist/services/health-probe.d.ts +5 -0
  14. package/dist/services/health-probe.js +35 -0
  15. package/dist/services/runtime-detector.d.ts +3 -0
  16. package/dist/services/runtime-detector.js +21 -2
  17. package/dist/services/runtime-setup.d.ts +3 -0
  18. package/dist/services/runtime-setup.js +11 -2
  19. package/dist/studio-dist/assets/charts-Dh48HebV.js +68 -0
  20. package/dist/studio-dist/assets/graph-DWteCadQ.js +7 -0
  21. package/dist/studio-dist/assets/{icons-zP8LLgPh.js → icons-N5J4OhGx.js} +66 -51
  22. package/dist/studio-dist/assets/index-D6JA5F-X.js +42 -0
  23. package/dist/studio-dist/assets/index-mdQkg9ul.css +1 -0
  24. package/dist/studio-dist/assets/react-vendor-l0sNRNKZ.js +1 -0
  25. package/dist/studio-dist/assets/tanstack-query-Day3Mt-4.js +17 -0
  26. package/dist/studio-dist/assets/tanstack-router-BB95iErN.js +25 -0
  27. package/dist/studio-dist/assets/{tanstack-table-DhwRvuH2.js → tanstack-table-Biem1hxK.js} +1 -1
  28. package/dist/studio-dist/favicon.svg +7 -4
  29. package/dist/studio-dist/index.html +19 -10
  30. package/package.json +14 -11
  31. package/dist/studio-dist/assets/charts-Dso0hPUR.js +0 -68
  32. package/dist/studio-dist/assets/graph-CsV2nWGn.js +0 -23
  33. package/dist/studio-dist/assets/index-CLyEkXMx.css +0 -1
  34. package/dist/studio-dist/assets/index-CNXFX_ar.js +0 -27
  35. package/dist/studio-dist/assets/react-vendor--Eh9ivFN.js +0 -17
  36. package/dist/studio-dist/assets/tanstack-query-CiM1U6F5.js +0 -1
  37. package/dist/studio-dist/assets/tanstack-router-Btjy0MKq.js +0 -25
@@ -135,6 +135,7 @@ export async function createProject(opts, version, currentPath = false, localRep
135
135
  { label: "RabbitMQ", value: "rabbitmq" },
136
136
  { label: "AWS SQS", value: "sqs" },
137
137
  { label: "Redis/BullMQ", value: "redis" },
138
+ { label: "NATS JetStream", value: "nats" },
138
139
  ],
139
140
  })
140
141
  : Promise.resolve(null),
@@ -262,6 +263,34 @@ export async function createProject(opts, version, currentPath = false, localRep
262
263
  fsExtra.copySync(src, `${dirPath}/${file}`);
263
264
  }
264
265
  }
266
+ const gitignorePath = `${dirPath}/.gitignore`;
267
+ const gitignoreLine = "\n# Blok Studio trace data (managed by blokctl)\n.blok/\n";
268
+ if (fsExtra.existsSync(gitignorePath)) {
269
+ const existing = fsExtra.readFileSync(gitignorePath, "utf8");
270
+ if (!existing.includes(".blok/")) {
271
+ fsExtra.appendFileSync(gitignorePath, gitignoreLine);
272
+ }
273
+ }
274
+ else {
275
+ fsExtra.writeFileSync(gitignorePath, gitignoreLine.trimStart());
276
+ }
277
+ fsExtra.ensureDirSync(`${dirPath}/.blok`);
278
+ fsExtra.writeFileSync(`${dirPath}/.blok/README.md`, [
279
+ "# .blok/",
280
+ "",
281
+ "Auto-generated by `blokctl dev`. This directory holds the SQLite trace",
282
+ "database that powers Blok Studio (run history, logs, events, dashboards).",
283
+ "",
284
+ "- `trace.db` — SQLite file, persists across restarts",
285
+ "- 7-day retention by default; tune with `BLOK_TRACE_RETENTION_DAYS`",
286
+ "",
287
+ "Open it visually with `blokctl studio` (no trigger required — works",
288
+ "like `prisma studio`). Or wipe everything via the **Clear all data**",
289
+ "button in Settings.",
290
+ "",
291
+ "This directory is gitignored.",
292
+ "",
293
+ ].join("\n"));
265
294
  if (fsExtra.existsSync(`${primaryTriggerDir}/Dockerfile`)) {
266
295
  fsExtra.copySync(`${primaryTriggerDir}/Dockerfile`, `${dirPath}/Dockerfile`);
267
296
  }
@@ -539,6 +568,14 @@ export async function createProject(opts, version, currentPath = false, localRep
539
568
  console.log(color.dim(" docker compose up -d redis redis-commander"));
540
569
  console.log(" Redis Commander UI: http://localhost:8081");
541
570
  }
571
+ if (selectedTriggers.includes("queue") && queueProvider === "nats") {
572
+ console.log(color.cyan("\n📦 NATS JetStream Setup (for Queue trigger):"));
573
+ console.log(" Start NATS with Docker:");
574
+ console.log(color.dim(" cd infra/development"));
575
+ console.log(color.dim(" docker network create shared-network"));
576
+ console.log(color.dim(" docker compose up -d nats"));
577
+ console.log(" NATS Monitoring: http://localhost:8222");
578
+ }
542
579
  console.log("\nFor more documentation, visit https://blok.build/");
543
580
  if (examples) {
544
581
  console.log(examples_url);
@@ -907,6 +944,12 @@ function updateQueueProvider(triggerDestDir, provider) {
907
944
  port: Number(process.env.REDIS_PORT) || 6379,
908
945
  })`,
909
946
  },
947
+ nats: {
948
+ importName: "NATSAdapter",
949
+ init: `new NATSAdapter({
950
+ servers: (process.env.NATS_SERVERS || "localhost:4222").split(","),
951
+ })`,
952
+ },
910
953
  };
911
954
  const config = adapterConfigs[provider];
912
955
  if (!config)
@@ -914,6 +957,12 @@ function updateQueueProvider(triggerDestDir, provider) {
914
957
  content = content.replace(/import \{ (\w+), (\w+) \} from ["']@blokjs\/trigger-queue["'];/, `import { ${config.importName}, QueueTrigger } from "@blokjs/trigger-queue";`);
915
958
  content = content.replace(/(export default class \w+ extends QueueTrigger \{[\s\S]*?)\n\tprotected adapter = new \w+\(\{[\s\S]*?\}\);/, `$1\n\tprotected adapter = ${config.init};`);
916
959
  fsExtra.writeFileSync(serverPath, content);
960
+ const workflowPath = `${triggerDestDir}/workflows/messages/on-message.ts`;
961
+ if (fsExtra.existsSync(workflowPath)) {
962
+ let workflowContent = fsExtra.readFileSync(workflowPath, "utf8");
963
+ workflowContent = workflowContent.replace(/provider: "kafka"/, `provider: "${provider}"`);
964
+ fsExtra.writeFileSync(workflowPath, workflowContent);
965
+ }
917
966
  }
918
967
  function getProviderDependencies(triggers, pubsubProvider, queueProvider) {
919
968
  const deps = {};
@@ -927,6 +976,7 @@ function getProviderDependencies(triggers, pubsubProvider, queueProvider) {
927
976
  rabbitmq: { amqplib: "^0.10.9" },
928
977
  sqs: { "@aws-sdk/client-sqs": "^3.980.0" },
929
978
  redis: { ioredis: "^5.9.2", bullmq: "^5.67.2" },
979
+ nats: { nats: "^2.28.0" },
930
980
  };
931
981
  if (triggers.includes("pubsub") && pubsubProviderDeps[pubsubProvider]) {
932
982
  Object.assign(deps, pubsubProviderDeps[pubsubProvider]);
@@ -970,6 +1020,10 @@ SQS_QUEUE_URL=`,
970
1020
  REDIS_HOST=localhost
971
1021
  REDIS_PORT=6379
972
1022
  REDIS_PASSWORD=`,
1023
+ nats: `
1024
+ # NATS JetStream
1025
+ NATS_SERVERS=localhost:4222
1026
+ NATS_STREAM_NAME=blok-queue`,
973
1027
  };
974
1028
  if (triggers.includes("pubsub") && pubsubEnvVars[pubsubProvider]) {
975
1029
  lines.push(pubsubEnvVars[pubsubProvider]);
@@ -12,7 +12,7 @@ declare const package_dev_dependencies: {
12
12
  };
13
13
  declare const python3_file = "\nfrom core.blok import BlokService\nfrom core.types.context import Context\nfrom core.types.blok_response import BlokResponse\nfrom core.types.global_error import GlobalError\nfrom typing import Any, Dict\nimport traceback\n\nclass Node(BlokService):\n def __init__(self):\n BlokService.__init__(self)\n self.input_schema = {}\n self.output_schema = {}\n\n async def handle(self, ctx: Context, inputs: Dict[str, Any]) -> BlokResponse:\n response = BlokResponse()\n\n try:\n response.setSuccess({ \"message\": \"Hello World from Python3!\" })\n except Exception as error:\n err = GlobalError(error)\n err.setCode(500)\n err.setName(self.name)\n\n stack_trace = traceback.format_exc()\n err.setStack(stack_trace)\n response.success = False\n response.setError(err)\n\n return response\n";
14
14
  declare const examples_url = "\nExamples:\n1- Open \"workflow-docs.json\" in your browser at http://localhost:4000/workflow-docs\n2- Open \"db-manager.json\" in your browser at http://localhost:4000/db-manager\n3- Open \"dashboard-gen.json\" in your browser at http://localhost:4000/dashboard-gen\n4- Open \"countries.json\" in your browser at http://localhost:4000/countries\n\nFor more documentation, visit src/nodes/examples/README.md. The first three examples require a PostgreSQL database to function.\n";
15
- declare const workflow_template = "\n{\n\t\"name\": \"\",\n\t\"description\": \"\",\n\t\"version\": \"1.0.0\",\n\t\"trigger\": {\n\t\t\"http\": {\n\t\t\t\"method\": \"GET\",\n\t\t\t\"path\": \"/\",\n\t\t\t\"accept\": \"application/json\"\n\t\t}\n\t},\n\t\"steps\": [\n\t\t{\n\t\t\t\"name\": \"node-name\",\n\t\t\t\"node\": \"node-module-name\",\n\t\t\t\"type\": \"module\"\n\t\t}\n\t],\n\t\"nodes\": {\n\t\t\"name\": {\n\t\t\t\"inputs\": {\n\n\t\t\t}\n\t\t}\n\t}\n}\n";
15
+ declare const workflow_template = "\n{\n\t\"name\": \"My Workflow\",\n\t\"description\": \"What this workflow does\",\n\t\"version\": \"1.0.0\",\n\t\"trigger\": {\n\t\t\"http\": {\n\t\t\t\"method\": \"GET\",\n\t\t\t\"accept\": \"application/json\"\n\t\t}\n\t},\n\t\"steps\": [\n\t\t{\n\t\t\t\"id\": \"echo\",\n\t\t\t\"use\": \"@blokjs/respond\",\n\t\t\t\"inputs\": {\n\t\t\t\t\"body\": \"$.req.body\"\n\t\t\t}\n\t\t}\n\t]\n}\n";
16
16
  declare const supervisord_nodejs = "\n[supervisord]\nnodaemon=true\n\n[program:nodejs_app]\ncommand=npm start\ndirectory=/app\nautostart=true\nautorestart=true\nstderr_logfile=/var/log/nodejs.err.log\nstdout_logfile=/var/log/nodejs.out.log\n";
17
17
  declare const supervisord_python = "\n[program:python_app]\ncommand=python3 /app/.blok/runtimes/python3/server.py\ndirectory=/app\nautostart=true\nautorestart=true\nstderr_logfile=/var/log/python.err.log\nstdout_logfile=/var/log/python.out.log\n";
18
18
  declare const go_node_file = "package main\n\nimport (\n\t\"github.com/blok/sdk\"\n)\n\ntype HelloWorldNode struct{}\n\nfunc (n *HelloWorldNode) Execute(ctx *sdk.Context, config map[string]interface{}) (*sdk.ExecutionResult, error) {\n\t// Access request body\n\tname := \"World\"\n\tif body, ok := ctx.Request.Body.(map[string]interface{}); ok {\n\t\tif nameVal, ok := body[\"name\"].(string); ok {\n\t\t\tname = nameVal\n\t\t}\n\t}\n\n\t// Access configuration\n\tprefix := \"Hello\"\n\tif prefixVal, ok := config[\"prefix\"].(string); ok {\n\t\tprefix = prefixVal\n\t}\n\n\t// Store result in context for downstream nodes\n\tctx.Vars[\"greeting\"] = prefix + \", \" + name + \"!\"\n\n\t// Return successful result\n\treturn &sdk.ExecutionResult{\n\t\tSuccess: true,\n\t\tData: map[string]interface{}{\n\t\t\t\"message\": prefix + \", \" + name + \"!\",\n\t\t\t\"timestamp\": sdk.GetCurrentTimestamp(),\n\t\t\t\"language\": \"Go\",\n\t\t},\n\t\tErrors: nil,\n\t}, nil\n}\n\nfunc main() {\n\t// Register node\n\tregistry := sdk.NewNodeRegistry()\n\tregistry.Register(\"{{NODE_NAME}}\", &HelloWorldNode{})\n\n\t// Start HTTP server\n\tserver := sdk.NewHTTPServer(registry, \":8080\")\n\tif err := server.Start(); err != nil {\n\t\tpanic(err)\n\t}\n}\n";
@@ -33,7 +33,7 @@ declare const php_dockerfile = "FROM php:8.2-cli-alpine AS builder\nWORKDIR /app
33
33
  declare const ruby_node_file = "require_relative '../../lib/blok'\n\nmodule Blok\n module Nodes\n class {{NODE_NAME_PASCAL}}Node < Blok::NodeHandler\n def execute(ctx, config)\n # Access request body\n name = ctx.request.body.is_a?(Hash) ? ctx.request.body['name'] : nil\n name ||= 'World'\n\n # Access configuration\n prefix = config['prefix'] || 'Hello'\n\n message = \"#{prefix}, #{name}!\"\n\n # Store in context for downstream nodes\n ctx.vars['greeting'] = message\n\n # Return response\n {\n 'message' => message,\n 'timestamp' => Time.now.utc.iso8601,\n 'language' => 'Ruby'\n }\n end\n end\n end\nend\n";
34
34
  declare const ruby_gemfile = "source 'https://rubygems.org'\n\nruby '>= 3.1'\n\ngem 'sinatra', '~> 4.0'\ngem 'puma', '~> 6.4'\ngem 'rackup', '~> 2.1'\n";
35
35
  declare const ruby_dockerfile = "FROM ruby:3.2-alpine AS builder\nRUN apk add --no-cache build-base\nWORKDIR /app\nCOPY Gemfile Gemfile.lock ./\nRUN bundle install --without development test\n\nFROM ruby:3.2-alpine\nRUN apk --no-cache add ca-certificates wget\nWORKDIR /app\nCOPY --from=builder /usr/local/bundle /usr/local/bundle\nCOPY . .\n\nEXPOSE 8080\nENV PORT=8080\nENV RACK_ENV=production\nHEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\\n CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1\n\nCMD [\"bundle\", \"exec\", \"puma\", \"-b\", \"tcp://0.0.0.0:8080\"]\n";
36
- declare const agents_md = "# Blok Project\n\nBlok is a TypeScript-first workflow orchestration framework. It executes declarative workflows (JSON or TypeScript DSL) composed of steps (nodes) that run across 8 language runtimes: NodeJS, Python3, Go, Rust, Java, C#, PHP, and Ruby.\n\n## Project Structure\n\n```\n\u251C\u2500\u2500 src/\n\u2502 \u2514\u2500\u2500 nodes/ # TypeScript node implementations\n\u251C\u2500\u2500 runtimes/ # Non-NodeJS runtime nodes (Go, Python3, etc.)\n\u2502 \u2514\u2500\u2500 {lang}/nodes/ # Language-specific node implementations\n\u251C\u2500\u2500 workflows/\n\u2502 \u251C\u2500\u2500 json/ # Workflow definitions (JSON)\n\u2502 \u251C\u2500\u2500 yaml/ # Workflow definitions (YAML)\n\u2502 \u2514\u2500\u2500 toml/ # Workflow definitions (TOML)\n\u251C\u2500\u2500 .blok/\n\u2502 \u251C\u2500\u2500 config.json # Runtime configuration (ports, start commands)\n\u2502 \u2514\u2500\u2500 runtimes/ # Auto-generated runtime scaffolds\n\u251C\u2500\u2500 .env.local # Environment variables (ports, paths)\n\u2514\u2500\u2500 supervisord.conf # Process management config\n```\n\n## Commands\n\n```bash\nnpm run dev # Start dev server (or blokctl dev for multi-runtime)\nnpm run build # Build project\nnpm test # Run tests\nblokctl create node <name> # Scaffold a new node\nblokctl create workflow <n># Scaffold a new workflow\nblokctl trace # Open Blok Studio (trace visualization)\nblokctl studio # Alias for blokctl trace\n```\n\n## Context \u2014 Critical Data Flow\n\nThe Context type is the central execution state passed through every step.\n\n```typescript\ntype Context = {\n id: string; // Unique request ID\n request: RequestContext; // Incoming request (body, headers, params, query)\n response: ResponseContext; // Current step output \u2014 OVERWRITTEN every step\n vars?: VarsContext; // Persistent variables \u2014 PERSISTS across workflow\n config: ConfigContext; // Node config (inputs resolved by Mapper)\n env?: EnvContext; // process.env access\n logger: LoggerContext;\n error: ErrorContext;\n};\n```\n\n### The Two Critical Rules\n\n**Rule 1: \\`ctx.response.data\\` is OVERWRITTEN after every step.**\nEach step's output replaces the previous \\`ctx.response.data\\`. If you need a step's output later, store it in \\`ctx.vars\\`.\n\n**Rule 2: \\`ctx.vars\\` PERSISTS across the entire workflow.**\nUse \\`set_var: true\\` on a step to auto-store its output in \\`ctx.vars[stepName]\\`. Downstream steps access it via \\`ctx.vars['step-name']\\`.\n\n### Data Flow Example\n\n```\nStep 1: \"fetch-user\" (set_var: true)\n \u2192 ctx.response.data = { id: \"123\", name: \"Alice\" }\n \u2192 ctx.vars[\"fetch-user\"] = { id: \"123\", name: \"Alice\" }\n\nStep 2: \"transform\"\n \u2192 ctx.response.data = { result: \"done\" } \u2190 Step 1 output GONE from response\n \u2192 ctx.vars[\"fetch-user\"] still available\n\nStep 3: \"output\"\n \u2192 Can read ctx.vars[\"fetch-user\"].name \u2190 still \"Alice\"\n```\n\n### Blueprint Mapper \u2014 Expression Resolution\n\nNode inputs support dynamic expressions resolved BEFORE node execution:\n\n```json\n{\n \"inputs\": {\n \"userId\": \"js/ctx.request.body.userId\",\n \"chain\": \"js/ctx.vars['previous-step'].chain\",\n \"previous\": \"js/ctx.response.data.result\"\n }\n}\n```\n\nAvailable in js/ expressions: \\`ctx\\`, \\`data\\` (ctx.response.data), \\`func\\` (ctx.func), \\`vars\\` (ctx.vars)\n\n---\n\n## Creating Nodes with defineNode\n\nUse \\`defineNode()\\` for all new nodes. Never use the legacy class-based pattern.\n\n```typescript\nimport { defineNode } from \"@blokjs/runner\";\nimport { z } from \"zod\";\n\nexport default defineNode({\n name: \"fetch-user\",\n description: \"Fetches user by ID\",\n\n input: z.object({\n userId: z.string().uuid(),\n }),\n\n output: z.object({\n user: z.object({\n id: z.string(),\n name: z.string(),\n email: z.string().email(),\n }),\n }),\n\n async execute(ctx, input) {\n const user = await fetchUser(input.userId);\n return { user };\n },\n});\n```\n\n### Key Behaviors\n\n- Zod input/output validation runs automatically\n- ZodError is mapped to GlobalError with HTTP 400\n- \\`flow: true\\` nodes return NodeBase[] for conditional execution\n- \\`contentType\\` sets response Content-Type (e.g., \"text/html\")\n- Always \\`export default defineNode(...)\\`\n\n---\n\n## Workflow Structure (JSON)\n\n```json\n{\n \"name\": \"My Workflow\",\n \"version\": \"1.0.0\",\n \"trigger\": {\n \"http\": { \"method\": \"POST\", \"path\": \"/api/process\", \"accept\": \"application/json\" }\n },\n \"steps\": [\n { \"name\": \"fetch\", \"node\": \"@blokjs/api-call\", \"type\": \"module\" },\n { \"name\": \"process\", \"node\": \"my-node\", \"type\": \"module\", \"set_var\": true },\n { \"name\": \"go-step\", \"node\": \"chain-test\", \"type\": \"runtime.go\" }\n ],\n \"nodes\": {\n \"fetch\": { \"inputs\": { \"url\": \"https://api.example.com\", \"method\": \"GET\" } },\n \"process\": { \"inputs\": { \"data\": \"js/ctx.response.data\" } },\n \"go-step\": { \"inputs\": { \"processed\": \"js/ctx.vars['process']\" } }\n }\n}\n```\n\n### Step Types\n\n| Type | Description |\n|------|-------------|\n| \\`module\\` | TypeScript node from registered modules |\n| \\`local\\` | TypeScript node from filesystem (NODES_PATH) |\n| \\`runtime.python3\\` | Python3 SDK container (port 9007) |\n| \\`runtime.go\\` | Go SDK container (port 9001) |\n| \\`runtime.rust\\` | Rust SDK container (port 9002) |\n| \\`runtime.java\\` | Java SDK container (port 9003) |\n| \\`runtime.csharp\\` | C# SDK container (port 9004) |\n| \\`runtime.php\\` | PHP SDK container (port 9005) |\n| \\`runtime.ruby\\` | Ruby SDK container (port 9006) |\n\n### Conditional Workflow (if-else)\n\n```json\n{\n \"nodes\": {\n \"filter\": {\n \"conditions\": [\n {\n \"type\": \"if\",\n \"condition\": \"ctx.request.query.active === \\\\\"true\\\\\"\",\n \"steps\": [{ \"name\": \"active-path\", \"node\": \"handle-active\", \"type\": \"module\" }]\n },\n {\n \"type\": \"else\",\n \"steps\": [{ \"name\": \"default-path\", \"node\": \"handle-default\", \"type\": \"module\" }]\n }\n ]\n }\n }\n}\n```\n\n---\n\n## Trigger Types\n\n| Trigger | Example Config |\n|---------|---------------|\n| \\`http\\` | \\`{ \"method\": \"GET\", \"path\": \"/\", \"accept\": \"application/json\" }\\` |\n| \\`grpc\\` | \\`{ \"service\": \"UserService\", \"method\": \"GetUser\" }\\` |\n| \\`cron\\` | \\`{ \"schedule\": \"0 * * * *\", \"timezone\": \"UTC\" }\\` |\n| \\`queue\\` | \\`{ \"provider\": \"kafka\", \"topic\": \"events\" }\\` |\n| \\`pubsub\\` | \\`{ \"provider\": \"gcp\", \"topic\": \"updates\" }\\` |\n| \\`webhook\\` | \\`{ \"source\": \"github\", \"events\": [\"push\"] }\\` |\n| \\`websocket\\` | \\`{ \"events\": [\"message\"], \"path\": \"/ws\" }\\` |\n| \\`sse\\` | \\`{ \"events\": [\"update\"], \"path\": \"/stream\" }\\` |\n| \\`worker\\` | \\`{ \"queue\": \"jobs\", \"concurrency\": 5 }\\` |\n\n---\n\n## Runtime Adapter System\n\nAll non-NodeJS SDKs communicate via HTTP:\n- **POST /execute** \u2014 Execute node with context\n- **GET /health** \u2014 Health check\n\nEnvironment variables: \\`RUNTIME_{LANG}_HOST\\` / \\`RUNTIME_{LANG}_PORT\\`\n\nRuntime nodes auto-save \\`result.data\\` to \\`ctx.vars[stepName]\\`.\n\n---\n\n## Blok Studio\n\nReal-time workflow trace visualization UI.\n\n- Launch: \\`blokctl trace\\` or \\`blokctl studio\\`\n- API: \\`/__blok/runs\\`, \\`/__blok/runs/:id\\`, \\`/__blok/runs/:id/stream\\` (SSE)\n- Disable: \\`BLOK_TRACE_ENABLED=false\\`\n\n---\n\n## Do NOT\n\n- Do NOT rely on \\`ctx.response.data\\` for data from non-previous steps \u2014 it gets overwritten\n- Do NOT create class-based nodes \u2014 use \\`defineNode()\\` instead\n- Do NOT use \\`any\\` type \u2014 use \\`unknown\\` and narrow with Zod\n- Do NOT hardcode runtime ports \u2014 use environment variables\n- Do NOT skip Zod input/output schemas\n- Do NOT edit files in \\`.blok/runtimes/\\` \u2014 they are auto-generated\n\n## Do\n\n- Use \\`ctx.vars\\` with \\`set_var: true\\` to pass data between non-adjacent steps\n- Use \\`js/ctx.vars['step-name'].field\\` in workflow inputs for data flow\n- Use Zod schemas for all input/output validation\n- Use \\`defineNode()\\` for all new nodes\n- Handle errors via GlobalError with appropriate HTTP status codes\n- Keep nodes focused \u2014 one responsibility per node\n";
37
- declare const claude_md = "# Blok Project \u2014 Claude Code Guide\n\nRead \\`AGENTS.md\\` for full architecture and API details. This file contains Claude-specific guidance.\n\n## Quick Commands\n\n\\`\\`\\`bash\nnpm run dev # Start dev server\nblokctl dev # Multi-runtime dev server\nblokctl create node <name> # Scaffold new node\nblokctl create workflow <name> # Scaffold new workflow\nblokctl trace # Open Blok Studio\nnpm test # Run tests\n\\`\\`\\`\n\n## Context Rules (Memorize These)\n\n1. **\\`ctx.response.data\\` is OVERWRITTEN every step.** Previous output GONE unless stored in vars.\n2. **\\`ctx.vars\\` PERSISTS across the workflow.** Use \\`set_var: true\\` or \\`js/ctx.vars['step']\\`.\n3. **Blueprint Mapper resolves \\`js/\\` expressions BEFORE node execution.**\n\nWhen users have data flow issues, check these three things first.\n\n## Debugging Workflows\n\n1. **Verify structure**: Every \\`steps[].name\\` must match a key in \\`nodes\\`\n2. **Trace data flow**: Which steps have \\`set_var: true\\`? Do \\`js/\\` expressions reference correct step names?\n3. **Check runtimes**: SDK containers running? \\`GET http://localhost:{port}/health\\`\n4. **Check Studio traces**: \\`/__blok/runs/:id\\` shows step-by-step inputs/outputs/errors\n\n### Common Errors\n\n| Error | Fix |\n|-------|-----|\n| \\`Node type X not found\\` | Wrong \\`type\\` in step \u2014 use module, local, or runtime.* |\n| \\`Validation failed\\` | Zod schema mismatch \u2014 check input schema vs actual data |\n| \\`Runtime execution error\\` | SDK container not running \u2014 check health endpoint |\n| \\`ctx.vars['X'] undefined\\` | Source step missing \\`set_var: true\\` or name mismatch |\n\n## Generating Code\n\nAlways use \\`defineNode()\\`. Never class-based BlokService.\n\n\\`\\`\\`typescript\nimport { defineNode } from \"@blokjs/runner\";\nimport { z } from \"zod\";\n\nexport default defineNode({\n name: \"node-name\",\n description: \"What this node does\",\n input: z.object({ /* Zod schema */ }),\n output: z.object({ /* Zod schema */ }),\n async execute(ctx, input) {\n return { /* must match output schema */ };\n },\n});\n\\`\\`\\`\n\n### Checklist:\n- Zod input schema covers all inputs\n- Zod output schema matches execute() return\n- Node name matches workflow references\n- No \\`any\\` types \u2014 use \\`z.unknown()\\` if dynamic\n- \\`export default defineNode(...)\\`\n\n## Blok Studio Help\n\n- Launch: \\`blokctl trace\\` or navigate to \\`/__blok\\`\n- \"No output\" \u2192 Node not returning data or Zod output validation failed\n- \"Step error\" \u2192 Expand error \u2014 check if 400 (validation) or 500 (runtime)\n- \"Vars not passing\" \u2192 Source step needs \\`set_var: true\\`, target needs \\`js/ctx.vars['name']\\`\n\n## Do NOT\n\n- Do NOT suggest class-based BlokService for new nodes\n- Do NOT generate code with \\`any\\` types\n- Do NOT assume \\`ctx.response.data\\` persists across steps\n- Do NOT skip Zod schemas when creating nodes\n- Do NOT edit files in \\`.blok/runtimes/\\`\n";
36
+ declare const agents_md = "# Blok Project\n\nBlok is a TypeScript-first workflow orchestration framework. It executes declarative workflows (JSON or TypeScript DSL) composed of steps (nodes) that run across 8 language runtimes: NodeJS, Python3, Go, Rust, Java, C#, PHP, and Ruby.\n\n## Project Structure\n\n```\n\u251C\u2500\u2500 src/\n\u2502 \u2514\u2500\u2500 nodes/ # TypeScript node implementations\n\u251C\u2500\u2500 runtimes/ # Non-NodeJS runtime nodes (Go, Python3, etc.)\n\u2502 \u2514\u2500\u2500 {lang}/nodes/ # Language-specific node implementations\n\u251C\u2500\u2500 workflows/\n\u2502 \u251C\u2500\u2500 json/ # Workflow definitions (JSON)\n\u2502 \u251C\u2500\u2500 yaml/ # Workflow definitions (YAML)\n\u2502 \u2514\u2500\u2500 toml/ # Workflow definitions (TOML)\n\u251C\u2500\u2500 .blok/\n\u2502 \u251C\u2500\u2500 config.json # Runtime configuration (ports, start commands)\n\u2502 \u2514\u2500\u2500 runtimes/ # Auto-generated runtime scaffolds\n\u251C\u2500\u2500 .env.local # Environment variables (ports, paths)\n\u2514\u2500\u2500 supervisord.conf # Process management config\n```\n\n## Commands\n\n```bash\nnpm run dev # Start dev server (or blokctl dev for multi-runtime)\nnpm run build # Build project\nnpm test # Run tests\nblokctl create node <name> # Scaffold a new node\nblokctl create workflow <n># Scaffold a new workflow\nblokctl trace # Open Blok Studio (trace visualization)\nblokctl studio # Alias for blokctl trace\n```\n\n## Context \u2014 Critical Data Flow\n\nThe Context type is the central execution state passed through every step.\n\n```typescript\ntype Context = {\n id: string; // Unique request ID\n request: RequestContext; // Incoming request (body, headers, params, query)\n response: ResponseContext; // Current step output \u2014 OVERWRITTEN every step\n vars?: VarsContext; // Persistent variables \u2014 PERSISTS across workflow\n config: ConfigContext; // Node config (inputs resolved by Mapper)\n env?: EnvContext; // process.env access\n logger: LoggerContext;\n error: ErrorContext;\n};\n```\n\n### The Two Critical Rules\n\n**Rule 1: \\`ctx.response.data\\` is OVERWRITTEN after every step.**\nEach step's output replaces the previous \\`ctx.response.data\\`. If you need a step's output later, store it in \\`ctx.vars\\`.\n\n**Rule 2: \\`ctx.vars\\` PERSISTS across the entire workflow.**\nUse \\`set_var: true\\` on a step to auto-store its output in \\`ctx.vars[stepName]\\`. Downstream steps access it via \\`ctx.vars['step-name']\\`.\n\n### Data Flow Example\n\n```\nStep 1: \"fetch-user\" (set_var: true)\n \u2192 ctx.response.data = { id: \"123\", name: \"Alice\" }\n \u2192 ctx.vars[\"fetch-user\"] = { id: \"123\", name: \"Alice\" }\n\nStep 2: \"transform\"\n \u2192 ctx.response.data = { result: \"done\" } \u2190 Step 1 output GONE from response\n \u2192 ctx.vars[\"fetch-user\"] still available\n\nStep 3: \"output\"\n \u2192 Can read ctx.vars[\"fetch-user\"].name \u2190 still \"Alice\"\n```\n\n### Blueprint Mapper \u2014 Expression Resolution\n\nNode inputs support dynamic expressions resolved BEFORE node execution:\n\n```json\n{\n \"inputs\": {\n \"userId\": \"js/ctx.request.body.userId\",\n \"chain\": \"js/ctx.vars['previous-step'].chain\",\n \"previous\": \"js/ctx.response.data.result\"\n }\n}\n```\n\nAvailable in js/ expressions: \\`ctx\\`, \\`data\\` (ctx.response.data), \\`func\\` (ctx.func), \\`vars\\` (ctx.vars)\n\n---\n\n## Creating Nodes with defineNode\n\nUse \\`defineNode()\\` for all new nodes. Never use the legacy class-based pattern.\n\n```typescript\nimport { defineNode } from \"@blokjs/runner\";\nimport { z } from \"zod\";\n\nexport default defineNode({\n name: \"fetch-user\",\n description: \"Fetches user by ID\",\n\n input: z.object({\n userId: z.string().uuid(),\n }),\n\n output: z.object({\n user: z.object({\n id: z.string(),\n name: z.string(),\n email: z.string().email(),\n }),\n }),\n\n async execute(ctx, input) {\n const user = await fetchUser(input.userId);\n return { user };\n },\n});\n```\n\n### Key Behaviors\n\n- Zod input/output validation runs automatically\n- ZodError is mapped to GlobalError with HTTP 400\n- \\`flow: true\\` nodes return NodeBase[] for conditional execution\n- \\`contentType\\` sets response Content-Type (e.g., \"text/html\")\n- Always \\`export default defineNode(...)\\`\n\n---\n\n## Workflow Structure (JSON)\n\n```json\n{\n \"name\": \"My Workflow\",\n \"version\": \"1.0.0\",\n \"trigger\": {\n \"http\": { \"method\": \"POST\", \"path\": \"/api/process\", \"accept\": \"application/json\" }\n },\n \"steps\": [\n { \"name\": \"fetch\", \"node\": \"@blokjs/api-call\", \"type\": \"module\" },\n { \"name\": \"process\", \"node\": \"my-node\", \"type\": \"module\", \"set_var\": true },\n { \"name\": \"go-step\", \"node\": \"chain-test\", \"type\": \"runtime.go\" }\n ],\n \"nodes\": {\n \"fetch\": { \"inputs\": { \"url\": \"https://api.example.com\", \"method\": \"GET\" } },\n \"process\": { \"inputs\": { \"data\": \"js/ctx.response.data\" } },\n \"go-step\": { \"inputs\": { \"processed\": \"js/ctx.vars['process']\" } }\n }\n}\n```\n\n### Step Types\n\n| Type | Description |\n|------|-------------|\n| \\`module\\` | TypeScript node from registered modules |\n| \\`local\\` | TypeScript node from filesystem (NODES_PATH) |\n| \\`runtime.python3\\` | Python3 SDK container (port 9007) |\n| \\`runtime.go\\` | Go SDK container (port 9001) |\n| \\`runtime.rust\\` | Rust SDK container (port 9002) |\n| \\`runtime.java\\` | Java SDK container (port 9003) |\n| \\`runtime.csharp\\` | C# SDK container (port 9004) |\n| \\`runtime.php\\` | PHP SDK container (port 9005) |\n| \\`runtime.ruby\\` | Ruby SDK container (port 9006) |\n\n### Conditional Workflow (if-else)\n\n```json\n{\n \"nodes\": {\n \"filter\": {\n \"conditions\": [\n {\n \"type\": \"if\",\n \"condition\": \"ctx.request.query.active === \\\\\"true\\\\\"\",\n \"steps\": [{ \"name\": \"active-path\", \"node\": \"handle-active\", \"type\": \"module\" }]\n },\n {\n \"type\": \"else\",\n \"steps\": [{ \"name\": \"default-path\", \"node\": \"handle-default\", \"type\": \"module\" }]\n }\n ]\n }\n }\n}\n```\n\n---\n\n## Trigger Types\n\n| Trigger | Example Config |\n|---------|---------------|\n| \\`http\\` | \\`{ \"method\": \"GET\", \"path\": \"/\", \"accept\": \"application/json\" }\\` |\n| \\`grpc\\` | \\`{ \"service\": \"UserService\", \"method\": \"GetUser\" }\\` |\n| \\`cron\\` | \\`{ \"schedule\": \"0 * * * *\", \"timezone\": \"UTC\" }\\` |\n| \\`queue\\` | \\`{ \"provider\": \"kafka\", \"topic\": \"events\" }\\` |\n| \\`pubsub\\` | \\`{ \"provider\": \"gcp\", \"topic\": \"updates\" }\\` |\n| \\`webhook\\` | \\`{ \"source\": \"github\", \"events\": [\"push\"] }\\` |\n| \\`websocket\\` | \\`{ \"events\": [\"message\"], \"path\": \"/ws\" }\\` |\n| \\`sse\\` | \\`{ \"events\": [\"update\"], \"path\": \"/stream\" }\\` |\n| \\`worker\\` | \\`{ \"queue\": \"jobs\", \"concurrency\": 5, \"retries\": 3 }\\` |\n\n### Worker Trigger\n\nThe worker trigger processes background jobs from a queue with retry logic and concurrency control.\n\n\\`\\`\\`typescript\nWorkflow({ name: \"Process Job\", version: \"1.0.0\" })\n .addTrigger(\"worker\", { queue: \"background-jobs\" })\n .addStep({\n name: \"process\",\n node: \"my-processor\",\n type: \"module\",\n inputs: { payload: \"js/ctx.request.body\", jobId: \"js/ctx.request.params.jobId\" },\n });\n\\`\\`\\`\n\nJob context: \\`ctx.request.body\\` = payload, \\`ctx.request.params.queue\\` = queue name, \\`ctx.request.params.jobId\\` = job ID, \\`ctx.request.params.attempt\\` = attempt count, \\`ctx.vars._worker_job\\` = full metadata.\n\nAdapters: NATS JetStream (recommended), BullMQ (Redis), InMemory (dev only).\n\n### NATS JetStream\n\nRecommended queue/worker backend. Environment variables:\n\\`\\`\\`\nNATS_SERVERS=localhost:4222\nNATS_STREAM_NAME=blok-queue # or blok-worker for worker trigger\nNATS_TOKEN= # optional auth\n\\`\\`\\`\n\nQueue providers: \\`kafka\\`, \\`rabbitmq\\`, \\`sqs\\`, \\`redis\\`, \\`beanstalk\\`, \\`nats\\`\n\n### Standalone Workers (Go, Rust, Python)\n\nGo, Rust, and Python SDKs include standalone NATS workers that connect directly to NATS without the TypeScript runner:\n\n\\`\\`\\`\nWORKER_CONCURRENCY=1 # Max concurrent jobs\nWORKER_MAX_RETRIES=3 # Max delivery attempts\nWORKER_QUEUES=queue1,queue2 # Queues to consume\n\\`\\`\\`\n\n---\n\n## Testing Utilities\n\n\\`@blokjs/runner\\` provides testing utilities for nodes and workflows.\n\n### NodeTestHarness \u2014 Unit test a single node:\n\\`\\`\\`typescript\nimport { NodeTestHarness } from \"@blokjs/runner\";\nconst harness = new NodeTestHarness(myNode);\nconst result = await harness.execute({ input: \"data\" });\nharness.assertSuccess(result);\nharness.assertOutput(result, { expected: \"output\" });\n\\`\\`\\`\n\n### WorkflowTestRunner \u2014 Integration test a workflow:\n\\`\\`\\`typescript\nimport { WorkflowTestRunner } from \"@blokjs/runner\";\nconst runner = new WorkflowTestRunner({ verbose: true });\nrunner.registerNode(\"validate\", ValidateNode);\nrunner.mockNode(\"external-api\", async (input) => ({ result: \"mocked\" }));\nrunner.loadWorkflow(workflowDefinition);\nconst result = await runner.execute({ input: \"data\" });\n// result.success, result.output, result.trace, result.nodeResults\n\\`\\`\\`\n\n---\n\n## Runtime Adapter System\n\nAll non-NodeJS SDKs communicate via HTTP:\n- **POST /execute** \u2014 Execute node with context\n- **GET /health** \u2014 Health check\n\nEnvironment variables: \\`RUNTIME_{LANG}_HOST\\` / \\`RUNTIME_{LANG}_PORT\\`\n\nRuntime nodes auto-save \\`result.data\\` to \\`ctx.vars[stepName]\\`.\n\n---\n\n## Blok Studio\n\nReal-time workflow trace visualization UI.\n\n- Launch: \\`blokctl trace\\` or \\`blokctl studio\\`\n- API: \\`/__blok/runs\\`, \\`/__blok/runs/:id\\`, \\`/__blok/runs/:id/stream\\` (SSE)\n- Disable: \\`BLOK_TRACE_ENABLED=false\\`\n\n---\n\n## Do NOT\n\n- Do NOT rely on \\`ctx.response.data\\` for data from non-previous steps \u2014 it gets overwritten\n- Do NOT create class-based nodes \u2014 use \\`defineNode()\\` instead\n- Do NOT use \\`any\\` type \u2014 use \\`unknown\\` and narrow with Zod\n- Do NOT hardcode runtime ports \u2014 use environment variables\n- Do NOT skip Zod input/output schemas\n- Do NOT edit files in \\`.blok/runtimes/\\` \u2014 they are auto-generated\n\n## Do\n\n- Use \\`ctx.vars\\` with \\`set_var: true\\` to pass data between non-adjacent steps\n- Use \\`js/ctx.vars['step-name'].field\\` in workflow inputs for data flow\n- Use Zod schemas for all input/output validation\n- Use \\`defineNode()\\` for all new nodes\n- Handle errors via GlobalError with appropriate HTTP status codes\n- Keep nodes focused \u2014 one responsibility per node\n";
37
+ declare const claude_md = "# Blok Project \u2014 Claude Code Guide\n\nRead \\`AGENTS.md\\` for full architecture and API details. This file contains Claude-specific guidance.\n\n## Quick Commands\n\n\\`\\`\\`bash\nnpm run dev # Start dev server\nblokctl dev # Multi-runtime dev server\nblokctl create node <name> # Scaffold new node\nblokctl create workflow <name> # Scaffold new workflow\nblokctl trace # Open Blok Studio\nnpm test # Run tests\n\\`\\`\\`\n\n## Context Rules (Memorize These)\n\n1. **\\`ctx.response.data\\` is OVERWRITTEN every step.** Previous output GONE unless stored in vars.\n2. **\\`ctx.vars\\` PERSISTS across the workflow.** Use \\`set_var: true\\` or \\`js/ctx.vars['step']\\`.\n3. **Blueprint Mapper resolves \\`js/\\` expressions BEFORE node execution.**\n\nWhen users have data flow issues, check these three things first.\n\n## Debugging Workflows\n\n1. **Verify structure**: Every \\`steps[].name\\` must match a key in \\`nodes\\`\n2. **Trace data flow**: Which steps have \\`set_var: true\\`? Do \\`js/\\` expressions reference correct step names?\n3. **Check runtimes**: SDK containers running? \\`GET http://localhost:{port}/health\\`\n4. **Check Studio traces**: \\`/__blok/runs/:id\\` shows step-by-step inputs/outputs/errors\n\n### Common Errors\n\n| Error | Fix |\n|-------|-----|\n| \\`Node type X not found\\` | Wrong \\`type\\` in step \u2014 use module, local, or runtime.* |\n| \\`Validation failed\\` | Zod schema mismatch \u2014 check input schema vs actual data |\n| \\`Runtime execution error\\` | SDK container not running \u2014 check health endpoint |\n| \\`ctx.vars['X'] undefined\\` | Source step missing \\`set_var: true\\` or name mismatch |\n\n## Generating Code\n\nAlways use \\`defineNode()\\`. Never class-based BlokService.\n\n\\`\\`\\`typescript\nimport { defineNode } from \"@blokjs/runner\";\nimport { z } from \"zod\";\n\nexport default defineNode({\n name: \"node-name\",\n description: \"What this node does\",\n input: z.object({ /* Zod schema */ }),\n output: z.object({ /* Zod schema */ }),\n async execute(ctx, input) {\n return { /* must match output schema */ };\n },\n});\n\\`\\`\\`\n\n### Checklist:\n- Zod input schema covers all inputs\n- Zod output schema matches execute() return\n- Node name matches workflow references\n- No \\`any\\` types \u2014 use \\`z.unknown()\\` if dynamic\n- \\`export default defineNode(...)\\`\n\n## Worker Workflows\n\nWorker trigger processes background jobs from a queue:\n\n\\`\\`\\`typescript\nWorkflow({ name: \"Process Job\", version: \"1.0.0\" })\n .addTrigger(\"worker\", { queue: \"background-jobs\" })\n .addStep({ name: \"process\", node: \"my-processor\", type: \"module\",\n inputs: { payload: \"js/ctx.request.body\", jobId: \"js/ctx.request.params.jobId\" } });\n\\`\\`\\`\n\nJob data: \\`ctx.request.body\\` = payload, \\`ctx.request.params.queue/jobId/attempt\\` = metadata.\nAdapters: NATS JetStream (recommended), BullMQ (Redis), InMemory (dev).\n\n## Testing\n\n\\`\\`\\`typescript\nimport { NodeTestHarness, WorkflowTestRunner } from \"@blokjs/runner\";\n\n// Unit test a node\nconst harness = new NodeTestHarness(myNode);\nconst result = await harness.execute({ input: \"data\" });\nharness.assertSuccess(result);\n\n// Integration test a workflow\nconst runner = new WorkflowTestRunner({ mockAllNodes: true });\nrunner.loadWorkflow(definition);\nconst wfResult = await runner.execute({ input: \"data\" });\n\\`\\`\\`\n\n## Blok Studio Help\n\n- Launch: \\`blokctl trace\\` or navigate to \\`/__blok\\`\n- \"No output\" \u2192 Node not returning data or Zod output validation failed\n- \"Step error\" \u2192 Expand error \u2014 check if 400 (validation) or 500 (runtime)\n- \"Vars not passing\" \u2192 Source step needs \\`set_var: true\\`, target needs \\`js/ctx.vars['name']\\`\n\n## Debugging Workers\n\n- NATS not reachable \u2192 Check \\`NATS_SERVERS\\` env var, ensure NATS is running\n- Job timeout \u2192 Increase \\`timeout\\` in trigger config or optimize node\n- Max retries exceeded \u2192 Check node errors, job moves to DLQ\n\n## Do NOT\n\n- Do NOT suggest class-based BlokService for new nodes\n- Do NOT generate code with \\`any\\` types\n- Do NOT assume \\`ctx.response.data\\` persists across steps\n- Do NOT skip Zod schemas when creating nodes\n- Do NOT edit files in \\`.blok/runtimes/\\`\n";
38
38
  declare const function_first_node_file = "import { defineNode } from \"@blokjs/runner\";\nimport { z } from \"zod\";\n\n/**\n * A function-first node that demonstrates the modern defineNode pattern.\n * This node is type-safe, validated, and requires 60% less boilerplate.\n */\nexport default defineNode({\n\tname: \"{{NODE_NAME}}\",\n\tdescription: \"A function-first node with Zod validation\",\n\n\t// Input schema using Zod - automatically validated\n\tinput: z.object({\n\t\tmessage: z.string().optional().default(\"Hello World\"),\n\t}),\n\n\t// Output schema using Zod - automatically validated\n\toutput: z.object({\n\t\tmessage: z.string(),\n\t\ttimestamp: z.string(),\n\t}),\n\n\t// Execute function - type-safe with inferred types from Zod schemas\n\tasync execute(ctx, input) {\n\t\t// Your business logic here\n\t\t// - ctx.vars: Access workflow variables\n\t\t// - ctx.request: Access HTTP request data\n\t\t// - ctx.logger: Log messages\n\t\t// - ctx.env: Access environment variables\n\n\t\t// Example: Store data for downstream nodes\n\t\tctx.vars[\"processed-message\"] = input.message;\n\n\t\t// Return type-safe output (validated automatically)\n\t\treturn {\n\t\t\tmessage: `Processed: ${input.message}`,\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t};\n\t},\n});\n";
39
39
  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, };
@@ -72,30 +72,24 @@ For more documentation, visit src/nodes/examples/README.md. The first three exam
72
72
  `;
73
73
  const workflow_template = `
74
74
  {
75
- "name": "",
76
- "description": "",
75
+ "name": "My Workflow",
76
+ "description": "What this workflow does",
77
77
  "version": "1.0.0",
78
78
  "trigger": {
79
79
  "http": {
80
80
  "method": "GET",
81
- "path": "/",
82
81
  "accept": "application/json"
83
82
  }
84
83
  },
85
84
  "steps": [
86
85
  {
87
- "name": "node-name",
88
- "node": "node-module-name",
89
- "type": "module"
90
- }
91
- ],
92
- "nodes": {
93
- "name": {
86
+ "id": "echo",
87
+ "use": "@blokjs/respond",
94
88
  "inputs": {
95
-
89
+ "body": "$.req.body"
96
90
  }
97
91
  }
98
- }
92
+ ]
99
93
  }
100
94
  `;
101
95
  const supervisord_nodejs = `
@@ -815,7 +809,73 @@ export default defineNode({
815
809
  | \\\`webhook\\\` | \\\`{ "source": "github", "events": ["push"] }\\\` |
816
810
  | \\\`websocket\\\` | \\\`{ "events": ["message"], "path": "/ws" }\\\` |
817
811
  | \\\`sse\\\` | \\\`{ "events": ["update"], "path": "/stream" }\\\` |
818
- | \\\`worker\\\` | \\\`{ "queue": "jobs", "concurrency": 5 }\\\` |
812
+ | \\\`worker\\\` | \\\`{ "queue": "jobs", "concurrency": 5, "retries": 3 }\\\` |
813
+
814
+ ### Worker Trigger
815
+
816
+ The worker trigger processes background jobs from a queue with retry logic and concurrency control.
817
+
818
+ \\\`\\\`\\\`typescript
819
+ Workflow({ name: "Process Job", version: "1.0.0" })
820
+ .addTrigger("worker", { queue: "background-jobs" })
821
+ .addStep({
822
+ name: "process",
823
+ node: "my-processor",
824
+ type: "module",
825
+ inputs: { payload: "js/ctx.request.body", jobId: "js/ctx.request.params.jobId" },
826
+ });
827
+ \\\`\\\`\\\`
828
+
829
+ Job context: \\\`ctx.request.body\\\` = payload, \\\`ctx.request.params.queue\\\` = queue name, \\\`ctx.request.params.jobId\\\` = job ID, \\\`ctx.request.params.attempt\\\` = attempt count, \\\`ctx.vars._worker_job\\\` = full metadata.
830
+
831
+ Adapters: NATS JetStream (recommended), BullMQ (Redis), InMemory (dev only).
832
+
833
+ ### NATS JetStream
834
+
835
+ Recommended queue/worker backend. Environment variables:
836
+ \\\`\\\`\\\`
837
+ NATS_SERVERS=localhost:4222
838
+ NATS_STREAM_NAME=blok-queue # or blok-worker for worker trigger
839
+ NATS_TOKEN= # optional auth
840
+ \\\`\\\`\\\`
841
+
842
+ Queue providers: \\\`kafka\\\`, \\\`rabbitmq\\\`, \\\`sqs\\\`, \\\`redis\\\`, \\\`beanstalk\\\`, \\\`nats\\\`
843
+
844
+ ### Standalone Workers (Go, Rust, Python)
845
+
846
+ Go, Rust, and Python SDKs include standalone NATS workers that connect directly to NATS without the TypeScript runner:
847
+
848
+ \\\`\\\`\\\`
849
+ WORKER_CONCURRENCY=1 # Max concurrent jobs
850
+ WORKER_MAX_RETRIES=3 # Max delivery attempts
851
+ WORKER_QUEUES=queue1,queue2 # Queues to consume
852
+ \\\`\\\`\\\`
853
+
854
+ ---
855
+
856
+ ## Testing Utilities
857
+
858
+ \\\`@blokjs/runner\\\` provides testing utilities for nodes and workflows.
859
+
860
+ ### NodeTestHarness — Unit test a single node:
861
+ \\\`\\\`\\\`typescript
862
+ import { NodeTestHarness } from "@blokjs/runner";
863
+ const harness = new NodeTestHarness(myNode);
864
+ const result = await harness.execute({ input: "data" });
865
+ harness.assertSuccess(result);
866
+ harness.assertOutput(result, { expected: "output" });
867
+ \\\`\\\`\\\`
868
+
869
+ ### WorkflowTestRunner — Integration test a workflow:
870
+ \\\`\\\`\\\`typescript
871
+ import { WorkflowTestRunner } from "@blokjs/runner";
872
+ const runner = new WorkflowTestRunner({ verbose: true });
873
+ runner.registerNode("validate", ValidateNode);
874
+ runner.mockNode("external-api", async (input) => ({ result: "mocked" }));
875
+ runner.loadWorkflow(workflowDefinition);
876
+ const result = await runner.execute({ input: "data" });
877
+ // result.success, result.output, result.trace, result.nodeResults
878
+ \\\`\\\`\\\`
819
879
 
820
880
  ---
821
881
 
@@ -924,6 +984,36 @@ export default defineNode({
924
984
  - No \\\`any\\\` types — use \\\`z.unknown()\\\` if dynamic
925
985
  - \\\`export default defineNode(...)\\\`
926
986
 
987
+ ## Worker Workflows
988
+
989
+ Worker trigger processes background jobs from a queue:
990
+
991
+ \\\`\\\`\\\`typescript
992
+ Workflow({ name: "Process Job", version: "1.0.0" })
993
+ .addTrigger("worker", { queue: "background-jobs" })
994
+ .addStep({ name: "process", node: "my-processor", type: "module",
995
+ inputs: { payload: "js/ctx.request.body", jobId: "js/ctx.request.params.jobId" } });
996
+ \\\`\\\`\\\`
997
+
998
+ Job data: \\\`ctx.request.body\\\` = payload, \\\`ctx.request.params.queue/jobId/attempt\\\` = metadata.
999
+ Adapters: NATS JetStream (recommended), BullMQ (Redis), InMemory (dev).
1000
+
1001
+ ## Testing
1002
+
1003
+ \\\`\\\`\\\`typescript
1004
+ import { NodeTestHarness, WorkflowTestRunner } from "@blokjs/runner";
1005
+
1006
+ // Unit test a node
1007
+ const harness = new NodeTestHarness(myNode);
1008
+ const result = await harness.execute({ input: "data" });
1009
+ harness.assertSuccess(result);
1010
+
1011
+ // Integration test a workflow
1012
+ const runner = new WorkflowTestRunner({ mockAllNodes: true });
1013
+ runner.loadWorkflow(definition);
1014
+ const wfResult = await runner.execute({ input: "data" });
1015
+ \\\`\\\`\\\`
1016
+
927
1017
  ## Blok Studio Help
928
1018
 
929
1019
  - Launch: \\\`blokctl trace\\\` or navigate to \\\`/__blok\\\`
@@ -931,6 +1021,12 @@ export default defineNode({
931
1021
  - "Step error" → Expand error — check if 400 (validation) or 500 (runtime)
932
1022
  - "Vars not passing" → Source step needs \\\`set_var: true\\\`, target needs \\\`js/ctx.vars['name']\\\`
933
1023
 
1024
+ ## Debugging Workers
1025
+
1026
+ - NATS not reachable → Check \\\`NATS_SERVERS\\\` env var, ensure NATS is running
1027
+ - Job timeout → Increase \\\`timeout\\\` in trigger config or optimize node
1028
+ - Max retries exceeded → Check node errors, job moves to DLQ
1029
+
934
1030
  ## Do NOT
935
1031
 
936
1032
  - Do NOT suggest class-based BlokService for new nodes
@@ -2,6 +2,8 @@ import { spawn } from "node:child_process";
2
2
  import http from "node:http";
3
3
  import path from "node:path";
4
4
  import fsExtra from "fs-extra";
5
+ import { waitForGrpcPort } from "../../services/health-probe.js";
6
+ import { detectRr } from "../../services/runtime-detector.js";
5
7
  import { readProjectConfig } from "../../services/runtime-setup.js";
6
8
  const runningProcesses = [];
7
9
  function spawnProcess(cmd, args, name, currentPath, cwd, env) {
@@ -38,7 +40,7 @@ function killAllGroups(signal) {
38
40
  }
39
41
  }
40
42
  }
41
- function waitForHealth(port, timeoutMs, proc) {
43
+ function waitForHttpHealth(port, timeoutMs, proc) {
42
44
  return new Promise((resolve) => {
43
45
  if (proc && proc.exitCode !== null) {
44
46
  resolve(false);
@@ -74,13 +76,25 @@ function waitForHealth(port, timeoutMs, proc) {
74
76
  }
75
77
  export async function devProject(opts) {
76
78
  const currentPath = process.cwd();
77
- console.log("Starting the development server...");
79
+ const useHttpFallback = opts.withHttpFallback === true;
80
+ const transport = useHttpFallback ? "http" : "grpc";
81
+ console.log(`Starting the development server (transport=${transport})...`);
78
82
  console.log("Current path: ", currentPath);
83
+ if (useHttpFallback) {
84
+ console.log(" ⚠ --with-http-fallback is deprecated and will be removed in v0.4.0.");
85
+ }
79
86
  const config = readProjectConfig(currentPath);
80
87
  const runtimeDefs = [];
81
88
  if (config?.runtimes) {
82
89
  for (const [, rt] of Object.entries(config.runtimes)) {
83
- const cmdParts = rt.startCmd.split(" ");
90
+ let bootCmd = useHttpFallback ? rt.startCmd : (rt.grpcStartCmd ?? rt.startCmd);
91
+ if (rt.kind === "php" && !useHttpFallback && bootCmd.startsWith("rr ")) {
92
+ const rrBin = detectRr();
93
+ if (rrBin && rrBin !== "rr") {
94
+ bootCmd = `${rrBin}${bootCmd.slice(2)}`;
95
+ }
96
+ }
97
+ const cmdParts = bootCmd.split(" ");
84
98
  const cmd = cmdParts[0];
85
99
  const args = cmdParts.slice(1);
86
100
  const runtimeCwd = path.resolve(currentPath, rt.cwd);
@@ -88,16 +102,21 @@ export async function devProject(opts) {
88
102
  console.log(` Warning: ${rt.label} runtime directory not found at ${rt.cwd}. Skipping.`);
89
103
  continue;
90
104
  }
105
+ const grpcPort = rt.grpcPort ?? rt.port + 1000;
106
+ const probePort = useHttpFallback ? rt.port : grpcPort;
107
+ const env = {
108
+ PORT: String(rt.port),
109
+ GRPC_PORT: String(grpcPort),
110
+ HOST: "0.0.0.0",
111
+ BLOK_TRANSPORT: transport,
112
+ };
91
113
  runtimeDefs.push({
92
114
  cmd,
93
115
  args,
94
- name: `${rt.label} Runtime (port ${rt.port})`,
116
+ name: `${rt.label} Runtime (${transport} port ${probePort})`,
95
117
  cwd: runtimeCwd,
96
- env: {
97
- PORT: String(rt.port),
98
- HOST: "0.0.0.0",
99
- },
100
- port: rt.port,
118
+ env,
119
+ port: probePort,
101
120
  });
102
121
  }
103
122
  }
@@ -132,15 +151,21 @@ export async function devProject(opts) {
132
151
  }
133
152
  }
134
153
  if (config?.runtimes && Object.keys(config.runtimes).length > 0) {
135
- console.log("\nRuntime health endpoints:");
154
+ console.log("\nRuntime listeners:");
136
155
  for (const [, rt] of Object.entries(config.runtimes)) {
137
- console.log(` ${rt.label}: http://localhost:${rt.port}/health`);
156
+ if (useHttpFallback) {
157
+ console.log(` ${rt.label}: http://localhost:${rt.port}/health`);
158
+ }
159
+ else {
160
+ const grpcPort = rt.grpcPort ?? rt.port + 1000;
161
+ console.log(` ${rt.label}: gRPC 127.0.0.1:${grpcPort}`);
162
+ }
138
163
  }
139
164
  }
140
165
  if (healthChecks.length > 0) {
141
166
  console.log("\nWaiting for runtimes to be ready...");
142
167
  const maxWait = 120_000;
143
- const results = await Promise.all(healthChecks.map((hc) => waitForHealth(hc.port, maxWait, hc.proc)));
168
+ const results = await Promise.all(healthChecks.map((hc) => useHttpFallback ? waitForHttpHealth(hc.port, maxWait, hc.proc) : waitForGrpcPort(hc.port, maxWait, hc.proc)));
144
169
  const allReady = results.every(Boolean);
145
170
  if (allReady) {
146
171
  console.log("All runtimes ready.\n");
@@ -151,6 +176,17 @@ export async function devProject(opts) {
151
176
  console.log("Starting NodeJS runner anyway.\n");
152
177
  }
153
178
  }
179
+ const traceEnv = {};
180
+ if (!process.env.BLOK_TRACE_STORE) {
181
+ traceEnv.BLOK_TRACE_STORE = "sqlite";
182
+ }
183
+ if (!process.env.BLOK_TRACE_SQLITE_PATH) {
184
+ traceEnv.BLOK_TRACE_SQLITE_PATH = path.join(".blok", "trace.db");
185
+ }
186
+ const triggerEnv = {
187
+ ...traceEnv,
188
+ BLOK_TRANSPORT: transport,
189
+ };
154
190
  if (config?.triggers && Object.keys(config.triggers).length > 0) {
155
191
  console.log("Starting triggers...");
156
192
  for (const [, trigger] of Object.entries(config.triggers)) {
@@ -162,11 +198,12 @@ export async function devProject(opts) {
162
198
  }
163
199
  spawnProcess(cmd, args, `${trigger.label} (port ${trigger.port})`, currentPath, undefined, {
164
200
  PORT: String(trigger.port),
201
+ ...triggerEnv,
165
202
  });
166
203
  }
167
204
  }
168
205
  else {
169
- spawnProcess("bun", ["--watch", "run", "src/index.ts"], "Blok Runner", currentPath);
206
+ spawnProcess("bun", ["--watch", "run", "src/index.ts"], "Blok Runner", currentPath, undefined, triggerEnv);
170
207
  }
171
208
  const keepAlive = setInterval(() => { }, 60_000);
172
209
  let stopping = false;
@@ -12,7 +12,7 @@ const VALID_TRIGGER_TYPES = [
12
12
  ];
13
13
  const VALID_STEP_TYPES = ["module", "local", "runtime.python3", "runtime.go", "runtime.java"];
14
14
  const VALID_HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH", "ANY", "*"];
15
- const VALID_QUEUE_PROVIDERS = ["kafka", "rabbitmq", "sqs", "redis", "beanstalk"];
15
+ const VALID_QUEUE_PROVIDERS = ["kafka", "rabbitmq", "sqs", "redis", "beanstalk", "nats"];
16
16
  const VALID_PUBSUB_PROVIDERS = ["gcp", "aws", "azure"];
17
17
  const VALID_WEBHOOK_SOURCES = ["github", "stripe", "shopify", "custom"];
18
18
  export function validateWorkflow(jsonString) {
@@ -1,6 +1,7 @@
1
1
  import { Command } from "commander";
2
2
  import { program } from "../../services/commander.js";
3
3
  import { migrateNode } from "./node.js";
4
+ import { migrateWorkflows } from "./workflows.js";
4
5
  const migrate = new Command("migrate").description("Migrate nodes and workflows to newer patterns");
5
6
  const node = new Command("node")
6
7
  .description("Migrate a class-based node to function-first pattern")
@@ -10,5 +11,15 @@ const node = new Command("node")
10
11
  .action(async (options) => {
11
12
  await migrateNode(options);
12
13
  });
14
+ const workflows = new Command("workflows")
15
+ .description("Migrate v1 JSON workflows to the v2 shape (id+use+inputs, branch primitive, ANY method)")
16
+ .option("-d, --dir <value>", "Path to the JSON workflows directory (defaults to ./workflows/json or ./triggers/http/workflows/json)")
17
+ .option("--dry-run", "Print what would change without writing files")
18
+ .option("--backup", "Create .bak files next to each migrated workflow (default true)")
19
+ .option("--no-backup", "Skip backup creation")
20
+ .action(async (options) => {
21
+ await migrateWorkflows(options);
22
+ });
13
23
  migrate.addCommand(node);
24
+ migrate.addCommand(workflows);
14
25
  program.addCommand(migrate);
@@ -0,0 +1,3 @@
1
+ import type { OptionValues } from "commander";
2
+ export declare function migrateWorkflows(opts: OptionValues): Promise<void>;
3
+ export declare function rewriteLegacyExpressions<T>(value: T): T;