blokctl 0.6.18 → 0.6.19

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.
@@ -1,2 +1,5 @@
1
1
  import type { OptionValues } from "commander";
2
2
  export declare function createProject(opts: OptionValues, version: string, currentPath?: boolean, localRepoPath?: string): Promise<void>;
3
+ export declare function updateQueueProvider(triggerDestDir: string, provider: string, explicit: boolean): void;
4
+ export declare function getProviderDependencies(triggers: string[], pubsubProvider: string, queueProvider: string, explicitQueueProvider?: boolean): Record<string, string>;
5
+ export declare function getProviderEnvVars(triggers: string[], pubsubProvider: string, queueProvider: string, explicitQueueProvider?: boolean): string;
@@ -17,7 +17,7 @@ const exec = util.promisify(child_process.exec);
17
17
  const HOME_DIR = `${os.homedir()}/.blok`;
18
18
  const GITHUB_REPO_LOCAL = `${HOME_DIR}/blok`;
19
19
  const GITHUB_REPO_REMOTE = "https://github.com/well-prado/blok.git";
20
- const GITHUB_REPO_RELEASE_TAG = "v0.6.18";
20
+ const GITHUB_REPO_RELEASE_TAG = "v0.6.19";
21
21
  fsExtra.ensureDirSync(HOME_DIR);
22
22
  const options = {
23
23
  baseDir: HOME_DIR,
@@ -43,6 +43,7 @@ export async function createProject(opts, version, currentPath = false, localRep
43
43
  let selectedManager = opts.packageManager || "npm";
44
44
  let pubsubProvider = opts.pubsubProvider || "gcp";
45
45
  let queueProvider = opts.queueProvider || "kafka";
46
+ let explicitQueueProvider = Boolean(opts.queueProvider);
46
47
  let detectedRuntimes = [];
47
48
  if (!skipPrompts) {
48
49
  console.log(figlet.textSync("blok CLI".toUpperCase(), {
@@ -168,6 +169,7 @@ export async function createProject(opts, version, currentPath = false, localRep
168
169
  projectName = blokctlProject.projectName;
169
170
  selectedTriggers = blokctlProject.triggers;
170
171
  pubsubProvider = blokctlProject.pubsubProvider || "gcp";
172
+ explicitQueueProvider = blokctlProject.queueProvider != null;
171
173
  queueProvider = blokctlProject.queueProvider || "kafka";
172
174
  selectedRuntimeKinds = blokctlProject.runtimes;
173
175
  selectedManager = blokctlProject.selectedManager;
@@ -334,7 +336,7 @@ export async function createProject(opts, version, currentPath = false, localRep
334
336
  updatePubSubProvider(triggerDestDir, pubsubProvider);
335
337
  }
336
338
  else if (triggerKind === "queue" || triggerKind === "worker") {
337
- updateQueueProvider(triggerDestDir, queueProvider);
339
+ updateQueueProvider(triggerDestDir, queueProvider, explicitQueueProvider);
338
340
  }
339
341
  }
340
342
  }
@@ -487,7 +489,7 @@ export async function createProject(opts, version, currentPath = false, localRep
487
489
  "@blokjs/trigger-websocket": "triggers/websocket",
488
490
  "@blokjs/trigger-worker": "triggers/worker",
489
491
  };
490
- const BLOKJS_DEP_RANGE = "^0.6.18";
492
+ const BLOKJS_DEP_RANGE = "^0.6.19";
491
493
  for (const depGroup of ["dependencies", "devDependencies", "peerDependencies"]) {
492
494
  const deps = packageJsonContent[depGroup];
493
495
  if (!deps)
@@ -543,7 +545,7 @@ export async function createProject(opts, version, currentPath = false, localRep
543
545
  ...packageJsonContent.devDependencies,
544
546
  blokctl: blokctlRef,
545
547
  };
546
- const providerDeps = getProviderDependencies(selectedTriggers, pubsubProvider, queueProvider);
548
+ const providerDeps = getProviderDependencies(selectedTriggers, pubsubProvider, queueProvider, explicitQueueProvider);
547
549
  if (Object.keys(providerDeps).length > 0) {
548
550
  packageJsonContent.dependencies = {
549
551
  ...packageJsonContent.dependencies,
@@ -650,12 +652,12 @@ export async function createProject(opts, version, currentPath = false, localRep
650
652
  const triggerEnvVars = generateTriggerEnvVars(triggerConfigs);
651
653
  fsExtra.appendFileSync(envLocal, triggerEnvVars);
652
654
  }
653
- const providerEnvVars = getProviderEnvVars(selectedTriggers, pubsubProvider, queueProvider);
655
+ const providerEnvVars = getProviderEnvVars(selectedTriggers, pubsubProvider, queueProvider, explicitQueueProvider);
654
656
  if (providerEnvVars) {
655
657
  fsExtra.appendFileSync(envLocal, providerEnvVars);
656
658
  }
657
659
  if (examples) {
658
- const chatEnvBlock = [
660
+ const exampleEnvLines = [
659
661
  "",
660
662
  "# Chat demo (--examples) — get a free OpenRouter key at https://openrouter.ai/keys",
661
663
  "OPENROUTER_API_KEY=",
@@ -665,26 +667,15 @@ export async function createProject(opts, version, currentPath = false, localRep
665
667
  "# Start one locally with: docker run --rm -p 6379:6379 redis:7-alpine",
666
668
  "# The plain /chat demo works without Redis; only /chat-memory needs it.",
667
669
  "REDIS_URL=redis://127.0.0.1:6379",
668
- "",
669
- "# Webhook router demo (--examples + --triggers webhook) — secrets per provider.",
670
- "# Stripe: copy from https://dashboard.stripe.com/webhooks (`whsec_…`).",
671
- "# GitHub: set in repo Settings → Webhooks → secret field.",
672
- "# Linear: workspace settings → API → Webhooks → signing secret.",
673
- "# Until set, signature verification fails with 401that's the gate working.",
674
- "STRIPE_WEBHOOK_SECRET=",
675
- "GITHUB_WEBHOOK_SECRET=",
676
- "LINEAR_WEBHOOK_SECRET=",
677
- "",
678
- "# Worker fan-out demo (--examples + --triggers worker) — POST /fanout/jobs with",
679
- "# `{items: [...], tenantId?: '...'}` enqueues N worker jobs onto `fanout-jobs`.",
680
- "# in-memory adapter works single-process; for cross-process set BLOK_WORKER_ADAPTER",
681
- "# to nats / redis / bullmq / rabbitmq / sqs / pg-boss / kafka and supply the matching",
682
- "# connection env (e.g. NATS_SERVERS=nats://127.0.0.1:4222, or REDIS_URL above).",
683
- "BLOK_WORKER_ADAPTER=in-memory",
684
- "NATS_SERVERS=nats://127.0.0.1:4222",
685
- "",
686
- ].join("\n");
687
- fsExtra.appendFileSync(envLocal, chatEnvBlock);
670
+ ];
671
+ if (selectedTriggers.includes("webhook")) {
672
+ exampleEnvLines.push("", "# Webhook router demo (--examples + --triggers webhook) — secrets per provider.", "# Stripe: copy from https://dashboard.stripe.com/webhooks (`whsec_…`).", "# GitHub: set in repo Settings → Webhooks → secret field.", "# Linear: workspace settings → API → Webhooks → signing secret.", "# Until set, signature verification fails with 401 — that's the gate working.", "STRIPE_WEBHOOK_SECRET=", "GITHUB_WEBHOOK_SECRET=", "LINEAR_WEBHOOK_SECRET=");
673
+ }
674
+ if (selectedTriggers.includes("worker") || selectedTriggers.includes("queue")) {
675
+ exampleEnvLines.push("", "# Worker fan-out demo (--examples + --triggers worker)POST /fanout/jobs with", "# `{items: [...], tenantId?: '...'}` enqueues N worker jobs onto `fanout-jobs`.", "# in-memory adapter (BLOK_WORKER_ADAPTER above) works single-process; for", "# cross-process set it to nats / redis / bullmq / rabbitmq / sqs / pg-boss /", "# kafka and supply the matching connection env", "# (e.g. NATS_SERVERS=nats://127.0.0.1:4222, or REDIS_URL above).");
676
+ }
677
+ exampleEnvLines.push("");
678
+ fsExtra.appendFileSync(envLocal, exampleEnvLines.join("\n"));
688
679
  }
689
680
  if (examples) {
690
681
  packageJsonContent.dependencies = {
@@ -726,6 +717,7 @@ export async function createProject(opts, version, currentPath = false, localRep
726
717
  console.log(`Run the command "npm run dev" to start the development server.`);
727
718
  console.log("\nTrigger endpoints:");
728
719
  const httpPort = triggerConfigs.find((tc) => tc.kind === "http")?.port;
720
+ const brokerConsumerKinds = new Set(["worker", "queue", "pubsub"]);
729
721
  for (const tc of triggerConfigs) {
730
722
  if (mountedOnHttp.has(tc.kind) && httpPort !== undefined) {
731
723
  const samplePath = tc.kind === "sse"
@@ -737,6 +729,10 @@ export async function createProject(opts, version, currentPath = false, localRep
737
729
  : "/ws/echo";
738
730
  console.log(` ${tc.label}: http://localhost:${httpPort}${samplePath} (mounted on HTTP)`);
739
731
  }
732
+ else if (brokerConsumerKinds.has(tc.kind)) {
733
+ const provider = tc.kind === "pubsub" ? pubsubProvider : explicitQueueProvider ? queueProvider : "in-memory";
734
+ console.log(` ${tc.label}: consumes via ${provider} (no HTTP endpoint)`);
735
+ }
740
736
  else {
741
737
  console.log(` ${tc.label}: http://localhost:${tc.port}/health-check`);
742
738
  }
@@ -747,7 +743,8 @@ export async function createProject(opts, version, currentPath = false, localRep
747
743
  console.log(` ${rc.label}: http://localhost:${rc.port}/health`);
748
744
  }
749
745
  }
750
- if ((selectedTriggers.includes("queue") || selectedTriggers.includes("worker")) && queueProvider === "redis") {
746
+ const workerInfraSelected = explicitQueueProvider && (selectedTriggers.includes("queue") || selectedTriggers.includes("worker"));
747
+ if (workerInfraSelected && queueProvider === "redis") {
751
748
  console.log(color.cyan("\n📦 Redis Setup (for Queue trigger):"));
752
749
  console.log(" Start Redis with Docker:");
753
750
  console.log(color.dim(" cd infra/development"));
@@ -755,7 +752,7 @@ export async function createProject(opts, version, currentPath = false, localRep
755
752
  console.log(color.dim(" docker compose up -d redis redis-commander"));
756
753
  console.log(" Redis Commander UI: http://localhost:8081");
757
754
  }
758
- if ((selectedTriggers.includes("queue") || selectedTriggers.includes("worker")) && queueProvider === "nats") {
755
+ if (workerInfraSelected && queueProvider === "nats") {
759
756
  console.log(color.cyan("\n📦 NATS JetStream Setup (for Queue trigger):"));
760
757
  console.log(" Start NATS with Docker:");
761
758
  console.log(color.dim(" cd infra/development"));
@@ -1481,11 +1478,13 @@ function updatePubSubProvider(triggerDestDir, provider) {
1481
1478
  content = content.replace(/(export default class \w+ extends PubSubTrigger \{[\s\S]*?)\n\tprotected adapter = new \w+\(\{[\s\S]*?\}\);/, `$1\n\tprotected adapter = ${config.init};`);
1482
1479
  fsExtra.writeFileSync(serverPath, content);
1483
1480
  }
1484
- function updateQueueProvider(triggerDestDir, provider) {
1481
+ export function updateQueueProvider(triggerDestDir, provider, explicit) {
1485
1482
  const serverPath = `${triggerDestDir}/runner/WorkerServer.ts`;
1486
1483
  if (!fsExtra.existsSync(serverPath))
1487
1484
  return;
1488
1485
  let content = fsExtra.readFileSync(serverPath, "utf8");
1486
+ if (!explicit)
1487
+ return;
1489
1488
  const adapterConfigs = {
1490
1489
  kafka: {
1491
1490
  importName: "KafkaAdapter",
@@ -1525,17 +1524,11 @@ function updateQueueProvider(triggerDestDir, provider) {
1525
1524
  const config = adapterConfigs[provider];
1526
1525
  if (!config)
1527
1526
  return;
1528
- content = content.replace(/import \{ (\w+), (\w+) \} from ["']@blokjs\/trigger-worker["'];/, `import { ${config.importName}, WorkerTrigger } from "@blokjs/trigger-worker";`);
1529
- content = content.replace(/(export default class \w+ extends WorkerTrigger \{[\s\S]*?)\n\tprotected adapter = new \w+\(\{[\s\S]*?\}\);/, `$1\n\tprotected adapter = ${config.init};`);
1527
+ content = content.replace(/import \{ WorkerTrigger \} from ["']@blokjs\/trigger-worker["'];/, `import { ${config.importName}, WorkerTrigger } from "@blokjs/trigger-worker";`);
1528
+ content = content.replace(/(export default class \w+ extends WorkerTrigger \{)/, `$1\n\tprotected adapter = ${config.init};\n`);
1530
1529
  fsExtra.writeFileSync(serverPath, content);
1531
- const workflowPath = `${triggerDestDir}/workflows/messages/on-message.ts`;
1532
- if (fsExtra.existsSync(workflowPath)) {
1533
- let workflowContent = fsExtra.readFileSync(workflowPath, "utf8");
1534
- workflowContent = workflowContent.replace(/provider: "kafka"/, `provider: "${provider}"`);
1535
- fsExtra.writeFileSync(workflowPath, workflowContent);
1536
- }
1537
1530
  }
1538
- function getProviderDependencies(triggers, pubsubProvider, queueProvider) {
1531
+ export function getProviderDependencies(triggers, pubsubProvider, queueProvider, explicitQueueProvider = false) {
1539
1532
  const deps = {};
1540
1533
  const pubsubProviderDeps = {
1541
1534
  gcp: { "@google-cloud/pubsub": "^5.0.0" },
@@ -1552,12 +1545,14 @@ function getProviderDependencies(triggers, pubsubProvider, queueProvider) {
1552
1545
  if (triggers.includes("pubsub") && pubsubProviderDeps[pubsubProvider]) {
1553
1546
  Object.assign(deps, pubsubProviderDeps[pubsubProvider]);
1554
1547
  }
1555
- if ((triggers.includes("queue") || triggers.includes("worker")) && queueProviderDeps[queueProvider]) {
1548
+ if (explicitQueueProvider &&
1549
+ (triggers.includes("queue") || triggers.includes("worker")) &&
1550
+ queueProviderDeps[queueProvider]) {
1556
1551
  Object.assign(deps, queueProviderDeps[queueProvider]);
1557
1552
  }
1558
1553
  return deps;
1559
1554
  }
1560
- function getProviderEnvVars(triggers, pubsubProvider, queueProvider) {
1555
+ export function getProviderEnvVars(triggers, pubsubProvider, queueProvider, explicitQueueProvider = false) {
1561
1556
  const lines = [];
1562
1557
  const pubsubEnvVars = {
1563
1558
  gcp: `
@@ -1599,8 +1594,15 @@ NATS_STREAM_NAME=blok-queue`,
1599
1594
  if (triggers.includes("pubsub") && pubsubEnvVars[pubsubProvider]) {
1600
1595
  lines.push(pubsubEnvVars[pubsubProvider]);
1601
1596
  }
1602
- if ((triggers.includes("queue") || triggers.includes("worker")) && queueEnvVars[queueProvider]) {
1603
- lines.push(queueEnvVars[queueProvider]);
1597
+ const hasWorkerTrigger = triggers.includes("queue") || triggers.includes("worker");
1598
+ if (hasWorkerTrigger) {
1599
+ if (explicitQueueProvider && queueEnvVars[queueProvider]) {
1600
+ lines.push(queueEnvVars[queueProvider]);
1601
+ }
1602
+ else {
1603
+ lines.push("\n# Worker adapter — dev default. Set a provider + the matching broker env" +
1604
+ "\n# (KAFKA_*, NATS_SERVERS, REDIS_*, etc.) for production.\nBLOK_WORKER_ADAPTER=in-memory");
1605
+ }
1604
1606
  }
1605
1607
  return lines.join("\n");
1606
1608
  }
@@ -35,7 +35,7 @@ declare const php_dockerfile = "FROM php:8.2-cli-alpine AS builder\nWORKDIR /app
35
35
  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";
36
36
  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";
37
37
  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";
38
- 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### Workflow Naming\n\nEvery workflow's \\`name\\` must be UNIQUE across the project. The\n\\`WorkflowRegistry\\` rejects duplicate names at boot, so a collision\nmeans only one of the colliding workflows ever registers.\n\nPrefer a dotted \\`domain.action\\` convention \u2014 \\`countries.list\\`,\n\\`users.create\\`, \\`orders.refund\\`. The typed client\n(\\`@blokjs/client\\`) and \\`blokctl gen app-types\\` nest workflows by\ntheir dotted name, so a clean name surfaces as\n\\`blok.countries.list(...)\\` instead of a quoted\n\\`blok[\"World Countries\"]\\` accessor. Duplicate names also make\n\\`gen app-types\\` report a collision and DROP one workflow from the\ngenerated \\`BlokApp\\` type.\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";
39
- 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## Workflow Naming\n\nWorkflow \\`name\\` must be UNIQUE across the project \u2014 the\n\\`WorkflowRegistry\\` rejects duplicates at boot. Use a dotted\n\\`domain.action\\` convention (\\`countries.list\\`, \\`users.create\\`)\nso the typed client (\\`@blokjs/client\\`) and \\`blokctl gen app-types\\`\nexpose clean nested accessors like \\`blok.countries.list(...)\\`. Duplicate\nnames make \\`gen app-types\\` flag a collision and drop one workflow from\nthe generated \\`BlokApp\\` type.\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
+ 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### Workflow Naming\n\nEvery workflow's \\`name\\` must be UNIQUE across the project. The\n\\`WorkflowRegistry\\` rejects duplicate names at boot, so a collision\nmeans only one of the colliding workflows ever registers.\n\nPrefer a dotted \\`domain.action\\` convention for the workflow\n\\`name\\` \u2014 \\`countries.list\\`, \\`users.create\\`,\n\\`orders.refund\\`. The typed client (\\`@blokjs/client\\`) and\n\\`blokctl gen app-types\\` nest workflows by their dotted name, so a clean\nname surfaces as \\`blok.countries.list(...)\\` instead of a quoted\n\\`blok[\"World Countries\"]\\` accessor. Duplicate names also make\n\\`gen app-types\\` report a collision and DROP one workflow from the\ngenerated \\`BlokApp\\` type.\n\nThe dotted convention applies to the workflow \\`name\\` only. Keep the\n\\`Workflows.ts\\` map KEYS dot-free (e.g. \\`\"refund-order\"\\`, not\n\\`\"orders.refund\"\\`) \u2014 the worker resolver treats the first dot in a map\nkey as a file-extension delimiter, so a dotted key fails to resolve at load.\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 \"steps\": [\n {\n \"id\": \"filter-request\",\n \"branch\": {\n \"when\": \"ctx.request.query.active === \\\\\"true\\\\\"\",\n \"then\": [{ \"id\": \"active-path\", \"use\": \"handle-active\", \"type\": \"module\" }],\n \"else\": [{ \"id\": \"default-path\", \"use\": \"handle-default\", \"type\": \"module\" }]\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| \\`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\nimport { workflow, $ } from \"@blokjs/helper\";\n\nexport default workflow({\n name: \"Process Job\",\n version: \"1.0.0\",\n trigger: { worker: { queue: \"background-jobs\", concurrency: 5, retries: 3 } },\n steps: [\n {\n id: \"process\",\n use: \"my-processor\",\n inputs: { payload: $.req.body, jobId: $.req.params.jobId },\n },\n ],\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\nWorker providers: \\`in-memory\\` (dev default, zero infra), \\`nats\\`, \\`bullmq\\`, \\`rabbitmq\\`, \\`sqs\\`, \\`kafka\\`, \\`redis\\`, \\`pg-boss\\`. Resolved per-workflow via \\`trigger.worker.provider\\`, then \\`BLOK_WORKER_ADAPTER\\`, then \\`in-memory\\`.\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";
39
+ 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## Workflow Naming\n\nWorkflow \\`name\\` must be UNIQUE across the project \u2014 the\n\\`WorkflowRegistry\\` rejects duplicates at boot. Use a dotted\n\\`domain.action\\` convention (\\`countries.list\\`, \\`users.create\\`)\nso the typed client (\\`@blokjs/client\\`) and \\`blokctl gen app-types\\`\nexpose clean nested accessors like \\`blok.countries.list(...)\\`. Duplicate\nnames make \\`gen app-types\\` flag a collision and drop one workflow from\nthe generated \\`BlokApp\\` type.\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\nimport { workflow, $ } from \"@blokjs/helper\";\n\nexport default workflow({\n name: \"Process Job\",\n version: \"1.0.0\",\n trigger: { worker: { queue: \"background-jobs\" } },\n steps: [\n { id: \"process\", use: \"my-processor\",\n inputs: { payload: $.req.body, jobId: $.req.params.jobId } },\n ],\n});\n\\`\\`\\`\n\nJob data: \\`ctx.request.body\\` = payload, \\`ctx.request.params.queue/jobId/attempt\\` = metadata.\nProviders: \\`in-memory\\` (dev default), \\`nats\\`, \\`bullmq\\`, \\`rabbitmq\\`, \\`sqs\\`, \\`kafka\\`, \\`redis\\`, \\`pg-boss\\` \u2014 set via \\`trigger.worker.provider\\` or \\`BLOK_WORKER_ADAPTER\\`.\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";
40
40
  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";
41
41
  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, };
@@ -768,15 +768,20 @@ Every workflow's \\\`name\\\` must be UNIQUE across the project. The
768
768
  \\\`WorkflowRegistry\\\` rejects duplicate names at boot, so a collision
769
769
  means only one of the colliding workflows ever registers.
770
770
 
771
- Prefer a dotted \\\`domain.action\\\` convention \\\`countries.list\\\`,
772
- \\\`users.create\\\`, \\\`orders.refund\\\`. The typed client
773
- (\\\`@blokjs/client\\\`) and \\\`blokctl gen app-types\\\` nest workflows by
774
- their dotted name, so a clean name surfaces as
775
- \\\`blok.countries.list(...)\\\` instead of a quoted
771
+ Prefer a dotted \\\`domain.action\\\` convention for the workflow
772
+ \\\`name\\\` — \\\`countries.list\\\`, \\\`users.create\\\`,
773
+ \\\`orders.refund\\\`. The typed client (\\\`@blokjs/client\\\`) and
774
+ \\\`blokctl gen app-types\\\` nest workflows by their dotted name, so a clean
775
+ name surfaces as \\\`blok.countries.list(...)\\\` instead of a quoted
776
776
  \\\`blok["World Countries"]\\\` accessor. Duplicate names also make
777
777
  \\\`gen app-types\\\` report a collision and DROP one workflow from the
778
778
  generated \\\`BlokApp\\\` type.
779
779
 
780
+ The dotted convention applies to the workflow \\\`name\\\` only. Keep the
781
+ \\\`Workflows.ts\\\` map KEYS dot-free (e.g. \\\`"refund-order"\\\`, not
782
+ \\\`"orders.refund"\\\`) — the worker resolver treats the first dot in a map
783
+ key as a file-extension delimiter, so a dotted key fails to resolve at load.
784
+
780
785
  ### Step Types
781
786
 
782
787
  | Type | Description |
@@ -795,21 +800,16 @@ generated \\\`BlokApp\\\` type.
795
800
 
796
801
  \`\`\`json
797
802
  {
798
- "nodes": {
799
- "filter": {
800
- "conditions": [
801
- {
802
- "type": "if",
803
- "condition": "ctx.request.query.active === \\\\"true\\\\"",
804
- "steps": [{ "name": "active-path", "node": "handle-active", "type": "module" }]
805
- },
806
- {
807
- "type": "else",
808
- "steps": [{ "name": "default-path", "node": "handle-default", "type": "module" }]
809
- }
810
- ]
803
+ "steps": [
804
+ {
805
+ "id": "filter-request",
806
+ "branch": {
807
+ "when": "ctx.request.query.active === \\\\"true\\\\"",
808
+ "then": [{ "id": "active-path", "use": "handle-active", "type": "module" }],
809
+ "else": [{ "id": "default-path", "use": "handle-default", "type": "module" }]
810
+ }
811
811
  }
812
- }
812
+ ]
813
813
  }
814
814
  \`\`\`
815
815
 
@@ -822,7 +822,6 @@ generated \\\`BlokApp\\\` type.
822
822
  | \\\`http\\\` | \\\`{ "method": "GET", "path": "/", "accept": "application/json" }\\\` |
823
823
  | \\\`grpc\\\` | \\\`{ "service": "UserService", "method": "GetUser" }\\\` |
824
824
  | \\\`cron\\\` | \\\`{ "schedule": "0 * * * *", "timezone": "UTC" }\\\` |
825
- | \\\`queue\\\` | \\\`{ "provider": "kafka", "topic": "events" }\\\` |
826
825
  | \\\`pubsub\\\` | \\\`{ "provider": "gcp", "topic": "updates" }\\\` |
827
826
  | \\\`webhook\\\` | \\\`{ "source": "github", "events": ["push"] }\\\` |
828
827
  | \\\`websocket\\\` | \\\`{ "events": ["message"], "path": "/ws" }\\\` |
@@ -834,19 +833,25 @@ generated \\\`BlokApp\\\` type.
834
833
  The worker trigger processes background jobs from a queue with retry logic and concurrency control.
835
834
 
836
835
  \\\`\\\`\\\`typescript
837
- Workflow({ name: "Process Job", version: "1.0.0" })
838
- .addTrigger("worker", { queue: "background-jobs" })
839
- .addStep({
840
- name: "process",
841
- node: "my-processor",
842
- type: "module",
843
- inputs: { payload: "js/ctx.request.body", jobId: "js/ctx.request.params.jobId" },
844
- });
836
+ import { workflow, $ } from "@blokjs/helper";
837
+
838
+ export default workflow({
839
+ name: "Process Job",
840
+ version: "1.0.0",
841
+ trigger: { worker: { queue: "background-jobs", concurrency: 5, retries: 3 } },
842
+ steps: [
843
+ {
844
+ id: "process",
845
+ use: "my-processor",
846
+ inputs: { payload: $.req.body, jobId: $.req.params.jobId },
847
+ },
848
+ ],
849
+ });
845
850
  \\\`\\\`\\\`
846
851
 
847
852
  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.
848
853
 
849
- Adapters: NATS JetStream (recommended), BullMQ (Redis), InMemory (dev only).
854
+ Worker providers: \\\`in-memory\\\` (dev default, zero infra), \\\`nats\\\`, \\\`bullmq\\\`, \\\`rabbitmq\\\`, \\\`sqs\\\`, \\\`kafka\\\`, \\\`redis\\\`, \\\`pg-boss\\\`. Resolved per-workflow via \\\`trigger.worker.provider\\\`, then \\\`BLOK_WORKER_ADAPTER\\\`, then \\\`in-memory\\\`.
850
855
 
851
856
  ### NATS JetStream
852
857
 
@@ -1018,14 +1023,21 @@ export default defineNode({
1018
1023
  Worker trigger processes background jobs from a queue:
1019
1024
 
1020
1025
  \\\`\\\`\\\`typescript
1021
- Workflow({ name: "Process Job", version: "1.0.0" })
1022
- .addTrigger("worker", { queue: "background-jobs" })
1023
- .addStep({ name: "process", node: "my-processor", type: "module",
1024
- inputs: { payload: "js/ctx.request.body", jobId: "js/ctx.request.params.jobId" } });
1026
+ import { workflow, $ } from "@blokjs/helper";
1027
+
1028
+ export default workflow({
1029
+ name: "Process Job",
1030
+ version: "1.0.0",
1031
+ trigger: { worker: { queue: "background-jobs" } },
1032
+ steps: [
1033
+ { id: "process", use: "my-processor",
1034
+ inputs: { payload: $.req.body, jobId: $.req.params.jobId } },
1035
+ ],
1036
+ });
1025
1037
  \\\`\\\`\\\`
1026
1038
 
1027
1039
  Job data: \\\`ctx.request.body\\\` = payload, \\\`ctx.request.params.queue/jobId/attempt\\\` = metadata.
1028
- Adapters: NATS JetStream (recommended), BullMQ (Redis), InMemory (dev).
1040
+ Providers: \\\`in-memory\\\` (dev default), \\\`nats\\\`, \\\`bullmq\\\`, \\\`rabbitmq\\\`, \\\`sqs\\\`, \\\`kafka\\\`, \\\`redis\\\`, \\\`pg-boss\\\` — set via \\\`trigger.worker.provider\\\` or \\\`BLOK_WORKER_ADAPTER\\\`.
1029
1041
 
1030
1042
  ## Testing
1031
1043
 
@@ -133,9 +133,15 @@ export async function devProject(opts) {
133
133
  }
134
134
  }
135
135
  if (config?.triggers && Object.keys(config.triggers).length > 0) {
136
+ const brokerConsumerKinds = new Set(["worker", "queue", "pubsub"]);
136
137
  console.log("\nTrigger endpoints:");
137
138
  for (const [, trigger] of Object.entries(config.triggers)) {
138
- console.log(` ${trigger.label}: http://localhost:${trigger.port}/health-check`);
139
+ if (brokerConsumerKinds.has(trigger.kind)) {
140
+ console.log(` ${trigger.label}: consumes from broker (no HTTP endpoint)`);
141
+ }
142
+ else {
143
+ console.log(` ${trigger.label}: http://localhost:${trigger.port}/health-check`);
144
+ }
139
145
  }
140
146
  }
141
147
  if (config?.runtimes && Object.keys(config.runtimes).length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blokctl",
3
- "version": "0.6.18",
3
+ "version": "0.6.19",
4
4
  "author": "Deskree Technologies Inc.",
5
5
  "license": "Apache-2.0",
6
6
  "description": "cli for blok",
@@ -30,7 +30,7 @@
30
30
  "keywords": ["blokctl", "cli", "blok", "blok"],
31
31
  "dependencies": {
32
32
  "@ai-sdk/openai": "^1.3.22",
33
- "@blokjs/runner": "^0.6.18",
33
+ "@blokjs/runner": "^0.6.19",
34
34
  "@clack/prompts": "^1.0.0",
35
35
  "ai": "^4.3.16",
36
36
  "better-sqlite3": "^12.6.2",