blokctl 0.6.9 → 0.6.11

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.
@@ -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.9";
20
+ const GITHUB_REPO_RELEASE_TAG = "v0.6.11";
21
21
  fsExtra.ensureDirSync(HOME_DIR);
22
22
  const options = {
23
23
  baseDir: HOME_DIR,
@@ -264,15 +264,15 @@ export async function createProject(opts, version, currentPath = false, localRep
264
264
  const triggerConfigs = selectedTriggers.map((kind) => createTriggerConfig(kind));
265
265
  const mountedOnHttp = new Set();
266
266
  if (selectedTriggers.includes("http")) {
267
- for (const kind of ["sse", "websocket"]) {
267
+ for (const kind of ["sse", "websocket", "webhook"]) {
268
268
  if (selectedTriggers.includes(kind))
269
269
  mountedOnHttp.add(kind);
270
270
  }
271
271
  }
272
272
  const spawnedTriggerConfigs = triggerConfigs.filter((tc) => !mountedOnHttp.has(tc.kind));
273
273
  const primaryTrigger = selectedTriggers[0];
274
- const primaryTriggerDir = primaryTrigger === "pubsub" || primaryTrigger === "queue"
275
- ? `${repoSource}/triggers/${primaryTrigger === "queue" ? "worker" : primaryTrigger}/template`
274
+ const primaryTriggerDir = primaryTrigger === "pubsub" || primaryTrigger === "queue" || primaryTrigger === "worker"
275
+ ? `${repoSource}/triggers/${primaryTrigger === "queue" || primaryTrigger === "worker" ? "worker" : primaryTrigger}/template`
276
276
  : `${repoSource}/triggers/${primaryTrigger}`;
277
277
  const baseFiles = ["package.json", "tsconfig.json", ".env.example", ".gitignore", "vitest.config.ts"];
278
278
  for (const file of baseFiles) {
@@ -318,8 +318,8 @@ export async function createProject(opts, version, currentPath = false, localRep
318
318
  for (const triggerKind of selectedTriggers) {
319
319
  const triggerDestDir = `${dirPath}/src/triggers/${triggerKind}`;
320
320
  fsExtra.ensureDirSync(triggerDestDir);
321
- if (triggerKind === "pubsub" || triggerKind === "queue") {
322
- const templatePkgDir = triggerKind === "queue" ? "worker" : triggerKind;
321
+ if (triggerKind === "pubsub" || triggerKind === "queue" || triggerKind === "worker") {
322
+ const templatePkgDir = triggerKind === "queue" || triggerKind === "worker" ? "worker" : triggerKind;
323
323
  const templateDir = `${repoSource}/triggers/${templatePkgDir}/template/src`;
324
324
  if (fsExtra.existsSync(templateDir)) {
325
325
  fsExtra.copySync(templateDir, triggerDestDir);
@@ -332,7 +332,7 @@ export async function createProject(opts, version, currentPath = false, localRep
332
332
  if (triggerKind === "pubsub") {
333
333
  updatePubSubProvider(triggerDestDir, pubsubProvider);
334
334
  }
335
- else if (triggerKind === "queue") {
335
+ else if (triggerKind === "queue" || triggerKind === "worker") {
336
336
  updateQueueProvider(triggerDestDir, queueProvider);
337
337
  }
338
338
  }
@@ -383,7 +383,14 @@ export async function createProject(opts, version, currentPath = false, localRep
383
383
  fsExtra.writeFileSync(`${dirPath}/src/Nodes.ts`, sharedNodesContent);
384
384
  const sharedWorkflowsContent = generateSharedWorkflowsFile(selectedTriggers);
385
385
  fsExtra.writeFileSync(`${dirPath}/src/Workflows.ts`, sharedWorkflowsContent);
386
+ const triggersWithRealTemplate = new Set(["worker", "queue", "pubsub"]);
386
387
  for (const triggerKind of selectedTriggers) {
388
+ if (triggersWithRealTemplate.has(triggerKind)) {
389
+ continue;
390
+ }
391
+ if (mountedOnHttp.has(triggerKind)) {
392
+ continue;
393
+ }
387
394
  const entryContent = generateTriggerEntryFile(triggerKind, selectedTriggers);
388
395
  fsExtra.writeFileSync(`${dirPath}/src/triggers/${triggerKind}/index.ts`, entryContent);
389
396
  }
@@ -426,7 +433,7 @@ export async function createProject(opts, version, currentPath = false, localRep
426
433
  fsExtra.ensureDirSync(`${dirPath}/infra/metrics`);
427
434
  fsExtra.copySync(`${repoSource}/infra/metrics`, `${dirPath}/infra/metrics`);
428
435
  fsExtra.removeSync(`${dirPath}/public/metric`);
429
- if (selectedTriggers.includes("queue")) {
436
+ if (selectedTriggers.includes("queue") || selectedTriggers.includes("worker")) {
430
437
  fsExtra.ensureDirSync(`${dirPath}/infra/development`);
431
438
  fsExtra.copySync(`${repoSource}/infra/development`, `${dirPath}/infra/development`);
432
439
  }
@@ -478,7 +485,7 @@ export async function createProject(opts, version, currentPath = false, localRep
478
485
  "@blokjs/trigger-websocket": "triggers/websocket",
479
486
  "@blokjs/trigger-worker": "triggers/worker",
480
487
  };
481
- const BLOKJS_DEP_RANGE = "^0.6.9";
488
+ const BLOKJS_DEP_RANGE = "^0.6.11";
482
489
  for (const depGroup of ["dependencies", "devDependencies", "peerDependencies"]) {
483
490
  const deps = packageJsonContent[depGroup];
484
491
  if (!deps)
@@ -547,7 +554,8 @@ export async function createProject(opts, version, currentPath = false, localRep
547
554
  ? `file:${path.resolve(repoSource, "triggers/pubsub")}`
548
555
  : BLOKJS_DEP_RANGE;
549
556
  }
550
- if (selectedTriggers.includes("queue")) {
557
+ const needsTriggerWorker = selectedTriggers.includes("queue") || selectedTriggers.includes("worker") || examples;
558
+ if (needsTriggerWorker) {
551
559
  triggerPackageDeps["@blokjs/trigger-worker"] = localRepoPath
552
560
  ? `file:${path.resolve(repoSource, "triggers/worker")}`
553
561
  : BLOKJS_DEP_RANGE;
@@ -646,6 +654,23 @@ export async function createProject(opts, version, currentPath = false, localRep
646
654
  "# The plain /chat demo works without Redis; only /chat-memory needs it.",
647
655
  "REDIS_URL=redis://127.0.0.1:6379",
648
656
  "",
657
+ "# Webhook router demo (--examples + --triggers webhook) — secrets per provider.",
658
+ "# Stripe: copy from https://dashboard.stripe.com/webhooks (`whsec_…`).",
659
+ "# GitHub: set in repo Settings → Webhooks → secret field.",
660
+ "# Linear: workspace settings → API → Webhooks → signing secret.",
661
+ "# Until set, signature verification fails with 401 — that's the gate working.",
662
+ "STRIPE_WEBHOOK_SECRET=",
663
+ "GITHUB_WEBHOOK_SECRET=",
664
+ "LINEAR_WEBHOOK_SECRET=",
665
+ "",
666
+ "# Worker fan-out demo (--examples + --triggers worker) — POST /fanout/jobs with",
667
+ "# `{items: [...], tenantId?: '...'}` enqueues N worker jobs onto `fanout-jobs`.",
668
+ "# in-memory adapter works single-process; for cross-process set BLOK_WORKER_ADAPTER",
669
+ "# to nats / redis / bullmq / rabbitmq / sqs / pg-boss / kafka and supply the matching",
670
+ "# connection env (e.g. NATS_SERVERS=nats://127.0.0.1:4222, or REDIS_URL above).",
671
+ "BLOK_WORKER_ADAPTER=in-memory",
672
+ "NATS_SERVERS=nats://127.0.0.1:4222",
673
+ "",
649
674
  ].join("\n");
650
675
  fsExtra.appendFileSync(envLocal, chatEnvBlock);
651
676
  }
@@ -704,7 +729,7 @@ export async function createProject(opts, version, currentPath = false, localRep
704
729
  console.log(` ${rc.label}: http://localhost:${rc.port}/health`);
705
730
  }
706
731
  }
707
- if (selectedTriggers.includes("queue") && queueProvider === "redis") {
732
+ if ((selectedTriggers.includes("queue") || selectedTriggers.includes("worker")) && queueProvider === "redis") {
708
733
  console.log(color.cyan("\n📦 Redis Setup (for Queue trigger):"));
709
734
  console.log(" Start Redis with Docker:");
710
735
  console.log(color.dim(" cd infra/development"));
@@ -712,7 +737,7 @@ export async function createProject(opts, version, currentPath = false, localRep
712
737
  console.log(color.dim(" docker compose up -d redis redis-commander"));
713
738
  console.log(" Redis Commander UI: http://localhost:8081");
714
739
  }
715
- if (selectedTriggers.includes("queue") && queueProvider === "nats") {
740
+ if ((selectedTriggers.includes("queue") || selectedTriggers.includes("worker")) && queueProvider === "nats") {
716
741
  console.log(color.cyan("\n📦 NATS JetStream Setup (for Queue trigger):"));
717
742
  console.log(" Start NATS with Docker:");
718
743
  console.log(color.dim(" cd infra/development"));
@@ -782,8 +807,8 @@ function generateSharedWorkflowsFile(triggers) {
782
807
  imports.push('import OnPubSubMessage from "./workflows/pubsub/messages/on-message";');
783
808
  workflowEntries.push('\t"on-pubsub-message": OnPubSubMessage,');
784
809
  }
785
- else if (trigger === "queue") {
786
- imports.push('import ProcessJob from "./workflows/queue/jobs/process-job";');
810
+ else if (trigger === "queue" || trigger === "worker") {
811
+ imports.push(`import ProcessJob from "./workflows/${trigger}/jobs/process-job";`);
787
812
  workflowEntries.push('\t"process-job": ProcessJob,');
788
813
  }
789
814
  }
@@ -803,12 +828,14 @@ function generateTriggerEntryFile(triggerKind, selectedTriggers = [triggerKind])
803
828
  if (triggerKind === "http") {
804
829
  const sseAlsoSelected = selectedTriggers.includes("sse");
805
830
  const wsAlsoSelected = selectedTriggers.includes("websocket");
806
- const needsShared = sseAlsoSelected || wsAlsoSelected;
831
+ const webhookAlsoSelected = selectedTriggers.includes("webhook");
832
+ const needsShared = sseAlsoSelected || wsAlsoSelected || webhookAlsoSelected;
807
833
  const sharedHelperImports = needsShared
808
834
  ? `\nimport { NodeMap, WorkflowRegistry } from "@blokjs/runner";\nimport sharedNodes from "../../Nodes";\nimport sharedWorkflows from "../../Workflows";`
809
835
  : "";
810
836
  const sseImports = sseAlsoSelected ? `\nimport SSETrigger from "@blokjs/trigger-sse";` : "";
811
837
  const wsImports = wsAlsoSelected ? `\nimport WebSocketTrigger from "@blokjs/trigger-websocket";` : "";
838
+ const webhookImports = webhookAlsoSelected ? `\nimport WebhookTrigger from "@blokjs/trigger-webhook";` : "";
812
839
  const sharedBootstrapPrelude = needsShared
813
840
  ? `\n\n // Build a NodeMap from the shared Nodes record; both SSE and
814
841
  // WebSocket triggers consume this via setNodeMap so they can
@@ -831,15 +858,15 @@ function generateTriggerEntryFile(triggerKind, selectedTriggers = [triggerKind])
831
858
  for (const [name, wf] of Object.entries(sharedWorkflows)) {
832
859
  const w = wf as {
833
860
  name?: string;
834
- trigger?: { sse?: unknown; websocket?: unknown };
835
- _config?: { name?: string; trigger?: { sse?: unknown; websocket?: unknown } };
861
+ trigger?: { sse?: unknown; websocket?: unknown; webhook?: unknown };
862
+ _config?: { name?: string; trigger?: { sse?: unknown; websocket?: unknown; webhook?: unknown } };
836
863
  };
837
864
  const triggerCfg = w._config?.trigger ?? w.trigger;
838
865
  if (!triggerCfg) continue;
839
- if (!triggerCfg.sse && !triggerCfg.websocket) continue;
866
+ if (!triggerCfg.sse && !triggerCfg.websocket && !triggerCfg.webhook) continue;
840
867
  const resolvedName = w._config?.name ?? w.name ?? name;
841
868
  if (registry.get(resolvedName)) continue;
842
- const kind = triggerCfg.sse ? "sse" : "websocket";
869
+ const kind = triggerCfg.sse ? "sse" : triggerCfg.websocket ? "websocket" : "webhook";
843
870
  registry.register({
844
871
  name: resolvedName,
845
872
  source: \`\${kind}:\${name}\`,
@@ -876,10 +903,26 @@ function generateTriggerEntryFile(triggerKind, selectedTriggers = [triggerKind])
876
903
  });
877
904
  await wsTrigger.listen();`
878
905
  : "";
879
- const fullBootstrap = `${sharedBootstrapPrelude}${sseBootstrap}${wsBootstrap}`;
906
+ const webhookBootstrap = webhookAlsoSelected
907
+ ? `\n // Mount Webhook trigger on the HTTP process's shared Hono app.
908
+ // WebhookTrigger.constructor(app, httpTrigger?) mirrors SSE / WS —
909
+ // when an HttpTrigger is supplied, the webhook trigger registers
910
+ // its /webhooks/<provider> routes inside addPreCatchAllHook so they
911
+ // win Hono's first-match dispatch over the legacy workflow catch-
912
+ // all. The shared @blokjs/trigger-webhook singleton this creates
913
+ // is also what @blokjs/hmac-verify (and other webhook-aware helpers)
914
+ // look up at run time.
915
+ const webhookTrigger = new WebhookTrigger(this.httpTrigger.getApp(), this.httpTrigger);
916
+ webhookTrigger.setNodeMap({
917
+ nodes: subTriggerNodeMap,
918
+ workflows: sharedWorkflows as unknown as Parameters<typeof webhookTrigger.setNodeMap>[0]["workflows"],
919
+ });
920
+ await webhookTrigger.listen();`
921
+ : "";
922
+ const fullBootstrap = `${sharedBootstrapPrelude}${sseBootstrap}${wsBootstrap}${webhookBootstrap}`;
880
923
  return `import { DefaultLogger } from "@blokjs/runner";
881
924
  import { type Span, metrics, trace } from "@opentelemetry/api";
882
- import HttpTrigger from "./runner/HttpTrigger";${sharedHelperImports}${sseImports}${wsImports}
925
+ import HttpTrigger from "./runner/HttpTrigger";${sharedHelperImports}${sseImports}${wsImports}${webhookImports}
883
926
 
884
927
  export default class App {
885
928
  private httpTrigger: HttpTrigger = <HttpTrigger>{};
@@ -1353,7 +1396,7 @@ function fixRunnerImportPaths(triggerDestDir, triggerKind) {
1353
1396
  else if (triggerKind === "pubsub") {
1354
1397
  fileFixes.push({ file: `${triggerDestDir}/runner/PubSubServer.ts`, up: "../../../" });
1355
1398
  }
1356
- else if (triggerKind === "queue") {
1399
+ else if (triggerKind === "queue" || triggerKind === "worker") {
1357
1400
  fileFixes.push({ file: `${triggerDestDir}/runner/WorkerServer.ts`, up: "../../../" });
1358
1401
  }
1359
1402
  for (const { file, up } of fileFixes) {
@@ -13,7 +13,7 @@ declare const package_dev_dependencies: {
13
13
  "@types/pg": string;
14
14
  };
15
15
  declare const python3_file = "\nfrom core.blok import BlokService\nfrom core.types.context import Context\nfrom core.types.blok_response import BlokResponse\nfrom core.types.global_error import GlobalError\nfrom typing import Any, Dict\nimport traceback\n\nclass Node(BlokService):\n def __init__(self):\n BlokService.__init__(self)\n self.input_schema = {}\n self.output_schema = {}\n\n async def handle(self, ctx: Context, inputs: Dict[str, Any]) -> BlokResponse:\n response = BlokResponse()\n\n try:\n response.setSuccess({ \"message\": \"Hello World from Python3!\" })\n except Exception as error:\n err = GlobalError(error)\n err.setCode(500)\n err.setName(self.name)\n\n stack_trace = traceback.format_exc()\n err.setStack(stack_trace)\n response.success = False\n response.setError(err)\n\n return response\n";
16
- declare const examples_url = "\nExamples:\n1- Open \"workflow-docs.json\" in your browser at http://localhost:4000/workflow-docs\n2- Open \"db-manager.json\" in your browser at http://localhost:4000/db-manager\n3- Open \"dashboard-gen.json\" in your browser at http://localhost:4000/dashboard-gen\n4- Open \"countries.json\" in your browser at http://localhost:4000/countries\n5- Open \"chat.json\" in your browser at http://localhost:4000/chat (set OPENROUTER_API_KEY first)\n6- Open \"chat-memory.json\" in your browser at http://localhost:4000/chat-memory (needs OPENROUTER_API_KEY + Redis at REDIS_URL)\n\nFor more documentation, visit src/nodes/examples/README.md. The first three examples require a PostgreSQL database to function.\n";
16
+ declare const examples_url = "\nExamples:\n1- Open \"workflow-docs.json\" in your browser at http://localhost:4000/workflow-docs\n2- Open \"db-manager.json\" in your browser at http://localhost:4000/db-manager\n3- Open \"dashboard-gen.json\" in your browser at http://localhost:4000/dashboard-gen\n4- Open \"countries.json\" in your browser at http://localhost:4000/countries\n5- Open \"chat.json\" in your browser at http://localhost:4000/chat (set OPENROUTER_API_KEY first)\n6- Open \"chat-memory.json\" in your browser at http://localhost:4000/chat-memory (needs OPENROUTER_API_KEY + Redis at REDIS_URL)\n7- Webhook router: POST /webhooks/{stripe,github,linear} with signed bodies \u2014 set the matching *_WEBHOOK_SECRET env vars (needs --triggers webhook)\n8- LLM agent w/ tool calls: open http://localhost:4000/agent \u2014 model picks between get_weather and calculate tools (needs OPENROUTER_API_KEY + Redis)\n9- Worker fan-out: POST /fanout/jobs with body '{items:[...], tenantId?:\"...\"}' to enqueue N worker jobs (needs --triggers worker; BLOK_WORKER_ADAPTER=in-memory works single-process)\n\nFor more documentation, visit src/nodes/examples/README.md. The first three examples require a PostgreSQL database to function.\n";
17
17
  declare const workflow_template = "\n{\n\t\"name\": \"My Workflow\",\n\t\"description\": \"What this workflow does\",\n\t\"version\": \"1.0.0\",\n\t\"trigger\": {\n\t\t\"http\": {\n\t\t\t\"method\": \"GET\",\n\t\t\t\"accept\": \"application/json\"\n\t\t}\n\t},\n\t\"steps\": [\n\t\t{\n\t\t\t\"id\": \"echo\",\n\t\t\t\"use\": \"@blokjs/respond\",\n\t\t\t\"inputs\": {\n\t\t\t\t\"body\": \"$.req.body\"\n\t\t\t}\n\t\t}\n\t]\n}\n";
18
18
  declare const supervisord_nodejs = "\n[supervisord]\nnodaemon=true\n\n[program:nodejs_app]\ncommand=npm start\ndirectory=/app\nautostart=true\nautorestart=true\nstderr_logfile=/var/log/nodejs.err.log\nstdout_logfile=/var/log/nodejs.out.log\n";
19
19
  declare const supervisord_python = "\n[program:python_app]\ncommand=python3 /app/.blok/runtimes/python3/server.py\ndirectory=/app\nautostart=true\nautorestart=true\nstderr_logfile=/var/log/python.err.log\nstdout_logfile=/var/log/python.out.log\n";
@@ -71,6 +71,9 @@ Examples:
71
71
  4- Open "countries.json" in your browser at http://localhost:4000/countries
72
72
  5- Open "chat.json" in your browser at http://localhost:4000/chat (set OPENROUTER_API_KEY first)
73
73
  6- Open "chat-memory.json" in your browser at http://localhost:4000/chat-memory (needs OPENROUTER_API_KEY + Redis at REDIS_URL)
74
+ 7- Webhook router: POST /webhooks/{stripe,github,linear} with signed bodies — set the matching *_WEBHOOK_SECRET env vars (needs --triggers webhook)
75
+ 8- LLM agent w/ tool calls: open http://localhost:4000/agent — model picks between get_weather and calculate tools (needs OPENROUTER_API_KEY + Redis)
76
+ 9- Worker fan-out: POST /fanout/jobs with body '{items:[...], tenantId?:"..."}' to enqueue N worker jobs (needs --triggers worker; BLOK_WORKER_ADAPTER=in-memory works single-process)
74
77
 
75
78
  For more documentation, visit src/nodes/examples/README.md. The first three examples require a PostgreSQL database to function.
76
79
  `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blokctl",
3
- "version": "0.6.9",
3
+ "version": "0.6.11",
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.9",
33
+ "@blokjs/runner": "^0.6.11",
34
34
  "@clack/prompts": "^1.0.0",
35
35
  "ai": "^4.3.16",
36
36
  "better-sqlite3": "^12.6.2",