blokctl 0.6.17 → 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.
- package/dist/commands/create/project.d.ts +3 -0
- package/dist/commands/create/project.js +44 -42
- package/dist/commands/create/utils/Examples.d.ts +2 -2
- package/dist/commands/create/utils/Examples.js +66 -29
- package/dist/commands/dev/index.js +7 -1
- package/dist/commands/nodes/index.d.ts +1 -0
- package/dist/commands/nodes/index.js +13 -0
- package/dist/commands/nodes/listNodes.d.ts +12 -0
- package/dist/commands/nodes/listNodes.js +56 -0
- package/dist/commands/nodes/listNodes.test.d.ts +1 -0
- package/dist/commands/nodes/listNodes.test.js +72 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +2 -2
|
@@ -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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
670
|
-
"# Stripe: copy from https://dashboard.stripe.com/webhooks (`whsec_…`).",
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
"#
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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
|
-
|
|
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 (
|
|
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 \{
|
|
1529
|
-
content = content.replace(/(export default class \w+ extends WorkerTrigger \{
|
|
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 (
|
|
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
|
-
|
|
1603
|
-
|
|
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### 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## 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\
|
|
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, };
|
|
@@ -762,6 +762,26 @@ export default defineNode({
|
|
|
762
762
|
}
|
|
763
763
|
\`\`\`
|
|
764
764
|
|
|
765
|
+
### Workflow Naming
|
|
766
|
+
|
|
767
|
+
Every workflow's \\\`name\\\` must be UNIQUE across the project. The
|
|
768
|
+
\\\`WorkflowRegistry\\\` rejects duplicate names at boot, so a collision
|
|
769
|
+
means only one of the colliding workflows ever registers.
|
|
770
|
+
|
|
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
|
+
\\\`blok["World Countries"]\\\` accessor. Duplicate names also make
|
|
777
|
+
\\\`gen app-types\\\` report a collision and DROP one workflow from the
|
|
778
|
+
generated \\\`BlokApp\\\` type.
|
|
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
|
+
|
|
765
785
|
### Step Types
|
|
766
786
|
|
|
767
787
|
| Type | Description |
|
|
@@ -780,21 +800,16 @@ export default defineNode({
|
|
|
780
800
|
|
|
781
801
|
\`\`\`json
|
|
782
802
|
{
|
|
783
|
-
"
|
|
784
|
-
|
|
785
|
-
"
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
{
|
|
792
|
-
"type": "else",
|
|
793
|
-
"steps": [{ "name": "default-path", "node": "handle-default", "type": "module" }]
|
|
794
|
-
}
|
|
795
|
-
]
|
|
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
|
+
}
|
|
796
811
|
}
|
|
797
|
-
|
|
812
|
+
]
|
|
798
813
|
}
|
|
799
814
|
\`\`\`
|
|
800
815
|
|
|
@@ -807,7 +822,6 @@ export default defineNode({
|
|
|
807
822
|
| \\\`http\\\` | \\\`{ "method": "GET", "path": "/", "accept": "application/json" }\\\` |
|
|
808
823
|
| \\\`grpc\\\` | \\\`{ "service": "UserService", "method": "GetUser" }\\\` |
|
|
809
824
|
| \\\`cron\\\` | \\\`{ "schedule": "0 * * * *", "timezone": "UTC" }\\\` |
|
|
810
|
-
| \\\`queue\\\` | \\\`{ "provider": "kafka", "topic": "events" }\\\` |
|
|
811
825
|
| \\\`pubsub\\\` | \\\`{ "provider": "gcp", "topic": "updates" }\\\` |
|
|
812
826
|
| \\\`webhook\\\` | \\\`{ "source": "github", "events": ["push"] }\\\` |
|
|
813
827
|
| \\\`websocket\\\` | \\\`{ "events": ["message"], "path": "/ws" }\\\` |
|
|
@@ -819,19 +833,25 @@ export default defineNode({
|
|
|
819
833
|
The worker trigger processes background jobs from a queue with retry logic and concurrency control.
|
|
820
834
|
|
|
821
835
|
\\\`\\\`\\\`typescript
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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
|
+
});
|
|
830
850
|
\\\`\\\`\\\`
|
|
831
851
|
|
|
832
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.
|
|
833
853
|
|
|
834
|
-
|
|
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\\\`.
|
|
835
855
|
|
|
836
856
|
### NATS JetStream
|
|
837
857
|
|
|
@@ -945,6 +965,16 @@ npm test # Run tests
|
|
|
945
965
|
|
|
946
966
|
When users have data flow issues, check these three things first.
|
|
947
967
|
|
|
968
|
+
## Workflow Naming
|
|
969
|
+
|
|
970
|
+
Workflow \\\`name\\\` must be UNIQUE across the project — the
|
|
971
|
+
\\\`WorkflowRegistry\\\` rejects duplicates at boot. Use a dotted
|
|
972
|
+
\\\`domain.action\\\` convention (\\\`countries.list\\\`, \\\`users.create\\\`)
|
|
973
|
+
so the typed client (\\\`@blokjs/client\\\`) and \\\`blokctl gen app-types\\\`
|
|
974
|
+
expose clean nested accessors like \\\`blok.countries.list(...)\\\`. Duplicate
|
|
975
|
+
names make \\\`gen app-types\\\` flag a collision and drop one workflow from
|
|
976
|
+
the generated \\\`BlokApp\\\` type.
|
|
977
|
+
|
|
948
978
|
## Debugging Workflows
|
|
949
979
|
|
|
950
980
|
1. **Verify structure**: Every step has an \\\`id\\\` and a \\\`use\\\` (v2). v1's \\\`name\\\` + \\\`nodes{}\\\` still works but is normalized at load time.
|
|
@@ -993,14 +1023,21 @@ export default defineNode({
|
|
|
993
1023
|
Worker trigger processes background jobs from a queue:
|
|
994
1024
|
|
|
995
1025
|
\\\`\\\`\\\`typescript
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
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
|
+
});
|
|
1000
1037
|
\\\`\\\`\\\`
|
|
1001
1038
|
|
|
1002
1039
|
Job data: \\\`ctx.request.body\\\` = payload, \\\`ctx.request.params.queue/jobId/attempt\\\` = metadata.
|
|
1003
|
-
|
|
1040
|
+
Providers: \\\`in-memory\\\` (dev default), \\\`nats\\\`, \\\`bullmq\\\`, \\\`rabbitmq\\\`, \\\`sqs\\\`, \\\`kafka\\\`, \\\`redis\\\`, \\\`pg-boss\\\` — set via \\\`trigger.worker.provider\\\` or \\\`BLOK_WORKER_ADAPTER\\\`.
|
|
1004
1041
|
|
|
1005
1042
|
## Testing
|
|
1006
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
|
-
|
|
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) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { program } from "../../services/commander.js";
|
|
3
|
+
import { listNodes } from "./listNodes.js";
|
|
4
|
+
const nodes = new Command("nodes").description("Inspect the node catalog of a running Blok server");
|
|
5
|
+
const list = new Command("list")
|
|
6
|
+
.description("List every node across all runtimes (hits GET /__blok/nodes on a running server)")
|
|
7
|
+
.option("-u, --url <value>", "Base URL of the running Blok server", "http://localhost:4000")
|
|
8
|
+
.option("--json", "Output raw JSON instead of a table")
|
|
9
|
+
.action(async (options) => {
|
|
10
|
+
await listNodes(options);
|
|
11
|
+
});
|
|
12
|
+
nodes.addCommand(list);
|
|
13
|
+
program.addCommand(nodes);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { OptionValues } from "commander";
|
|
2
|
+
export interface NodeEntry {
|
|
3
|
+
name: string;
|
|
4
|
+
runtime: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
inputSchema: unknown | null;
|
|
7
|
+
outputSchema: unknown | null;
|
|
8
|
+
tags?: string[];
|
|
9
|
+
}
|
|
10
|
+
export declare function schemaMark(node: NodeEntry): string;
|
|
11
|
+
export declare function formatCatalog(nodes: readonly NodeEntry[]): string;
|
|
12
|
+
export declare function listNodes(opts: OptionValues): Promise<void>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import color from "picocolors";
|
|
2
|
+
export function schemaMark(node) {
|
|
3
|
+
const parts = [];
|
|
4
|
+
if (node.inputSchema)
|
|
5
|
+
parts.push("in");
|
|
6
|
+
if (node.outputSchema)
|
|
7
|
+
parts.push("out");
|
|
8
|
+
return parts.length > 0 ? parts.join(",") : "—";
|
|
9
|
+
}
|
|
10
|
+
export function formatCatalog(nodes) {
|
|
11
|
+
if (nodes.length === 0)
|
|
12
|
+
return "No nodes found.";
|
|
13
|
+
const rows = nodes.map((n) => ({
|
|
14
|
+
name: n.name,
|
|
15
|
+
runtime: n.runtime,
|
|
16
|
+
schema: schemaMark(n),
|
|
17
|
+
description: n.description ?? "",
|
|
18
|
+
}));
|
|
19
|
+
const nameW = Math.max("NAME".length, ...rows.map((r) => r.name.length));
|
|
20
|
+
const rtW = Math.max("RUNTIME".length, ...rows.map((r) => r.runtime.length));
|
|
21
|
+
const schW = Math.max("SCHEMA".length, ...rows.map((r) => r.schema.length));
|
|
22
|
+
const header = `${"NAME".padEnd(nameW)} ${"RUNTIME".padEnd(rtW)} ${"SCHEMA".padEnd(schW)} DESCRIPTION`;
|
|
23
|
+
const lines = [header];
|
|
24
|
+
for (const r of rows) {
|
|
25
|
+
lines.push(`${r.name.padEnd(nameW)} ${r.runtime.padEnd(rtW)} ${r.schema.padEnd(schW)} ${r.description}`.trimEnd());
|
|
26
|
+
}
|
|
27
|
+
return lines.join("\n");
|
|
28
|
+
}
|
|
29
|
+
export async function listNodes(opts) {
|
|
30
|
+
const baseUrl = (opts.url ?? "http://localhost:4000").replace(/\/+$/, "");
|
|
31
|
+
const endpoint = `${baseUrl}/__blok/nodes`;
|
|
32
|
+
let body;
|
|
33
|
+
try {
|
|
34
|
+
const res = await fetch(endpoint);
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
console.log(color.red(`❌ ${endpoint} returned HTTP ${res.status}.`));
|
|
37
|
+
process.exit(1);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
body = (await res.json());
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
console.log(color.red(`❌ Could not reach ${color.cyan(endpoint)} — is the Blok server running? ` +
|
|
44
|
+
`Pass --url <baseUrl> to point elsewhere. (${err.message})`));
|
|
45
|
+
process.exit(1);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const nodes = body.nodes ?? [];
|
|
49
|
+
if (opts.json === true) {
|
|
50
|
+
console.log(JSON.stringify(nodes, null, 2));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
console.log(color.cyan(`\n📦 Node catalog — ${nodes.length} node(s) across runtimes (${baseUrl})\n`));
|
|
54
|
+
console.log(formatCatalog(nodes));
|
|
55
|
+
console.log("");
|
|
56
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { formatCatalog, listNodes, schemaMark } from "./listNodes.js";
|
|
3
|
+
describe("schemaMark", () => {
|
|
4
|
+
it("reports which schemas a node exposes", () => {
|
|
5
|
+
expect(schemaMark({ name: "a", runtime: "module", inputSchema: {}, outputSchema: {} })).toBe("in,out");
|
|
6
|
+
expect(schemaMark({ name: "a", runtime: "module", inputSchema: {}, outputSchema: null })).toBe("in");
|
|
7
|
+
expect(schemaMark({ name: "a", runtime: "module", inputSchema: null, outputSchema: {} })).toBe("out");
|
|
8
|
+
expect(schemaMark({ name: "a", runtime: "module", inputSchema: null, outputSchema: null })).toBe("—");
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
describe("formatCatalog", () => {
|
|
12
|
+
it("renders an aligned table with a header", () => {
|
|
13
|
+
const nodes = [
|
|
14
|
+
{
|
|
15
|
+
name: "@blokjs/respond",
|
|
16
|
+
runtime: "module",
|
|
17
|
+
description: "Shape the HTTP response",
|
|
18
|
+
inputSchema: {},
|
|
19
|
+
outputSchema: null,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: "@py/search",
|
|
23
|
+
runtime: "runtime.python3",
|
|
24
|
+
description: "Semantic search",
|
|
25
|
+
inputSchema: {},
|
|
26
|
+
outputSchema: {},
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
const out = formatCatalog(nodes);
|
|
30
|
+
const lines = out.split("\n");
|
|
31
|
+
expect(lines[0]).toMatch(/^NAME\s+RUNTIME\s+SCHEMA\s+DESCRIPTION$/);
|
|
32
|
+
expect(out).toContain("@blokjs/respond");
|
|
33
|
+
expect(out).toContain("runtime.python3");
|
|
34
|
+
expect(out).toContain("in,out");
|
|
35
|
+
expect(out).toContain("Semantic search");
|
|
36
|
+
const rtOffset = lines[0].indexOf("RUNTIME");
|
|
37
|
+
expect(lines[1].slice(rtOffset, rtOffset + 6)).toBe("module");
|
|
38
|
+
});
|
|
39
|
+
it("handles an empty catalog", () => {
|
|
40
|
+
expect(formatCatalog([])).toBe("No nodes found.");
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
describe("listNodes (fetch-mocked)", () => {
|
|
44
|
+
const realFetch = globalThis.fetch;
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
globalThis.fetch = realFetch;
|
|
47
|
+
vi.restoreAllMocks();
|
|
48
|
+
});
|
|
49
|
+
it("fetches /__blok/nodes from the given --url and prints the table", async () => {
|
|
50
|
+
let seenUrl = "";
|
|
51
|
+
globalThis.fetch = vi.fn(async (url) => {
|
|
52
|
+
seenUrl = String(url);
|
|
53
|
+
return new Response(JSON.stringify({ count: 1, nodes: [{ name: "@x/a", runtime: "module", inputSchema: {}, outputSchema: null }] }), { status: 200, headers: { "content-type": "application/json" } });
|
|
54
|
+
});
|
|
55
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => { });
|
|
56
|
+
await listNodes({ url: "https://api.test/" });
|
|
57
|
+
expect(seenUrl).toBe("https://api.test/__blok/nodes");
|
|
58
|
+
const printed = log.mock.calls.map((c) => String(c[0])).join("\n");
|
|
59
|
+
expect(printed).toContain("@x/a");
|
|
60
|
+
expect(printed).toContain("1 node(s)");
|
|
61
|
+
});
|
|
62
|
+
it("emits raw JSON with --json", async () => {
|
|
63
|
+
globalThis.fetch = vi.fn(async () => new Response(JSON.stringify({ nodes: [{ name: "@x/a", runtime: "module", inputSchema: null, outputSchema: null }] }), {
|
|
64
|
+
status: 200,
|
|
65
|
+
headers: { "content-type": "application/json" },
|
|
66
|
+
}));
|
|
67
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => { });
|
|
68
|
+
await listNodes({ url: "http://localhost:4000", json: true });
|
|
69
|
+
const printed = log.mock.calls.map((c) => String(c[0])).join("\n");
|
|
70
|
+
expect(JSON.parse(printed)).toEqual([{ name: "@x/a", runtime: "module", inputSchema: null, outputSchema: null }]);
|
|
71
|
+
});
|
|
72
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ import "./commands/install/index.js";
|
|
|
9
9
|
import "./commands/search/index.js";
|
|
10
10
|
import "./commands/generate/index.js";
|
|
11
11
|
import "./commands/gen/index.js";
|
|
12
|
+
import "./commands/nodes/index.js";
|
|
12
13
|
import "./commands/config/index.js";
|
|
13
14
|
import "./commands/migrate/index.js";
|
|
14
15
|
import "./commands/graph/index.js";
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,7 @@ import "./commands/install/index.js";
|
|
|
24
24
|
import "./commands/search/index.js";
|
|
25
25
|
import "./commands/generate/index.js";
|
|
26
26
|
import "./commands/gen/index.js";
|
|
27
|
+
import "./commands/nodes/index.js";
|
|
27
28
|
import "./commands/config/index.js";
|
|
28
29
|
import "./commands/migrate/index.js";
|
|
29
30
|
import "./commands/graph/index.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blokctl",
|
|
3
|
-
"version": "0.6.
|
|
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.
|
|
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",
|