blokctl 0.6.20 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/__tests__/modular-observability.capstone.e2e.test.js +72 -0
  2. package/dist/commands/create/node.js +46 -66
  3. package/dist/commands/create/project.js +55 -9
  4. package/dist/commands/create/utils/Examples.d.ts +8 -20
  5. package/dist/commands/create/utils/Examples.js +138 -412
  6. package/dist/commands/dev/index.js +40 -1
  7. package/dist/commands/gen/appTypes.js +40 -1
  8. package/dist/commands/generate/NodeGenerator.d.ts +0 -2
  9. package/dist/commands/generate/NodeGenerator.js +0 -20
  10. package/dist/commands/generate/RuntimeGenerator.d.ts +0 -2
  11. package/dist/commands/generate/RuntimeGenerator.js +0 -19
  12. package/dist/commands/generate/RuntimeGenerator.test.js +0 -29
  13. package/dist/commands/generate/TriggerGenerator.d.ts +0 -2
  14. package/dist/commands/generate/TriggerGenerator.js +0 -19
  15. package/dist/commands/generate/WorkflowGenerator.d.ts +0 -2
  16. package/dist/commands/generate/WorkflowGenerator.js +0 -19
  17. package/dist/commands/generate/e2e/NodeGenerator.e2e.test.js +0 -12
  18. package/dist/commands/generate/e2e/RuntimeGenerator.e2e.test.js +0 -12
  19. package/dist/commands/generate/e2e/TriggerGenerator.e2e.test.js +0 -14
  20. package/dist/commands/monitor/monitor-component.js +5 -5
  21. package/dist/commands/observability/add.d.ts +2 -0
  22. package/dist/commands/observability/add.js +113 -0
  23. package/dist/commands/observability/alerting-module.test.js +43 -0
  24. package/dist/commands/observability/apply.d.ts +10 -0
  25. package/dist/commands/observability/apply.js +11 -0
  26. package/dist/commands/observability/descriptor.d.ts +37 -0
  27. package/dist/commands/observability/descriptor.js +203 -0
  28. package/dist/commands/observability/descriptor.test.d.ts +1 -0
  29. package/dist/commands/observability/descriptor.test.js +40 -0
  30. package/dist/commands/observability/index.d.ts +1 -0
  31. package/dist/commands/observability/index.js +53 -0
  32. package/dist/commands/observability/list.d.ts +2 -0
  33. package/dist/commands/observability/list.js +45 -0
  34. package/dist/commands/observability/logging-module.test.d.ts +1 -0
  35. package/dist/commands/observability/logging-module.test.js +43 -0
  36. package/dist/commands/observability/obs-stack-module.test.d.ts +1 -0
  37. package/dist/commands/observability/obs-stack-module.test.js +33 -0
  38. package/dist/commands/observability/remove.d.ts +2 -0
  39. package/dist/commands/observability/remove.js +62 -0
  40. package/dist/commands/observability/shared.d.ts +6 -0
  41. package/dist/commands/observability/shared.js +23 -0
  42. package/dist/commands/observability/status.d.ts +2 -0
  43. package/dist/commands/observability/status.js +36 -0
  44. package/dist/commands/observability/tracing-module.test.d.ts +1 -0
  45. package/dist/commands/observability/tracing-module.test.js +42 -0
  46. package/dist/commands/profile/index.js +7 -10
  47. package/dist/commands/watch/format.d.ts +23 -0
  48. package/dist/commands/watch/format.js +60 -0
  49. package/dist/commands/watch/index.d.ts +1 -0
  50. package/dist/commands/watch/index.js +53 -0
  51. package/dist/commands/watch/sse.d.ts +16 -0
  52. package/dist/commands/watch/sse.js +82 -0
  53. package/dist/index.d.ts +2 -0
  54. package/dist/index.js +4 -0
  55. package/dist/services/obs-setup.d.ts +5 -0
  56. package/dist/services/obs-setup.js +68 -0
  57. package/dist/services/obs-setup.test.d.ts +1 -0
  58. package/dist/services/obs-setup.test.js +71 -0
  59. package/dist/services/obs-tiers.d.ts +9 -0
  60. package/dist/services/obs-tiers.js +16 -0
  61. package/dist/services/observability-mutations.d.ts +4 -0
  62. package/dist/services/observability-mutations.js +46 -0
  63. package/dist/services/observability-mutations.test.d.ts +1 -0
  64. package/dist/services/observability-mutations.test.js +57 -0
  65. package/dist/services/runtime-setup.d.ts +12 -1
  66. package/dist/services/runtime-setup.js +274 -14
  67. package/dist/studio-dist/assets/{index-BD8_9YPN.js → index-CnFqCRQe.js} +17 -17
  68. package/dist/studio-dist/index.html +1 -1
  69. package/package.json +3 -3
  70. package/dist/commands/generate/GenerationAnalytics.d.ts +0 -61
  71. package/dist/commands/generate/GenerationAnalytics.js +0 -163
  72. package/dist/commands/generate/GenerationAnalytics.test.js +0 -407
  73. package/dist/commands/generate/PromptVersioning.d.ts +0 -25
  74. package/dist/commands/generate/PromptVersioning.js +0 -71
  75. package/dist/commands/generate/PromptVersioning.test.js +0 -120
  76. /package/dist/{commands/generate/GenerationAnalytics.test.d.ts → __tests__/modular-observability.capstone.e2e.test.d.ts} +0 -0
  77. /package/dist/commands/{generate/PromptVersioning.test.d.ts → observability/alerting-module.test.d.ts} +0 -0
@@ -1,9 +1,12 @@
1
1
  import { spawn } from "node:child_process";
2
+ import child_process from "node:child_process";
2
3
  import path from "node:path";
4
+ import util from "node:util";
3
5
  import fsExtra from "fs-extra";
4
6
  import { waitForGrpcPort } from "../../services/health-probe.js";
5
7
  import { detectRr } from "../../services/runtime-detector.js";
6
- import { readProjectConfig, validateProjectRuntimes } from "../../services/runtime-setup.js";
8
+ import { generateCSharpNodeRegistry, generateGoNodeRegistry, generateJavaNodeRegistry, generateRustNodeRegistry, readProjectConfig, validateProjectRuntimes, } from "../../services/runtime-setup.js";
9
+ const exec = util.promisify(child_process.exec);
7
10
  const runningProcesses = [];
8
11
  function spawnProcess(cmd, args, name, currentPath, cwd, env) {
9
12
  const child = spawn(cmd, args, {
@@ -98,6 +101,42 @@ export async function devProject(opts) {
98
101
  HOST: "0.0.0.0",
99
102
  BLOK_TRANSPORT: "grpc",
100
103
  };
104
+ if (rt.kind === "python3" || rt.kind === "ruby" || rt.kind === "php") {
105
+ env.BLOK_NODES_DIR = path.resolve(currentPath, "runtimes", rt.kind, "nodes");
106
+ }
107
+ if (rt.kind === "go") {
108
+ try {
109
+ generateGoNodeRegistry(currentPath);
110
+ }
111
+ catch (err) {
112
+ console.log(` Warning: Go user-node codegen failed: ${err.message}`);
113
+ }
114
+ }
115
+ if (rt.kind === "rust") {
116
+ try {
117
+ generateRustNodeRegistry(currentPath);
118
+ }
119
+ catch (err) {
120
+ console.log(` Warning: Rust user-node codegen failed: ${err.message}`);
121
+ }
122
+ }
123
+ if (rt.kind === "csharp") {
124
+ try {
125
+ generateCSharpNodeRegistry(currentPath);
126
+ }
127
+ catch (err) {
128
+ console.log(` Warning: C# user-node codegen failed: ${err.message}`);
129
+ }
130
+ }
131
+ if (rt.kind === "java") {
132
+ try {
133
+ generateJavaNodeRegistry(currentPath);
134
+ await exec("mvn package -q -DskipTests", { cwd: runtimeCwd, timeout: 300000 });
135
+ }
136
+ catch (err) {
137
+ console.log(` Warning: Java user-node codegen/build failed: ${err.message}`);
138
+ }
139
+ }
101
140
  runtimeDefs.push({
102
141
  cmd,
103
142
  args,
@@ -126,6 +126,35 @@ async function collectTsFiles(dir) {
126
126
  }
127
127
  return out;
128
128
  }
129
+ async function collectJsonWorkflowNames(dir) {
130
+ const names = [];
131
+ let dirents;
132
+ try {
133
+ dirents = await fsp.readdir(dir, { withFileTypes: true });
134
+ }
135
+ catch {
136
+ return names;
137
+ }
138
+ for (const d of dirents) {
139
+ if (d.name.startsWith("_") || d.name.startsWith("."))
140
+ continue;
141
+ const full = path.join(dir, d.name);
142
+ if (d.isDirectory()) {
143
+ names.push(...(await collectJsonWorkflowNames(full)));
144
+ }
145
+ else if (d.name.endsWith(".json")) {
146
+ try {
147
+ const parsed = JSON.parse(await fsp.readFile(full, "utf8"));
148
+ if (typeof parsed.name === "string" && (parsed.trigger !== undefined || parsed.steps !== undefined)) {
149
+ names.push(parsed.name);
150
+ }
151
+ }
152
+ catch {
153
+ }
154
+ }
155
+ }
156
+ return names;
157
+ }
129
158
  async function resolveWorkflowsDir(cwd, explicit) {
130
159
  const candidates = explicit
131
160
  ? [explicit]
@@ -159,6 +188,14 @@ export async function generateAppTypes(opts) {
159
188
  : path.join(cwd, opts.out ?? "blok-app.d.ts");
160
189
  console.log(color.dim(`Scanning ${color.cyan(dir)} (recursive)\n`));
161
190
  const files = await collectTsFiles(dir);
191
+ const jsonScanDirs = [dir, path.join(cwd, "workflows/json"), path.join(cwd, "triggers/http/workflows/json")];
192
+ const jsonNames = [...new Set((await Promise.all(jsonScanDirs.map((d) => collectJsonWorkflowNames(d)))).flat())];
193
+ const warnJsonSkipped = () => {
194
+ if (jsonNames.length === 0)
195
+ return;
196
+ console.log(color.yellow(`ℹ️ ${jsonNames.length} JSON workflow(s) are NOT in app-types (JSON has no TS type to import): ${jsonNames.join(", ")}.`));
197
+ console.log(color.dim(" Convert them to TS workflows to type them, or call them by string name on the client.\n"));
198
+ };
162
199
  const entries = [];
163
200
  const skipped = [];
164
201
  for (const file of files) {
@@ -173,6 +210,7 @@ export async function generateAppTypes(opts) {
173
210
  console.log(color.yellow("No TS workflows with a literal `name:` found — nothing to generate."));
174
211
  if (skipped.length > 0)
175
212
  console.log(color.dim(`Skipped (no literal name): ${skipped.join(", ")}`));
213
+ warnJsonSkipped();
176
214
  return;
177
215
  }
178
216
  const { source, collisions } = buildAppTypeSource(entries, outFile);
@@ -191,5 +229,6 @@ export async function generateAppTypes(opts) {
191
229
  if (skipped.length > 0) {
192
230
  console.log(color.dim(`ℹ️ Skipped ${skipped.length} file(s) without a literal workflow name (dynamic name or not a workflow): ${skipped.join(", ")}`));
193
231
  }
194
- console.log(color.dim('\nNext: `import type { BlokApp } from "<out>"` and `createBlokClient<BlokApp>({ baseUrl })`.\n'));
232
+ warnJsonSkipped();
233
+ console.log(color.dim('Next: `import type { BlokApp } from "<out>"` and `createBlokClient<BlokApp>({ baseUrl })`.\n'));
195
234
  }
@@ -7,8 +7,6 @@ type NodeInformation = {
7
7
  errors: string[];
8
8
  warnings: string[];
9
9
  attempts: number;
10
- promptVersion?: string;
11
- durationMs?: number;
12
10
  };
13
11
  };
14
12
  export type { NodeInformation };
@@ -1,8 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import { createOpenAI } from "@ai-sdk/openai";
3
3
  import { generateText } from "ai";
4
- import { GenerationAnalytics } from "./GenerationAnalytics.js";
5
- import { getVersionStamp, registerPromptContent } from "./PromptVersioning.js";
6
4
  import createFnNodeSystemPrompt from "./prompts/create-fn-node.system.js";
7
5
  import createNodeSystemPrompt from "./prompts/create-node.system.js";
8
6
  import * as CompilationValidator from "./validators/CompilationValidator.js";
@@ -10,10 +8,6 @@ import * as NodeValidator from "./validators/NodeValidator.js";
10
8
  export default class NodeGenerator {
11
9
  MAX_VALIDATION_ATTEMPTS = 3;
12
10
  async generateNode(nodeName, userPrompt, apiKey, update = false, nodeStyle = "function") {
13
- const analytics = GenerationAnalytics.getInstance();
14
- const getElapsed = analytics.startTimer();
15
- const promptId = nodeStyle === "function" ? "create-fn-node" : "create-node";
16
- const promptVersion = getVersionStamp(promptId);
17
11
  const openai = createOpenAI({
18
12
  compatibility: "strict",
19
13
  apiKey: apiKey,
@@ -21,7 +15,6 @@ export default class NodeGenerator {
21
15
  const promptTemplate = nodeStyle === "function" ? createFnNodeSystemPrompt : createNodeSystemPrompt;
22
16
  let prompt = promptTemplate.prompt;
23
17
  let existingCode = null;
24
- registerPromptContent(promptId, prompt);
25
18
  if (update) {
26
19
  const dirName = nodeName.toLowerCase().replace(/\s+/g, "-");
27
20
  const dirPath = process.cwd();
@@ -72,17 +65,6 @@ export default class NodeGenerator {
72
65
  console.log(`⚠️ Validation failed (attempt ${attempts}/${this.MAX_VALIDATION_ATTEMPTS}). Retrying with feedback...`);
73
66
  }
74
67
  }
75
- const durationMs = getElapsed();
76
- analytics.recordEvent({
77
- type: "node",
78
- subtype: nodeStyle,
79
- name: nodeName,
80
- success: isValid,
81
- attempts,
82
- durationMs,
83
- errors: allErrors,
84
- promptVersion,
85
- });
86
68
  return {
87
69
  nodeName,
88
70
  userPrompt,
@@ -92,8 +74,6 @@ export default class NodeGenerator {
92
74
  errors: validationErrors,
93
75
  warnings: validationWarnings,
94
76
  attempts,
95
- promptVersion,
96
- durationMs,
97
77
  },
98
78
  };
99
79
  }
@@ -11,8 +11,6 @@ export type RuntimeInformation = {
11
11
  errors: string[];
12
12
  warnings: string[];
13
13
  attempts: number;
14
- promptVersion?: string;
15
- durationMs?: number;
16
14
  };
17
15
  };
18
16
  declare const SUPPORTED_LANGUAGES: readonly ["go", "java", "rust", "python", "csharp", "php", "ruby"];
@@ -1,8 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import { createOpenAI } from "@ai-sdk/openai";
3
3
  import { generateText } from "ai";
4
- import { GenerationAnalytics } from "./GenerationAnalytics.js";
5
- import { getVersionStamp, registerPromptContent } from "./PromptVersioning.js";
6
4
  import createRuntimeSystemPrompt from "./prompts/create-runtime.system.js";
7
5
  const SUPPORTED_LANGUAGES = ["go", "java", "rust", "python", "csharp", "php", "ruby"];
8
6
  export function isSupportedLanguage(lang) {
@@ -11,15 +9,11 @@ export function isSupportedLanguage(lang) {
11
9
  export default class RuntimeGenerator {
12
10
  MAX_VALIDATION_ATTEMPTS = 3;
13
11
  async generateRuntime(language, userPrompt, apiKey, update = false, existingPath) {
14
- const analytics = GenerationAnalytics.getInstance();
15
- const getElapsed = analytics.startTimer();
16
- const promptVersion = getVersionStamp("create-runtime");
17
12
  const openai = createOpenAI({
18
13
  compatibility: "strict",
19
14
  apiKey: apiKey,
20
15
  });
21
16
  let prompt = createRuntimeSystemPrompt.prompt;
22
- registerPromptContent("create-runtime", prompt);
23
17
  if (update && existingPath) {
24
18
  const existingContent = this.readExistingRuntime(existingPath);
25
19
  prompt = `${createRuntimeSystemPrompt.updatePrompt}\n\n${existingContent}`;
@@ -54,17 +48,6 @@ export default class RuntimeGenerator {
54
48
  }
55
49
  }
56
50
  const files = this.parseFiles(generatedCode, language);
57
- const durationMs = getElapsed();
58
- analytics.recordEvent({
59
- type: "node",
60
- subtype: `runtime-${language}`,
61
- name: `runtime-${language}`,
62
- success: isValid,
63
- attempts,
64
- durationMs,
65
- errors: allErrors,
66
- promptVersion,
67
- });
68
51
  return {
69
52
  language,
70
53
  userPrompt,
@@ -75,8 +58,6 @@ export default class RuntimeGenerator {
75
58
  errors: validationErrors,
76
59
  warnings: validationWarnings,
77
60
  attempts,
78
- promptVersion,
79
- durationMs,
80
61
  },
81
62
  };
82
63
  }
@@ -6,7 +6,6 @@ vi.mock("ai", () => ({
6
6
  generateText: vi.fn(),
7
7
  }));
8
8
  import { generateText } from "ai";
9
- import { GenerationAnalytics } from "./GenerationAnalytics.js";
10
9
  import RuntimeGenerator, { isSupportedLanguage } from "./RuntimeGenerator.js";
11
10
  const mockedGenerateText = vi.mocked(generateText);
12
11
  describe("RuntimeGenerator", () => {
@@ -15,7 +14,6 @@ describe("RuntimeGenerator", () => {
15
14
  generator = new RuntimeGenerator();
16
15
  vi.clearAllMocks();
17
16
  vi.spyOn(console, "log").mockImplementation(() => { });
18
- GenerationAnalytics.resetInstance();
19
17
  });
20
18
  afterEach(() => {
21
19
  vi.restoreAllMocks();
@@ -433,39 +431,12 @@ func main() {
433
431
  expect(result.validationResult?.attempts).toBe(3);
434
432
  expect(result.validationResult?.errors.length).toBeGreaterThan(0);
435
433
  });
436
- it("should record analytics event", async () => {
437
- mockedGenerateText.mockResolvedValueOnce({ text: validGoRuntime });
438
- await generator.generateRuntime("go", "test", "test-key");
439
- const analytics = GenerationAnalytics.getInstance();
440
- const stats = analytics.getStats();
441
- expect(stats.totalGenerations).toBe(1);
442
- expect(stats.successCount).toBe(1);
443
- });
444
- it("should record failed analytics event", async () => {
445
- mockedGenerateText.mockResolvedValue({ text: "invalid" });
446
- await generator.generateRuntime("java", "test", "test-key");
447
- const analytics = GenerationAnalytics.getInstance();
448
- const stats = analytics.getStats();
449
- expect(stats.totalGenerations).toBe(1);
450
- expect(stats.failureCount).toBe(1);
451
- });
452
434
  it("should strip markdown fences from LLM output", async () => {
453
435
  const wrappedCode = `\`\`\`go\n${validGoRuntime}\n\`\`\``;
454
436
  mockedGenerateText.mockResolvedValueOnce({ text: wrappedCode });
455
437
  const result = await generator.generateRuntime("go", "test", "test-key");
456
438
  expect(result.validationResult?.valid).toBe(true);
457
439
  });
458
- it("should include prompt version in validation result", async () => {
459
- mockedGenerateText.mockResolvedValueOnce({ text: validGoRuntime });
460
- const result = await generator.generateRuntime("go", "test", "test-key");
461
- expect(result.validationResult?.promptVersion).toBe("create-runtime@1.0.0");
462
- });
463
- it("should include duration in validation result", async () => {
464
- mockedGenerateText.mockResolvedValueOnce({ text: validGoRuntime });
465
- const result = await generator.generateRuntime("go", "test", "test-key");
466
- expect(result.validationResult?.durationMs).toBeDefined();
467
- expect(typeof result.validationResult?.durationMs).toBe("number");
468
- });
469
440
  });
470
441
  describe("language-specific validation", () => {
471
442
  it("should warn Go code without go.mod", () => {
@@ -8,8 +8,6 @@ export type TriggerInformation = {
8
8
  errors: string[];
9
9
  warnings: string[];
10
10
  attempts: number;
11
- promptVersion?: string;
12
- durationMs?: number;
13
11
  };
14
12
  };
15
13
  export default class TriggerGenerator {
@@ -1,22 +1,16 @@
1
1
  import * as fs from "node:fs";
2
2
  import { createOpenAI } from "@ai-sdk/openai";
3
3
  import { generateText } from "ai";
4
- import { GenerationAnalytics } from "./GenerationAnalytics.js";
5
- import { getVersionStamp, registerPromptContent } from "./PromptVersioning.js";
6
4
  import createTriggerSystemPrompt from "./prompts/create-trigger.system.js";
7
5
  import * as CompilationValidator from "./validators/CompilationValidator.js";
8
6
  export default class TriggerGenerator {
9
7
  MAX_VALIDATION_ATTEMPTS = 3;
10
8
  async generateTrigger(triggerName, triggerType, userPrompt, apiKey, update = false, existingTriggerPath) {
11
- const analytics = GenerationAnalytics.getInstance();
12
- const getElapsed = analytics.startTimer();
13
- const promptVersion = getVersionStamp("create-trigger");
14
9
  const openai = createOpenAI({
15
10
  compatibility: "strict",
16
11
  apiKey: apiKey,
17
12
  });
18
13
  let prompt = createTriggerSystemPrompt.prompt;
19
- registerPromptContent("create-trigger", prompt);
20
14
  if (update && existingTriggerPath) {
21
15
  const existingContent = fs.readFileSync(existingTriggerPath, "utf8");
22
16
  prompt = `${createTriggerSystemPrompt.updatePrompt}\n\n${existingContent}`;
@@ -58,17 +52,6 @@ export default class TriggerGenerator {
58
52
  console.log(`⚠️ Trigger validation failed (attempt ${attempts}/${this.MAX_VALIDATION_ATTEMPTS}). Retrying with feedback...`);
59
53
  }
60
54
  }
61
- const durationMs = getElapsed();
62
- analytics.recordEvent({
63
- type: "trigger",
64
- subtype: triggerType,
65
- name: triggerName,
66
- success: isValid,
67
- attempts,
68
- durationMs,
69
- errors: allErrors,
70
- promptVersion,
71
- });
72
55
  return {
73
56
  triggerName,
74
57
  triggerType,
@@ -79,8 +62,6 @@ export default class TriggerGenerator {
79
62
  errors: validationErrors,
80
63
  warnings: validationWarnings,
81
64
  attempts,
82
- promptVersion,
83
- durationMs,
84
65
  },
85
66
  };
86
67
  }
@@ -8,8 +8,6 @@ export type WorkflowInformation = {
8
8
  errors: string[];
9
9
  warnings: string[];
10
10
  attempts: number;
11
- promptVersion?: string;
12
- durationMs?: number;
13
11
  };
14
12
  };
15
13
  export default class WorkflowGenerator {
@@ -1,22 +1,16 @@
1
1
  import * as fs from "node:fs";
2
2
  import { createOpenAI } from "@ai-sdk/openai";
3
3
  import { generateText } from "ai";
4
- import { GenerationAnalytics } from "./GenerationAnalytics.js";
5
- import { getVersionStamp, registerPromptContent } from "./PromptVersioning.js";
6
4
  import createWorkflowSystemPrompt from "./prompts/create-workflow.system.js";
7
5
  import * as WorkflowValidator from "./validators/WorkflowValidator.js";
8
6
  export default class WorkflowGenerator {
9
7
  MAX_VALIDATION_ATTEMPTS = 3;
10
8
  async generateWorkflow(workflowName, userPrompt, apiKey, triggerType, update = false, existingWorkflowPath) {
11
- const analytics = GenerationAnalytics.getInstance();
12
- const getElapsed = analytics.startTimer();
13
- const promptVersion = getVersionStamp("create-workflow");
14
9
  const openai = createOpenAI({
15
10
  compatibility: "strict",
16
11
  apiKey: apiKey,
17
12
  });
18
13
  let prompt = createWorkflowSystemPrompt.prompt;
19
- registerPromptContent("create-workflow", prompt);
20
14
  if (update && existingWorkflowPath) {
21
15
  const existingContent = fs.readFileSync(existingWorkflowPath, "utf8");
22
16
  prompt = `${createWorkflowSystemPrompt.updatePrompt}\n\n${existingContent}`;
@@ -50,17 +44,6 @@ export default class WorkflowGenerator {
50
44
  console.log(`⚠️ Workflow validation failed (attempt ${attempts}/${this.MAX_VALIDATION_ATTEMPTS}). Retrying with feedback...`);
51
45
  }
52
46
  }
53
- const durationMs = getElapsed();
54
- analytics.recordEvent({
55
- type: "workflow",
56
- subtype: triggerType,
57
- name: workflowName,
58
- success: isValid,
59
- attempts,
60
- durationMs,
61
- errors: allErrors,
62
- promptVersion,
63
- });
64
47
  return {
65
48
  workflowName,
66
49
  userPrompt,
@@ -71,8 +54,6 @@ export default class WorkflowGenerator {
71
54
  errors: validationErrors,
72
55
  warnings: validationWarnings,
73
56
  attempts,
74
- promptVersion,
75
- durationMs,
76
57
  },
77
58
  };
78
59
  }
@@ -115,18 +115,6 @@ describe("NodeGenerator E2E", () => {
115
115
  const callArgs = mockedGenerateText.mock.calls[0][0];
116
116
  expect(callArgs.temperature).toBe(0.2);
117
117
  });
118
- it("should include prompt version in validation result", async () => {
119
- mockedGenerateText.mockResolvedValueOnce({ text: VALID_FUNCTION_FIRST_NODE });
120
- mockValidPass();
121
- const result = await generator.generateNode("test-node", "Create a test node", "test-api-key", false, "function");
122
- expect(result.validationResult.promptVersion).toContain("create-fn-node@");
123
- });
124
- it("should include duration in validation result", async () => {
125
- mockedGenerateText.mockResolvedValueOnce({ text: VALID_FUNCTION_FIRST_NODE });
126
- mockValidPass();
127
- const result = await generator.generateNode("test-node", "Create a test node", "test-api-key", false, "function");
128
- expect(result.validationResult.durationMs).toBeGreaterThanOrEqual(0);
129
- });
130
118
  });
131
119
  describe("validation feedback loop", () => {
132
120
  it("should retry with feedback on first failure and succeed on second attempt", async () => {
@@ -645,18 +645,6 @@ describe("RuntimeGenerator E2E", () => {
645
645
  expect(mockedGenerateText).toHaveBeenCalledTimes(1);
646
646
  });
647
647
  });
648
- describe("analytics integration", () => {
649
- it("should include prompt version in validation result", async () => {
650
- mockedGenerateText.mockResolvedValueOnce({ text: VALID_GO_RUNTIME });
651
- const result = await generator.generateRuntime("go", "Create a runtime", "test-api-key");
652
- expect(result.validationResult.promptVersion).toContain("create-runtime@");
653
- });
654
- it("should include duration in validation result", async () => {
655
- mockedGenerateText.mockResolvedValueOnce({ text: VALID_GO_RUNTIME });
656
- const result = await generator.generateRuntime("go", "Create a runtime", "test-api-key");
657
- expect(result.validationResult.durationMs).toBeGreaterThanOrEqual(0);
658
- });
659
- });
660
648
  describe("multi-file parsing", () => {
661
649
  it("should parse files separated by // FILE: markers", async () => {
662
650
  const code = [
@@ -278,18 +278,4 @@ describe("TriggerGenerator E2E", () => {
278
278
  expect(system).toContain("loadNodes");
279
279
  });
280
280
  });
281
- describe("analytics integration", () => {
282
- it("should include prompt version in validation result", async () => {
283
- mockedGenerateText.mockResolvedValueOnce({ text: VALID_QUEUE_TRIGGER });
284
- mockedValidateCode.mockReturnValue({ success: true, errors: [], warnings: [] });
285
- const result = await generator.generateTrigger("test-trigger", "queue", "Create a test trigger", "test-api-key");
286
- expect(result.validationResult.promptVersion).toContain("create-trigger@");
287
- });
288
- it("should include duration in validation result", async () => {
289
- mockedGenerateText.mockResolvedValueOnce({ text: VALID_QUEUE_TRIGGER });
290
- mockedValidateCode.mockReturnValue({ success: true, errors: [], warnings: [] });
291
- const result = await generator.generateTrigger("test-trigger", "queue", "Create a test trigger", "test-api-key");
292
- expect(result.validationResult.durationMs).toBeGreaterThanOrEqual(0);
293
- });
294
- });
295
281
  });
@@ -120,11 +120,11 @@ const fetchPrometheusMetrics = async (host, token) => {
120
120
  mapWorkflow(wfErrors, "errors", (v) => Math.round(+v));
121
121
  mapWorkflow(wfReqs, "requests", (v) => Math.round(+v));
122
122
  const nodeMetricsRaw = await Promise.all([
123
- queryPrometheus("(sum(increase(node_total[1m])) by (node_name, workflow_path)) > 0", host, token),
124
- queryPrometheus("sum(increase(node_time[1m])) by (node_name, workflow_path)", host, token),
125
- queryPrometheus("(sum(increase(node_errors_total[1m])) by (node_name, workflow_path)) > 0", host, token),
126
- queryPrometheus("sum(increase(node_cpu[1m])) by (node_name, workflow_path)", host, token),
127
- queryPrometheus("sum(increase(node_memory[1m])) by (node_name, workflow_path)", host, token),
123
+ queryPrometheus("(sum(increase(blok_node_executions_total[1m])) by (node_name, workflow_path)) > 0", host, token),
124
+ queryPrometheus("sum(increase(blok_node_duration_seconds_sum[1m])) by (node_name, workflow_path)", host, token),
125
+ queryPrometheus("(sum(increase(blok_node_errors_total[1m])) by (node_name, workflow_path)) > 0", host, token),
126
+ queryPrometheus("sum(increase(blok_node_cpu_usage[1m])) by (node_name, workflow_path)", host, token),
127
+ queryPrometheus("sum(increase(blok_node_memory_bytes[1m])) by (node_name, workflow_path)", host, token),
128
128
  ]);
129
129
  const nodeMap = {};
130
130
  const setNodeMetric = (list, key, convert) => {
@@ -0,0 +1,2 @@
1
+ import type { OptionValues } from "../../services/commander.js";
2
+ export declare function observabilityAdd(moduleArg: string | undefined, options: OptionValues): Promise<void>;
@@ -0,0 +1,113 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import * as p from "@clack/prompts";
4
+ import color from "picocolors";
5
+ import { isNonInteractive } from "../../services/non-interactive.js";
6
+ import { rewriteObservabilityEnvBlock, withObservabilityModule } from "../../services/observability-mutations.js";
7
+ import { OBSERVABILITY_MODULE_IDS, allObservabilityModules, getObservabilityModule, resolveWithDependencies, } from "./descriptor.js";
8
+ import { ObservabilityCommandError, readConfigSafe, readFrameworkVersion, reportObservabilityError, resolveProjectRoot, } from "./shared.js";
9
+ export async function observabilityAdd(moduleArg, options) {
10
+ try {
11
+ const root = resolveProjectRoot(options.directory);
12
+ const config = readConfigSafe(root);
13
+ const enabled = config.observability ?? {};
14
+ const nonInteractive = isNonInteractive() || options.yes === true;
15
+ let id = moduleArg?.trim().toLowerCase();
16
+ if (!id) {
17
+ if (nonInteractive) {
18
+ throw new ObservabilityCommandError(`Specify a module: blokctl observability add <${OBSERVABILITY_MODULE_IDS.join("|")}>`);
19
+ }
20
+ const choices = allObservabilityModules()
21
+ .filter((m) => !enabled[m.id])
22
+ .map((m) => ({ value: m.id, label: m.label, hint: m.description }));
23
+ if (choices.length === 0) {
24
+ p.intro(color.inverse(" Add observability module "));
25
+ p.outro(color.dim("All observability modules are already enabled."));
26
+ return;
27
+ }
28
+ const picked = await p.select({ message: "Which observability module do you want to add?", options: choices });
29
+ if (p.isCancel(picked)) {
30
+ p.cancel("Cancelled.");
31
+ return;
32
+ }
33
+ id = picked;
34
+ }
35
+ const mod = getObservabilityModule(id);
36
+ if (!mod) {
37
+ throw new ObservabilityCommandError(`Unknown observability module "${id}". Known: ${OBSERVABILITY_MODULE_IDS.join(", ")}.`);
38
+ }
39
+ p.intro(color.inverse(` Add ${mod.label} `));
40
+ if (enabled[mod.id] && options.force !== true) {
41
+ p.outro(color.dim(`${mod.label} is already enabled. Re-run with --force to re-apply its scaffold.`));
42
+ return;
43
+ }
44
+ const { resolved, added } = resolveWithDependencies([mod.id]);
45
+ const newDeps = added.filter((d) => !enabled[d]);
46
+ if (newDeps.length > 0) {
47
+ const labels = newDeps.map((d) => getObservabilityModule(d)?.label ?? d).join(", ");
48
+ if (nonInteractive) {
49
+ p.log.info(color.dim(`Also enabling required dependencies: ${labels}`));
50
+ }
51
+ else {
52
+ const ok = await p.confirm({
53
+ message: `${mod.label} requires ${labels}. Enable ${newDeps.length > 1 ? "them" : "it"} too?`,
54
+ initialValue: true,
55
+ });
56
+ if (p.isCancel(ok) || !ok) {
57
+ p.outro(color.dim("Left unchanged."));
58
+ return;
59
+ }
60
+ }
61
+ }
62
+ const toApply = resolved.filter((rid) => rid === mod.id || !enabled[rid]);
63
+ const addedAt = new Date().toISOString();
64
+ const version = readFrameworkVersion(root);
65
+ const s = p.spinner();
66
+ let nextConfig = config;
67
+ const scaffoldOpts = {
68
+ projectDir: root,
69
+ nonInteractive,
70
+ tier: options.tier,
71
+ localRepo: options.local,
72
+ };
73
+ for (const rid of toApply) {
74
+ const d = getObservabilityModule(rid);
75
+ if (!d)
76
+ continue;
77
+ if (d.validate)
78
+ await d.validate(root);
79
+ if (d.scaffold) {
80
+ s.start(`Scaffolding ${d.label}…`);
81
+ await d.scaffold(scaffoldOpts);
82
+ s.stop(`${d.label} ready`);
83
+ }
84
+ if (d.setup)
85
+ await d.setup(scaffoldOpts);
86
+ const extra = rid === "obs-stack" ? { settings: { tier: options.tier ?? "lite" } } : {};
87
+ nextConfig = withObservabilityModule(nextConfig, rid, { enabled: true, addedAt, version, ...extra });
88
+ }
89
+ fs.mkdirSync(path.join(root, ".blok"), { recursive: true });
90
+ fs.writeFileSync(path.join(root, ".blok", "config.json"), `${JSON.stringify(nextConfig, null, 2)}\n`);
91
+ const enabledIds = Object.keys(nextConfig.observability ?? {});
92
+ const envBlocks = enabledIds.map((eid) => getObservabilityModule(eid)?.envBlock({ projectDir: root }) ?? "");
93
+ const envPath = path.join(root, ".env.local");
94
+ const envContent = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf8") : "";
95
+ fs.writeFileSync(envPath, rewriteObservabilityEnvBlock(envContent, envBlocks));
96
+ const deps = {};
97
+ for (const rid of toApply)
98
+ Object.assign(deps, getObservabilityModule(rid)?.packageDeps ?? {});
99
+ if (Object.keys(deps).length > 0)
100
+ mergePackageDeps(root, deps);
101
+ p.note(toApply.map((rid) => `${color.green("✓")} ${getObservabilityModule(rid)?.label ?? rid}`).join("\n"), `${mod.label} added`);
102
+ p.outro(color.dim(`Enabled modules: ${enabledIds.join(", ")}.`));
103
+ }
104
+ catch (err) {
105
+ reportObservabilityError(err);
106
+ }
107
+ }
108
+ function mergePackageDeps(root, deps) {
109
+ const pkgPath = path.join(root, "package.json");
110
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
111
+ pkg.dependencies = { ...(pkg.dependencies ?? {}), ...deps };
112
+ fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
113
+ }