blokctl 0.4.0 → 0.6.2

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.
@@ -0,0 +1,2 @@
1
+ import type { OptionValues } from "commander";
2
+ export declare function checkProject(_opts: OptionValues): Promise<void>;
@@ -0,0 +1,39 @@
1
+ import color from "picocolors";
2
+ import { readProjectConfig, validateProjectRuntimes } from "../../services/runtime-setup.js";
3
+ export async function checkProject(_opts) {
4
+ const currentPath = process.cwd();
5
+ const config = readProjectConfig(currentPath);
6
+ if (!config) {
7
+ console.error(" No .blok/config.json found. Run this from a Blok project directory.");
8
+ process.exit(1);
9
+ }
10
+ console.log(`\n ${color.bold("Blok Runtime Version Check")}`);
11
+ console.log(" ──────────────────────────\n");
12
+ const results = await validateProjectRuntimes(currentPath);
13
+ if (results.length === 0) {
14
+ console.log(" No runtime version constraints configured.");
15
+ console.log(" Runtime versions will be pinned automatically on next project creation.\n");
16
+ process.exit(0);
17
+ }
18
+ console.log(` ${color.bold("Project runtimes")} (.blok/config.json):`);
19
+ for (const r of results) {
20
+ if (r.satisfied) {
21
+ console.log(` ${color.green("✓")} ${r.label} ${r.found} (requires ${r.required})`);
22
+ }
23
+ else {
24
+ console.log(` ${color.red("✗")} ${r.label} ${r.found || "not installed"} (requires ${r.required})`);
25
+ }
26
+ }
27
+ console.log();
28
+ const failures = results.filter((r) => !r.satisfied);
29
+ if (failures.length > 0) {
30
+ for (const f of failures) {
31
+ console.log(f.message);
32
+ console.log();
33
+ }
34
+ console.log(` ${color.red(`${failures.length} check${failures.length > 1 ? "s" : ""} failed.`)}\n`);
35
+ process.exit(1);
36
+ }
37
+ console.log(` ${color.green("All checks passed.")}\n`);
38
+ process.exit(0);
39
+ }
@@ -11,12 +11,13 @@ import { isNonInteractive, parseCommaSeparated, resolveOrThrow } from "../../ser
11
11
  import { manager as pm } from "../../services/package-manager.js";
12
12
  import { detectRuntimes } from "../../services/runtime-detector.js";
13
13
  import { createTriggerConfig, generateRuntimeEnvVars, generateSupervisordConfig, generateTriggerEnvVars, generateTriggerSupervisordConfig, setupRuntime, writeProjectConfig, } from "../../services/runtime-setup.js";
14
+ import { computeDefaultConstraint } from "../../services/semver-utils.js";
14
15
  import { agents_md, claude_md, examples_url, node_file, package_dependencies, package_dev_dependencies, } from "./utils/Examples.js";
15
16
  const exec = util.promisify(child_process.exec);
16
17
  const HOME_DIR = `${os.homedir()}/.blok`;
17
18
  const GITHUB_REPO_LOCAL = `${HOME_DIR}/blok`;
18
19
  const GITHUB_REPO_REMOTE = "https://github.com/well-prado/blok.git";
19
- const GITHUB_REPO_RELEASE_TAG = "v0.4.0";
20
+ const GITHUB_REPO_RELEASE_TAG = "v0.6.2";
20
21
  fsExtra.ensureDirSync(HOME_DIR);
21
22
  const options = {
22
23
  baseDir: HOME_DIR,
@@ -86,8 +87,12 @@ export async function createProject(opts, version, currentPath = false, localRep
86
87
  { label: "NodeJS", value: "node", hint: "always included" },
87
88
  ...detectedRuntimes.map((rt) => {
88
89
  let hint;
89
- if (rt.available) {
90
- hint = `${rt.toolchain} ${rt.version || ""} detected`.trim();
90
+ if (rt.available && rt.version) {
91
+ const constraint = computeDefaultConstraint(rt.version);
92
+ hint = `${rt.toolchain} ${rt.version} detected (will pin ${constraint})`;
93
+ }
94
+ else if (rt.available) {
95
+ hint = `${rt.toolchain} detected`;
91
96
  }
92
97
  else if (rt.secondaryTool && !rt.secondaryTool.available) {
93
98
  hint = `${rt.secondaryTool.name} not found - will be skipped`;
@@ -412,7 +417,7 @@ export async function createProject(opts, version, currentPath = false, localRep
412
417
  "@blokjs/trigger-pubsub": "triggers/pubsub",
413
418
  "@blokjs/trigger-queue": "triggers/queue",
414
419
  };
415
- const BLOKJS_DEP_RANGE = "^0.4.0";
420
+ const BLOKJS_DEP_RANGE = "^0.6.2";
416
421
  for (const depGroup of ["dependencies", "devDependencies", "peerDependencies"]) {
417
422
  const deps = packageJsonContent[depGroup];
418
423
  if (!deps)
@@ -434,7 +439,11 @@ export async function createProject(opts, version, currentPath = false, localRep
434
439
  }
435
440
  }
436
441
  else if (typeof ver === "string" &&
437
- (ver === "^0.2.0" || ver === "^0.2" || ver === "0.2.0" || ver.startsWith("^0.2.") || ver.startsWith("0.2.")) &&
442
+ (ver === "^0.2.0" ||
443
+ ver === "^0.2" ||
444
+ ver === "0.2.0" ||
445
+ ver.startsWith("^0.2.") ||
446
+ ver.startsWith("0.2.")) &&
438
447
  (pkg.startsWith("@blokjs/") || pkg === "blokctl")) {
439
448
  deps[pkg] = BLOKJS_DEP_RANGE;
440
449
  }
@@ -475,12 +484,12 @@ export async function createProject(opts, version, currentPath = false, localRep
475
484
  if (selectedTriggers.includes("pubsub")) {
476
485
  triggerPackageDeps["@blokjs/trigger-pubsub"] = localRepoPath
477
486
  ? `file:${path.resolve(repoSource, "triggers/pubsub")}`
478
- : "^0.2.0";
487
+ : BLOKJS_DEP_RANGE;
479
488
  }
480
489
  if (selectedTriggers.includes("queue")) {
481
490
  triggerPackageDeps["@blokjs/trigger-queue"] = localRepoPath
482
491
  ? `file:${path.resolve(repoSource, "triggers/queue")}`
483
- : "^0.2.0";
492
+ : BLOKJS_DEP_RANGE;
484
493
  }
485
494
  if (Object.keys(triggerPackageDeps).length > 0) {
486
495
  packageJsonContent.dependencies = {
@@ -707,10 +716,10 @@ if (process.env.DISABLE_TRIGGER_RUN !== "true") {
707
716
  if (triggerKind === "sse") {
708
717
  return `import { DefaultLogger } from "@blokjs/runner";
709
718
  import { type Span, metrics, trace } from "@opentelemetry/api";
710
- import SSEServer from "./runner/SSEServer";
719
+ import SSETrigger from "./SSETrigger";
711
720
 
712
721
  export default class App {
713
- private sseServer: SSEServer = <SSEServer>{};
722
+ private sseTrigger: SSETrigger = <SSETrigger>{};
714
723
  protected trigger_initializer = 0;
715
724
  protected initializer = 0;
716
725
  protected tracer = trace.getTracer(
@@ -724,12 +733,12 @@ export default class App {
724
733
 
725
734
  constructor() {
726
735
  this.initializer = performance.now();
727
- this.sseServer = new SSEServer();
736
+ this.sseTrigger = new SSETrigger();
728
737
  }
729
738
 
730
739
  async run() {
731
740
  this.tracer.startActiveSpan("initialization", async (span: Span) => {
732
- await this.sseServer.listen();
741
+ await this.sseTrigger.listen();
733
742
  this.initializer = performance.now() - this.initializer;
734
743
 
735
744
  this.logger.log(\`Server initialized in \${(this.initializer).toFixed(2)}ms\`);
@@ -741,13 +750,9 @@ export default class App {
741
750
  span.end();
742
751
  });
743
752
  }
744
-
745
- getApp() {
746
- return this.sseServer.getApp();
747
- }
748
753
  }
749
754
 
750
- {
755
+ if (process.env.DISABLE_TRIGGER_RUN !== "true") {
751
756
  new App().run();
752
757
  }
753
758
  `;
@@ -864,26 +869,26 @@ function replaceBlokImportsInDirectory(dirPath) {
864
869
  }
865
870
  }
866
871
  function fixRunnerImportPaths(triggerDestDir, triggerKind) {
867
- const filesToFix = [];
872
+ const fileFixes = [];
868
873
  if (triggerKind === "http") {
869
- filesToFix.push(`${triggerDestDir}/runner/HttpTrigger.ts`);
874
+ fileFixes.push({ file: `${triggerDestDir}/runner/HttpTrigger.ts`, up: "../../../" });
870
875
  }
871
876
  else if (triggerKind === "sse") {
872
- filesToFix.push(`${triggerDestDir}/runner/SSEServer.ts`);
877
+ fileFixes.push({ file: `${triggerDestDir}/SSETrigger.ts`, up: "../../" });
873
878
  }
874
879
  else if (triggerKind === "pubsub") {
875
- filesToFix.push(`${triggerDestDir}/runner/PubSubServer.ts`);
880
+ fileFixes.push({ file: `${triggerDestDir}/runner/PubSubServer.ts`, up: "../../../" });
876
881
  }
877
882
  else if (triggerKind === "queue") {
878
- filesToFix.push(`${triggerDestDir}/runner/QueueServer.ts`);
883
+ fileFixes.push({ file: `${triggerDestDir}/runner/QueueServer.ts`, up: "../../../" });
879
884
  }
880
- for (const filePath of filesToFix) {
881
- if (!fsExtra.existsSync(filePath))
885
+ for (const { file, up } of fileFixes) {
886
+ if (!fsExtra.existsSync(file))
882
887
  continue;
883
- let content = fsExtra.readFileSync(filePath, "utf8");
884
- content = content.replace(/from ["']\.\.\/Nodes["']/g, 'from "../../../Nodes"');
885
- content = content.replace(/from ["']\.\.\/Workflows["']/g, 'from "../../../Workflows"');
886
- fsExtra.writeFileSync(filePath, content);
888
+ let content = fsExtra.readFileSync(file, "utf8");
889
+ content = content.replace(/from ["']\.\.\/Nodes["']/g, `from "${up}Nodes"`);
890
+ content = content.replace(/from ["']\.\.\/Workflows["']/g, `from "${up}Workflows"`);
891
+ fsExtra.writeFileSync(file, content);
887
892
  }
888
893
  }
889
894
  function updatePubSubProvider(triggerDestDir, provider) {
@@ -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, \"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";
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.prev\\` carries the immediately previous step's output.**\nEach step's output replaces \\`ctx.prev\\`. Use it for adjacent-step access only.\n\n**Rule 2: \\`ctx.state[id]\\` PERSISTS across the entire workflow.**\nEvery step's output is auto-stored at \\`ctx.state[<step-id>]\\` (the v2 default-store rule). Downstream steps reference it via \\`$.state.<id>\\` (TS DSL) or \\`\"$.state.<id>\"\\` / \\`\"js/ctx.state.<id>\"\\` (JSON). Opt out per step with \\`ephemeral: true\\`.\n\n### Data Flow Example\n\n```\nStep 1: id \"fetch-user\"\n \u2192 ctx.state[\"fetch-user\"] = { id: \"123\", name: \"Alice\" }\n \u2192 ctx.prev = { id: \"123\", name: \"Alice\" }\n\nStep 2: id \"transform\"\n \u2192 ctx.state[\"transform\"] = { result: \"done\" }\n \u2192 ctx.prev = { result: \"done\" } \u2190 Step 1 output GONE from prev\n \u2192 ctx.state[\"fetch-user\"] still available\n\nStep 3: id \"output\"\n \u2192 Can read ctx.state[\"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\\` (full context), \\`data\\` (ctx.prev.data), \\`func\\` (ctx.func), \\`vars\\` (alias for ctx.state).\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 { \"id\": \"fetch\", \"use\": \"@blokjs/api-call\", \"inputs\": { \"url\": \"https://api.example.com\", \"method\": \"GET\" } },\n { \"id\": \"process\", \"use\": \"my-node\", \"inputs\": { \"data\": \"$.state.fetch\" } },\n { \"id\": \"go-step\", \"use\": \"chain-test\", \"type\": \"runtime.go\", \"inputs\": { \"processed\": \"$.state.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 \\`$.state.<id>\\` (or \\`js/ctx.state.<id>\\`) to pass data between non-adjacent steps \u2014 every step default-stores its output there\n- Opt out per step with \\`ephemeral: true\\` when the step is a side effect only\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.prev\\` is the immediately previous step's output.** Overwritten every step.\n2. **\\`ctx.state[<id>]\\` PERSISTS across the workflow.** Every step default-stores its output there; reference via \\`$.state.<id>\\` or \\`js/ctx.state.<id>\\`. Opt out with \\`ephemeral: true\\`.\n3. **Blueprint Mapper resolves \\`$.<path>\\` and \\`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 step has an \\`id\\` and a \\`use\\` (v2). v1's \\`name\\` + \\`nodes{}\\` still works but is normalized at load time.\n2. **Trace data flow**: Does the target step reference the correct source id (\\`$.state.<id>\\`)? Did the source step have \\`ephemeral: true\\` accidentally?\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.state['X'] undefined\\` | Source step has \\`ephemeral: true\\`, or the id doesn't match what's referenced in \\`$.state.<id>\\` |\n| \\`set_var, which was removed in v0.5\\` | Drop \\`set_var: true\\` (it's the default) or replace \\`set_var: false\\` with \\`ephemeral: true\\`. Run \\`blokctl migrate workflows\\`. |\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- \"State not passing\" \u2192 Source step has \\`ephemeral: true\\`, OR target's \\`$.state.<id>\\` references a non-existent step id\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, };
@@ -657,25 +657,26 @@ type Context = {
657
657
 
658
658
  ### The Two Critical Rules
659
659
 
660
- **Rule 1: \\\`ctx.response.data\\\` is OVERWRITTEN after every step.**
661
- Each step's output replaces the previous \\\`ctx.response.data\\\`. If you need a step's output later, store it in \\\`ctx.vars\\\`.
660
+ **Rule 1: \\\`ctx.prev\\\` carries the immediately previous step's output.**
661
+ Each step's output replaces \\\`ctx.prev\\\`. Use it for adjacent-step access only.
662
662
 
663
- **Rule 2: \\\`ctx.vars\\\` PERSISTS across the entire workflow.**
664
- Use \\\`set_var: true\\\` on a step to auto-store its output in \\\`ctx.vars[stepName]\\\`. Downstream steps access it via \\\`ctx.vars['step-name']\\\`.
663
+ **Rule 2: \\\`ctx.state[id]\\\` PERSISTS across the entire workflow.**
664
+ Every step's output is auto-stored at \\\`ctx.state[<step-id>]\\\` (the v2 default-store rule). Downstream steps reference it via \\\`$.state.<id>\\\` (TS DSL) or \\\`"$.state.<id>"\\\` / \\\`"js/ctx.state.<id>"\\\` (JSON). Opt out per step with \\\`ephemeral: true\\\`.
665
665
 
666
666
  ### Data Flow Example
667
667
 
668
668
  \`\`\`
669
- Step 1: "fetch-user" (set_var: true)
670
- → ctx.response.data = { id: "123", name: "Alice" }
671
- → ctx.vars["fetch-user"] = { id: "123", name: "Alice" }
669
+ Step 1: id "fetch-user"
670
+ → ctx.state["fetch-user"] = { id: "123", name: "Alice" }
671
+ → ctx.prev = { id: "123", name: "Alice" }
672
672
 
673
- Step 2: "transform"
674
- → ctx.response.data = { result: "done" } ← Step 1 output GONE from response
675
- → ctx.vars["fetch-user"] still available
673
+ Step 2: id "transform"
674
+ → ctx.state["transform"] = { result: "done" }
675
+ → ctx.prev = { result: "done" } ← Step 1 output GONE from prev
676
+ → ctx.state["fetch-user"] still available
676
677
 
677
- Step 3: "output"
678
- → Can read ctx.vars["fetch-user"].name ← still "Alice"
678
+ Step 3: id "output"
679
+ → Can read ctx.state["fetch-user"].name ← still "Alice"
679
680
  \`\`\`
680
681
 
681
682
  ### Blueprint Mapper — Expression Resolution
@@ -692,7 +693,7 @@ Node inputs support dynamic expressions resolved BEFORE node execution:
692
693
  }
693
694
  \`\`\`
694
695
 
695
- Available in js/ expressions: \\\`ctx\\\`, \\\`data\\\` (ctx.response.data), \\\`func\\\` (ctx.func), \\\`vars\\\` (ctx.vars)
696
+ Available in js/ expressions: \\\`ctx\\\` (full context), \\\`data\\\` (ctx.prev.data), \\\`func\\\` (ctx.func), \\\`vars\\\` (alias for ctx.state).
696
697
 
697
698
  ---
698
699
 
@@ -747,15 +748,10 @@ export default defineNode({
747
748
  "http": { "method": "POST", "path": "/api/process", "accept": "application/json" }
748
749
  },
749
750
  "steps": [
750
- { "name": "fetch", "node": "@blokjs/api-call", "type": "module" },
751
- { "name": "process", "node": "my-node", "type": "module", "set_var": true },
752
- { "name": "go-step", "node": "chain-test", "type": "runtime.go" }
753
- ],
754
- "nodes": {
755
- "fetch": { "inputs": { "url": "https://api.example.com", "method": "GET" } },
756
- "process": { "inputs": { "data": "js/ctx.response.data" } },
757
- "go-step": { "inputs": { "processed": "js/ctx.vars['process']" } }
758
- }
751
+ { "id": "fetch", "use": "@blokjs/api-call", "inputs": { "url": "https://api.example.com", "method": "GET" } },
752
+ { "id": "process", "use": "my-node", "inputs": { "data": "$.state.fetch" } },
753
+ { "id": "go-step", "use": "chain-test", "type": "runtime.go", "inputs": { "processed": "$.state.process" } }
754
+ ]
759
755
  }
760
756
  \`\`\`
761
757
 
@@ -912,8 +908,8 @@ Real-time workflow trace visualization UI.
912
908
 
913
909
  ## Do
914
910
 
915
- - Use \\\`ctx.vars\\\` with \\\`set_var: true\\\` to pass data between non-adjacent steps
916
- - Use \\\`js/ctx.vars['step-name'].field\\\` in workflow inputs for data flow
911
+ - Use \\\`$.state.<id>\\\` (or \\\`js/ctx.state.<id>\\\`) to pass data between non-adjacent steps — every step default-stores its output there
912
+ - Opt out per step with \\\`ephemeral: true\\\` when the step is a side effect only
917
913
  - Use Zod schemas for all input/output validation
918
914
  - Use \\\`defineNode()\\\` for all new nodes
919
915
  - Handle errors via GlobalError with appropriate HTTP status codes
@@ -936,16 +932,16 @@ npm test # Run tests
936
932
 
937
933
  ## Context Rules (Memorize These)
938
934
 
939
- 1. **\\\`ctx.response.data\\\` is OVERWRITTEN every step.** Previous output GONE unless stored in vars.
940
- 2. **\\\`ctx.vars\\\` PERSISTS across the workflow.** Use \\\`set_var: true\\\` or \\\`js/ctx.vars['step']\\\`.
941
- 3. **Blueprint Mapper resolves \\\`js/\\\` expressions BEFORE node execution.**
935
+ 1. **\\\`ctx.prev\\\` is the immediately previous step's output.** Overwritten every step.
936
+ 2. **\\\`ctx.state[<id>]\\\` PERSISTS across the workflow.** Every step default-stores its output there; reference via \\\`$.state.<id>\\\` or \\\`js/ctx.state.<id>\\\`. Opt out with \\\`ephemeral: true\\\`.
937
+ 3. **Blueprint Mapper resolves \\\`$.<path>\\\` and \\\`js/\\\` expressions BEFORE node execution.**
942
938
 
943
939
  When users have data flow issues, check these three things first.
944
940
 
945
941
  ## Debugging Workflows
946
942
 
947
- 1. **Verify structure**: Every \\\`steps[].name\\\` must match a key in \\\`nodes\\\`
948
- 2. **Trace data flow**: Which steps have \\\`set_var: true\\\`? Do \\\`js/\\\` expressions reference correct step names?
943
+ 1. **Verify structure**: Every step has an \\\`id\\\` and a \\\`use\\\` (v2). v1's \\\`name\\\` + \\\`nodes{}\\\` still works but is normalized at load time.
944
+ 2. **Trace data flow**: Does the target step reference the correct source id (\\\`$.state.<id>\\\`)? Did the source step have \\\`ephemeral: true\\\` accidentally?
949
945
  3. **Check runtimes**: SDK containers running? \\\`GET http://localhost:{port}/health\\\`
950
946
  4. **Check Studio traces**: \\\`/__blok/runs/:id\\\` shows step-by-step inputs/outputs/errors
951
947
 
@@ -956,7 +952,8 @@ When users have data flow issues, check these three things first.
956
952
  | \\\`Node type X not found\\\` | Wrong \\\`type\\\` in step — use module, local, or runtime.* |
957
953
  | \\\`Validation failed\\\` | Zod schema mismatch — check input schema vs actual data |
958
954
  | \\\`Runtime execution error\\\` | SDK container not running — check health endpoint |
959
- | \\\`ctx.vars['X'] undefined\\\` | Source step missing \\\`set_var: true\\\` or name mismatch |
955
+ | \\\`ctx.state['X'] undefined\\\` | Source step has \\\`ephemeral: true\\\`, or the id doesn't match what's referenced in \\\`$.state.<id>\\\` |
956
+ | \\\`set_var, which was removed in v0.5\\\` | Drop \\\`set_var: true\\\` (it's the default) or replace \\\`set_var: false\\\` with \\\`ephemeral: true\\\`. Run \\\`blokctl migrate workflows\\\`. |
960
957
 
961
958
  ## Generating Code
962
959
 
@@ -1019,7 +1016,7 @@ const wfResult = await runner.execute({ input: "data" });
1019
1016
  - Launch: \\\`blokctl trace\\\` or navigate to \\\`/__blok\\\`
1020
1017
  - "No output" → Node not returning data or Zod output validation failed
1021
1018
  - "Step error" → Expand error — check if 400 (validation) or 500 (runtime)
1022
- - "Vars not passing" → Source step needs \\\`set_var: true\\\`, target needs \\\`js/ctx.vars['name']\\\`
1019
+ - "State not passing" → Source step has \\\`ephemeral: true\\\`, OR target's \\\`$.state.<id>\\\` references a non-existent step id
1023
1020
 
1024
1021
  ## Debugging Workers
1025
1022
 
@@ -1,10 +1,9 @@
1
1
  import { spawn } from "node:child_process";
2
- import http from "node:http";
3
2
  import path from "node:path";
4
3
  import fsExtra from "fs-extra";
5
4
  import { waitForGrpcPort } from "../../services/health-probe.js";
6
5
  import { detectRr } from "../../services/runtime-detector.js";
7
- import { readProjectConfig } from "../../services/runtime-setup.js";
6
+ import { readProjectConfig, validateProjectRuntimes } from "../../services/runtime-setup.js";
8
7
  const runningProcesses = [];
9
8
  function spawnProcess(cmd, args, name, currentPath, cwd, env) {
10
9
  const child = spawn(cmd, args, {
@@ -40,55 +39,45 @@ function killAllGroups(signal) {
40
39
  }
41
40
  }
42
41
  }
43
- function waitForHttpHealth(port, timeoutMs, proc) {
44
- return new Promise((resolve) => {
45
- if (proc && proc.exitCode !== null) {
46
- resolve(false);
47
- return;
48
- }
49
- const start = Date.now();
50
- let done = false;
51
- function finish(result) {
52
- if (done)
53
- return;
54
- done = true;
55
- clearInterval(interval);
56
- resolve(result);
57
- }
58
- proc?.on("exit", () => finish(false));
59
- const interval = setInterval(() => {
60
- if (done)
61
- return;
62
- if (Date.now() - start > timeoutMs) {
63
- finish(false);
64
- return;
65
- }
66
- const req = http.get(`http://localhost:${port}/health`, (res) => {
67
- res.resume();
68
- if (res.statusCode === 200)
69
- finish(true);
70
- });
71
- req.on("error", () => {
72
- });
73
- req.setTimeout(1000, () => req.destroy());
74
- }, 500);
75
- });
76
- }
77
42
  export async function devProject(opts) {
78
43
  const currentPath = process.cwd();
79
- const useHttpFallback = opts.withHttpFallback === true;
80
- const transport = useHttpFallback ? "http" : "grpc";
81
- console.log(`Starting the development server (transport=${transport})...`);
44
+ console.log("Starting the development server (transport=grpc)...");
82
45
  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
- }
86
46
  const config = readProjectConfig(currentPath);
47
+ const skipVersionCheck = opts.skipVersionCheck === true;
48
+ const validationResults = await validateProjectRuntimes(currentPath);
49
+ if (validationResults.length > 0) {
50
+ const failures = validationResults.filter((r) => !r.satisfied);
51
+ const successes = validationResults.filter((r) => r.satisfied);
52
+ if (failures.length > 0 && !skipVersionCheck) {
53
+ console.error("\n Runtime version requirements not met:\n");
54
+ for (const f of failures) {
55
+ console.error(f.message);
56
+ console.error();
57
+ }
58
+ console.error(" Tip: Use --skip-version-check to bypass this check.\n");
59
+ process.exit(1);
60
+ }
61
+ if (failures.length > 0 && skipVersionCheck) {
62
+ console.log("\n Runtime version warnings:");
63
+ for (const f of failures) {
64
+ console.log(` ! ${f.label} ${f.found || "not installed"} (requires ${f.required}) — SKIPPED`);
65
+ }
66
+ }
67
+ if (successes.length > 0) {
68
+ if (failures.length === 0)
69
+ console.log("\n Runtime version check:");
70
+ for (const s of successes) {
71
+ console.log(s.message);
72
+ }
73
+ }
74
+ console.log();
75
+ }
87
76
  const runtimeDefs = [];
88
77
  if (config?.runtimes) {
89
78
  for (const [, rt] of Object.entries(config.runtimes)) {
90
- let bootCmd = useHttpFallback ? rt.startCmd : (rt.grpcStartCmd ?? rt.startCmd);
91
- if (rt.kind === "php" && !useHttpFallback && bootCmd.startsWith("rr ")) {
79
+ let bootCmd = rt.grpcStartCmd ?? rt.startCmd;
80
+ if (rt.kind === "php" && bootCmd.startsWith("rr ")) {
92
81
  const rrBin = detectRr();
93
82
  if (rrBin && rrBin !== "rr") {
94
83
  bootCmd = `${rrBin}${bootCmd.slice(2)}`;
@@ -103,20 +92,19 @@ export async function devProject(opts) {
103
92
  continue;
104
93
  }
105
94
  const grpcPort = rt.grpcPort ?? rt.port + 1000;
106
- const probePort = useHttpFallback ? rt.port : grpcPort;
107
95
  const env = {
108
96
  PORT: String(rt.port),
109
97
  GRPC_PORT: String(grpcPort),
110
98
  HOST: "0.0.0.0",
111
- BLOK_TRANSPORT: transport,
99
+ BLOK_TRANSPORT: "grpc",
112
100
  };
113
101
  runtimeDefs.push({
114
102
  cmd,
115
103
  args,
116
- name: `${rt.label} Runtime (${transport} port ${probePort})`,
104
+ name: `${rt.label} Runtime (grpc port ${grpcPort})`,
117
105
  cwd: runtimeCwd,
118
106
  env,
119
- port: probePort,
107
+ port: grpcPort,
120
108
  });
121
109
  }
122
110
  }
@@ -153,19 +141,14 @@ export async function devProject(opts) {
153
141
  if (config?.runtimes && Object.keys(config.runtimes).length > 0) {
154
142
  console.log("\nRuntime listeners:");
155
143
  for (const [, rt] of Object.entries(config.runtimes)) {
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
- }
144
+ const grpcPort = rt.grpcPort ?? rt.port + 1000;
145
+ console.log(` ${rt.label}: gRPC 127.0.0.1:${grpcPort}`);
163
146
  }
164
147
  }
165
148
  if (healthChecks.length > 0) {
166
149
  console.log("\nWaiting for runtimes to be ready...");
167
150
  const maxWait = 120_000;
168
- const results = await Promise.all(healthChecks.map((hc) => useHttpFallback ? waitForHttpHealth(hc.port, maxWait, hc.proc) : waitForGrpcPort(hc.port, maxWait, hc.proc)));
151
+ const results = await Promise.all(healthChecks.map((hc) => waitForGrpcPort(hc.port, maxWait, hc.proc)));
169
152
  const allReady = results.every(Boolean);
170
153
  if (allReady) {
171
154
  console.log("All runtimes ready.\n");
@@ -185,7 +168,7 @@ export async function devProject(opts) {
185
168
  }
186
169
  const triggerEnv = {
187
170
  ...traceEnv,
188
- BLOK_TRANSPORT: transport,
171
+ BLOK_TRANSPORT: "grpc",
189
172
  };
190
173
  if (config?.triggers && Object.keys(config.triggers).length > 0) {
191
174
  console.log("Starting triggers...");
@@ -106,7 +106,8 @@ export class GenerationAnalytics {
106
106
  filtered = filtered.filter((e) => e.success === filter.success);
107
107
  }
108
108
  if (filter?.since) {
109
- filtered = filtered.filter((e) => e.timestamp >= filter.since);
109
+ const since = filter.since;
110
+ filtered = filtered.filter((e) => e.timestamp >= since);
110
111
  }
111
112
  return filtered;
112
113
  }
@@ -197,7 +197,7 @@ describe("NodeGenerator E2E", () => {
197
197
  });
198
198
  describe("code with markdown fences", () => {
199
199
  it("should return code as-is from LLM (cleanup happens at CLI layer)", async () => {
200
- const wrappedCode = "```typescript\n" + VALID_FUNCTION_FIRST_NODE + "\n```";
200
+ const wrappedCode = `\`\`\`typescript\n${VALID_FUNCTION_FIRST_NODE}\n\`\`\``;
201
201
  mockedGenerateText.mockResolvedValueOnce({ text: wrappedCode });
202
202
  mockValidPass();
203
203
  const result = await generator.generateNode("fetch-user", "Create a user fetcher", "test-api-key", false, "function");
@@ -252,7 +252,7 @@ describe("TriggerGenerator E2E", () => {
252
252
  });
253
253
  describe("markdown fence cleanup", () => {
254
254
  it("should strip markdown TypeScript fences from LLM response", async () => {
255
- const wrappedCode = "```typescript\n" + VALID_QUEUE_TRIGGER + "\n```";
255
+ const wrappedCode = `\`\`\`typescript\n${VALID_QUEUE_TRIGGER}\n\`\`\``;
256
256
  mockedGenerateText.mockResolvedValueOnce({ text: wrappedCode });
257
257
  mockedValidateCode.mockReturnValue({ success: true, errors: [], warnings: [] });
258
258
  const result = await generator.generateTrigger("kafka-queue", "queue", "Create a queue trigger", "test-api-key");
@@ -210,7 +210,7 @@ describe("WorkflowGenerator E2E", () => {
210
210
  });
211
211
  describe("markdown fence cleanup", () => {
212
212
  it("should strip markdown JSON fences from LLM response", async () => {
213
- const wrappedJson = "```json\n" + VALID_HTTP_WORKFLOW + "\n```";
213
+ const wrappedJson = `\`\`\`json\n${VALID_HTTP_WORKFLOW}\n\`\`\``;
214
214
  mockedGenerateText.mockResolvedValueOnce({
215
215
  text: wrappedJson,
216
216
  });