@q1k-oss/btree-workflows 0.0.1

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 (203) hide show
  1. package/.claude/settings.local.json +31 -0
  2. package/CLAUDE.md +181 -0
  3. package/LICENSE +21 -0
  4. package/README.md +920 -0
  5. package/behaviour-tree-workflows-landing/index.html +16 -0
  6. package/behaviour-tree-workflows-landing/package-lock.json +2074 -0
  7. package/behaviour-tree-workflows-landing/package.json +31 -0
  8. package/behaviour-tree-workflows-landing/public/favicon.svg +17 -0
  9. package/behaviour-tree-workflows-landing/src/App.css +103 -0
  10. package/behaviour-tree-workflows-landing/src/App.tsx +176 -0
  11. package/behaviour-tree-workflows-landing/src/components/BlackboardInspector.css +89 -0
  12. package/behaviour-tree-workflows-landing/src/components/BlackboardInspector.tsx +64 -0
  13. package/behaviour-tree-workflows-landing/src/components/ExampleSelector.css +64 -0
  14. package/behaviour-tree-workflows-landing/src/components/ExampleSelector.tsx +34 -0
  15. package/behaviour-tree-workflows-landing/src/components/ExecutionLog.css +107 -0
  16. package/behaviour-tree-workflows-landing/src/components/ExecutionLog.tsx +85 -0
  17. package/behaviour-tree-workflows-landing/src/components/Header.css +50 -0
  18. package/behaviour-tree-workflows-landing/src/components/Header.tsx +26 -0
  19. package/behaviour-tree-workflows-landing/src/components/StatusBadge.css +45 -0
  20. package/behaviour-tree-workflows-landing/src/components/StatusBadge.tsx +15 -0
  21. package/behaviour-tree-workflows-landing/src/components/Toolbar.css +74 -0
  22. package/behaviour-tree-workflows-landing/src/components/Toolbar.tsx +53 -0
  23. package/behaviour-tree-workflows-landing/src/components/TreeVisualizer.css +67 -0
  24. package/behaviour-tree-workflows-landing/src/components/TreeVisualizer.tsx +192 -0
  25. package/behaviour-tree-workflows-landing/src/components/YamlEditor.css +18 -0
  26. package/behaviour-tree-workflows-landing/src/components/YamlEditor.tsx +96 -0
  27. package/behaviour-tree-workflows-landing/src/lib/count-nodes.ts +11 -0
  28. package/behaviour-tree-workflows-landing/src/lib/execution-engine.ts +96 -0
  29. package/behaviour-tree-workflows-landing/src/lib/tree-layout.ts +136 -0
  30. package/behaviour-tree-workflows-landing/src/lib/yaml-examples.ts +549 -0
  31. package/behaviour-tree-workflows-landing/src/main.tsx +9 -0
  32. package/behaviour-tree-workflows-landing/src/stubs/activepieces.ts +18 -0
  33. package/behaviour-tree-workflows-landing/src/stubs/fs.ts +24 -0
  34. package/behaviour-tree-workflows-landing/src/stubs/path.ts +16 -0
  35. package/behaviour-tree-workflows-landing/src/stubs/temporal-activity.ts +6 -0
  36. package/behaviour-tree-workflows-landing/src/stubs/temporal-workflow.ts +22 -0
  37. package/behaviour-tree-workflows-landing/tsconfig.json +25 -0
  38. package/behaviour-tree-workflows-landing/vite.config.ts +40 -0
  39. package/demo-google-sheets.ts +181 -0
  40. package/demo-runtime-variables.ts +174 -0
  41. package/demo-template.ts +208 -0
  42. package/docs/ARCHITECTURE_SUMMARY.md +613 -0
  43. package/docs/NODE_REFERENCE.md +504 -0
  44. package/docs/README.md +53 -0
  45. package/docs/custom-nodes-architecture.md +826 -0
  46. package/docs/observability.md +175 -0
  47. package/docs/yaml-specification.md +990 -0
  48. package/examples/temporal/README.md +117 -0
  49. package/examples/temporal/activities.ts +373 -0
  50. package/examples/temporal/client.ts +115 -0
  51. package/examples/temporal/python-worker/activities.py +339 -0
  52. package/examples/temporal/python-worker/requirements.txt +12 -0
  53. package/examples/temporal/python-worker/worker.py +106 -0
  54. package/examples/temporal/worker.ts +66 -0
  55. package/examples/temporal/workflows.ts +6 -0
  56. package/examples/temporal/yaml-workflow-loader.ts +105 -0
  57. package/examples/yaml-test.ts +97 -0
  58. package/examples/yaml-workflows/01-simple-sequence.yaml +25 -0
  59. package/examples/yaml-workflows/02-parallel-timeout.yaml +45 -0
  60. package/examples/yaml-workflows/03-ecommerce-checkout.yaml +94 -0
  61. package/examples/yaml-workflows/04-ai-agent-workflow.yaml +346 -0
  62. package/examples/yaml-workflows/05-order-processing.yaml +146 -0
  63. package/examples/yaml-workflows/06-activity-test.yaml +71 -0
  64. package/examples/yaml-workflows/07-activity-simple-test.yaml +43 -0
  65. package/examples/yaml-workflows/08-file-processing.yaml +141 -0
  66. package/examples/yaml-workflows/09-http-request.yaml +137 -0
  67. package/examples/yaml-workflows/README.md +211 -0
  68. package/package.json +38 -0
  69. package/src/actions/code-execution.schema.ts +27 -0
  70. package/src/actions/code-execution.ts +218 -0
  71. package/src/actions/generate-file.test.ts +516 -0
  72. package/src/actions/generate-file.ts +166 -0
  73. package/src/actions/http-request.test.ts +784 -0
  74. package/src/actions/http-request.ts +228 -0
  75. package/src/actions/index.ts +20 -0
  76. package/src/actions/parse-file.test.ts +448 -0
  77. package/src/actions/parse-file.ts +139 -0
  78. package/src/actions/python-script.test.ts +439 -0
  79. package/src/actions/python-script.ts +154 -0
  80. package/src/base-node.test.ts +511 -0
  81. package/src/base-node.ts +605 -0
  82. package/src/behavior-tree.test.ts +431 -0
  83. package/src/behavior-tree.ts +283 -0
  84. package/src/blackboard.test.ts +222 -0
  85. package/src/blackboard.ts +192 -0
  86. package/src/composites/conditional.schema.ts +19 -0
  87. package/src/composites/conditional.test.ts +309 -0
  88. package/src/composites/conditional.ts +129 -0
  89. package/src/composites/for-each.schema.ts +23 -0
  90. package/src/composites/for-each.test.ts +254 -0
  91. package/src/composites/for-each.ts +132 -0
  92. package/src/composites/index.ts +15 -0
  93. package/src/composites/memory-sequence.schema.ts +19 -0
  94. package/src/composites/memory-sequence.test.ts +223 -0
  95. package/src/composites/memory-sequence.ts +98 -0
  96. package/src/composites/parallel.schema.ts +28 -0
  97. package/src/composites/parallel.test.ts +502 -0
  98. package/src/composites/parallel.ts +157 -0
  99. package/src/composites/reactive-sequence.schema.ts +19 -0
  100. package/src/composites/reactive-sequence.test.ts +170 -0
  101. package/src/composites/reactive-sequence.ts +85 -0
  102. package/src/composites/recovery.schema.ts +19 -0
  103. package/src/composites/recovery.test.ts +366 -0
  104. package/src/composites/recovery.ts +90 -0
  105. package/src/composites/selector.schema.ts +19 -0
  106. package/src/composites/selector.test.ts +387 -0
  107. package/src/composites/selector.ts +85 -0
  108. package/src/composites/sequence.schema.ts +19 -0
  109. package/src/composites/sequence.test.ts +337 -0
  110. package/src/composites/sequence.ts +72 -0
  111. package/src/composites/sub-tree.schema.ts +21 -0
  112. package/src/composites/sub-tree.test.ts +893 -0
  113. package/src/composites/sub-tree.ts +177 -0
  114. package/src/composites/while.schema.ts +24 -0
  115. package/src/composites/while.test.ts +381 -0
  116. package/src/composites/while.ts +149 -0
  117. package/src/data-store/index.ts +10 -0
  118. package/src/data-store/memory-store.ts +161 -0
  119. package/src/data-store/types.ts +94 -0
  120. package/src/debug/breakpoint.test.ts +47 -0
  121. package/src/debug/breakpoint.ts +30 -0
  122. package/src/debug/index.ts +17 -0
  123. package/src/debug/resume-point.test.ts +49 -0
  124. package/src/debug/resume-point.ts +29 -0
  125. package/src/decorators/delay.schema.ts +21 -0
  126. package/src/decorators/delay.test.ts +261 -0
  127. package/src/decorators/delay.ts +140 -0
  128. package/src/decorators/force-result.schema.ts +32 -0
  129. package/src/decorators/force-result.test.ts +133 -0
  130. package/src/decorators/force-result.ts +63 -0
  131. package/src/decorators/index.ts +13 -0
  132. package/src/decorators/invert.schema.ts +19 -0
  133. package/src/decorators/invert.test.ts +135 -0
  134. package/src/decorators/invert.ts +42 -0
  135. package/src/decorators/keep-running.schema.ts +20 -0
  136. package/src/decorators/keep-running.test.ts +105 -0
  137. package/src/decorators/keep-running.ts +49 -0
  138. package/src/decorators/precondition.schema.ts +19 -0
  139. package/src/decorators/precondition.test.ts +351 -0
  140. package/src/decorators/precondition.ts +139 -0
  141. package/src/decorators/repeat.schema.ts +21 -0
  142. package/src/decorators/repeat.test.ts +187 -0
  143. package/src/decorators/repeat.ts +94 -0
  144. package/src/decorators/run-once.schema.ts +19 -0
  145. package/src/decorators/run-once.test.ts +140 -0
  146. package/src/decorators/run-once.ts +61 -0
  147. package/src/decorators/soft-assert.schema.ts +19 -0
  148. package/src/decorators/soft-assert.test.ts +107 -0
  149. package/src/decorators/soft-assert.ts +68 -0
  150. package/src/decorators/timeout.schema.ts +21 -0
  151. package/src/decorators/timeout.test.ts +274 -0
  152. package/src/decorators/timeout.ts +159 -0
  153. package/src/errors.test.ts +63 -0
  154. package/src/errors.ts +34 -0
  155. package/src/events.test.ts +347 -0
  156. package/src/events.ts +183 -0
  157. package/src/index.ts +80 -0
  158. package/src/integrations/index.ts +30 -0
  159. package/src/integrations/integration-action.test.ts +571 -0
  160. package/src/integrations/integration-action.ts +233 -0
  161. package/src/integrations/piece-executor.ts +320 -0
  162. package/src/observability/execution-tracker.ts +320 -0
  163. package/src/observability/index.ts +23 -0
  164. package/src/observability/sinks.ts +138 -0
  165. package/src/observability/types.ts +130 -0
  166. package/src/registry-utils.ts +147 -0
  167. package/src/registry.test.ts +466 -0
  168. package/src/registry.ts +334 -0
  169. package/src/schemas/base.schema.ts +104 -0
  170. package/src/schemas/index.ts +223 -0
  171. package/src/schemas/integration.test.ts +238 -0
  172. package/src/schemas/tree-definition.schema.ts +170 -0
  173. package/src/schemas/validation.test.ts +146 -0
  174. package/src/schemas/validation.ts +122 -0
  175. package/src/scripting/index.ts +22 -0
  176. package/src/templates/template-loader.test.ts +281 -0
  177. package/src/templates/template-loader.ts +152 -0
  178. package/src/temporal-integration.test.ts +213 -0
  179. package/src/test-nodes.ts +259 -0
  180. package/src/types.ts +503 -0
  181. package/src/utilities/index.ts +17 -0
  182. package/src/utilities/log-message.test.ts +275 -0
  183. package/src/utilities/log-message.ts +134 -0
  184. package/src/utilities/regex-extract.test.ts +138 -0
  185. package/src/utilities/regex-extract.ts +108 -0
  186. package/src/utilities/variable-resolver.test.ts +416 -0
  187. package/src/utilities/variable-resolver.ts +318 -0
  188. package/src/utils/error-handler.test.ts +117 -0
  189. package/src/utils/error-handler.ts +48 -0
  190. package/src/utils/signal-check.test.ts +234 -0
  191. package/src/utils/signal-check.ts +140 -0
  192. package/src/yaml/errors.ts +143 -0
  193. package/src/yaml/index.ts +30 -0
  194. package/src/yaml/loader.ts +39 -0
  195. package/src/yaml/parser.ts +286 -0
  196. package/src/yaml/validation/semantic-validator.ts +196 -0
  197. package/templates/google-sheets/insert-row.yaml +76 -0
  198. package/templates/notification-sender.yaml +33 -0
  199. package/templates/order-validation.yaml +44 -0
  200. package/tsconfig.json +24 -0
  201. package/vitest.config.ts +25 -0
  202. package/workflows/order-processor.yaml +59 -0
  203. package/workflows/process-order-workflow.yaml +142 -0
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Tests for LogMessage Node
3
+ */
4
+
5
+ import {
6
+ afterEach,
7
+ beforeEach,
8
+ describe,
9
+ expect,
10
+ it,
11
+ vi,
12
+ } from "vitest";
13
+ import { NodeEventEmitter, NodeEventType } from "../events.js";
14
+ import {
15
+ type TemporalContext,
16
+ NodeStatus,
17
+ ScopedBlackboard,
18
+ } from "../index.js";
19
+ import { LogMessage } from "./log-message.js";
20
+
21
+ describe("LogMessage", () => {
22
+ let blackboard: ScopedBlackboard;
23
+ let context: TemporalContext;
24
+ let consoleLogSpy: unknown;
25
+ let consoleWarnSpy: unknown;
26
+ let consoleErrorSpy: unknown;
27
+ let consoleDebugSpy: unknown;
28
+
29
+ afterEach(() => {
30
+ vi.restoreAllMocks();
31
+ });
32
+
33
+ beforeEach(() => {
34
+ blackboard = new ScopedBlackboard();
35
+ context = {
36
+ blackboard: blackboard,
37
+ timestamp: Date.now(),
38
+ deltaTime: 0,
39
+ };
40
+
41
+ // Spy on console methods
42
+ consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {});
43
+ consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
44
+ consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
45
+ consoleDebugSpy = vi.spyOn(console, "debug").mockImplementation(() => {});
46
+ });
47
+
48
+ it("should log a simple message", async () => {
49
+ const node = new LogMessage({
50
+ id: "log-1",
51
+ message: "Test message",
52
+ });
53
+
54
+ const result = await node.tick(context);
55
+
56
+ expect(result).toBe(NodeStatus.SUCCESS);
57
+ expect(consoleLogSpy).toHaveBeenCalledWith(
58
+ expect.stringContaining("[LogMessage:log-1] Test message"),
59
+ );
60
+ });
61
+
62
+ it("should log message with blackboard value placeholder", async () => {
63
+ blackboard.set("username", "testuser");
64
+ blackboard.set("count", 42);
65
+
66
+ const node = new LogMessage({
67
+ id: "log-2",
68
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: This is intentional - LogMessage processes ${} syntax
69
+ message: "User: ${username}, Count: ${count}",
70
+ });
71
+
72
+ const result = await node.tick(context);
73
+
74
+ expect(result).toBe(NodeStatus.SUCCESS);
75
+ expect(consoleLogSpy).toHaveBeenCalledWith(
76
+ expect.stringContaining("[LogMessage:log-2] User: testuser, Count: 42"),
77
+ );
78
+ });
79
+
80
+ it("should handle missing blackboard values", async () => {
81
+ const node = new LogMessage({
82
+ id: "log-3",
83
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: This is intentional - LogMessage processes ${} syntax
84
+ message: "Value: ${missingKey}",
85
+ });
86
+
87
+ const result = await node.tick(context);
88
+
89
+ expect(result).toBe(NodeStatus.SUCCESS);
90
+ // Should keep the placeholder if value is missing
91
+ expect(consoleLogSpy).toHaveBeenCalledWith(
92
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: This is intentional - testing placeholder syntax
93
+ expect.stringContaining("[LogMessage:log-3] Value: ${missingKey}"),
94
+ );
95
+ });
96
+
97
+ it("should handle null values", async () => {
98
+ blackboard.set("nullValue", null);
99
+
100
+ const node = new LogMessage({
101
+ id: "log-4",
102
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: This is intentional - LogMessage processes ${} syntax
103
+ message: "Null value: ${nullValue}",
104
+ });
105
+
106
+ const result = await node.tick(context);
107
+
108
+ expect(result).toBe(NodeStatus.SUCCESS);
109
+ expect(consoleLogSpy).toHaveBeenCalledWith(
110
+ expect.stringContaining("[LogMessage:log-4] Null value: null"),
111
+ );
112
+ });
113
+
114
+ it("should handle object values", async () => {
115
+ blackboard.set("user", { name: "John", age: 30 });
116
+
117
+ const node = new LogMessage({
118
+ id: "log-5",
119
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: This is intentional - LogMessage processes ${} syntax
120
+ message: "User: ${user}",
121
+ });
122
+
123
+ const result = await node.tick(context);
124
+
125
+ expect(result).toBe(NodeStatus.SUCCESS);
126
+ expect(consoleLogSpy).toHaveBeenCalledWith(
127
+ expect.stringContaining(
128
+ '[LogMessage:log-5] User: {"name":"John","age":30}',
129
+ ),
130
+ );
131
+ });
132
+
133
+ it("should log with warn level", async () => {
134
+ const node = new LogMessage({
135
+ id: "log-6",
136
+ message: "Warning message",
137
+ level: "warn",
138
+ });
139
+
140
+ const result = await node.tick(context);
141
+
142
+ expect(result).toBe(NodeStatus.SUCCESS);
143
+ expect(consoleWarnSpy).toHaveBeenCalled();
144
+ expect(consoleLogSpy).not.toHaveBeenCalled();
145
+ });
146
+
147
+ it("should log with error level", async () => {
148
+ const node = new LogMessage({
149
+ id: "log-7",
150
+ message: "Error message",
151
+ level: "error",
152
+ });
153
+
154
+ const result = await node.tick(context);
155
+
156
+ expect(result).toBe(NodeStatus.SUCCESS);
157
+ expect(consoleErrorSpy).toHaveBeenCalled();
158
+ expect(consoleLogSpy).not.toHaveBeenCalled();
159
+ });
160
+
161
+ it("should log with debug level", async () => {
162
+ const node = new LogMessage({
163
+ id: "log-8",
164
+ message: "Debug message",
165
+ level: "debug",
166
+ });
167
+
168
+ const result = await node.tick(context);
169
+
170
+ expect(result).toBe(NodeStatus.SUCCESS);
171
+ expect(consoleDebugSpy).toHaveBeenCalled();
172
+ expect(consoleLogSpy).not.toHaveBeenCalled();
173
+ });
174
+
175
+ it("should default to info level", async () => {
176
+ const node = new LogMessage({
177
+ id: "log-9",
178
+ message: "Info message",
179
+ // level not specified
180
+ });
181
+
182
+ const result = await node.tick(context);
183
+
184
+ expect(result).toBe(NodeStatus.SUCCESS);
185
+ expect(consoleLogSpy).toHaveBeenCalled();
186
+ });
187
+
188
+ it("should handle multiple placeholders", async () => {
189
+ blackboard.set("name", "Alice");
190
+ blackboard.set("age", 25);
191
+ blackboard.set("city", "New York");
192
+
193
+ const node = new LogMessage({
194
+ id: "log-10",
195
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: This is intentional - LogMessage processes ${} syntax
196
+ message: "${name} is ${age} years old and lives in ${city}",
197
+ });
198
+
199
+ const result = await node.tick(context);
200
+
201
+ expect(result).toBe(NodeStatus.SUCCESS);
202
+ expect(consoleLogSpy).toHaveBeenCalledWith(
203
+ expect.stringContaining(
204
+ "[LogMessage:log-10] Alice is 25 years old and lives in New York",
205
+ ),
206
+ );
207
+ });
208
+
209
+ it("should handle array values", async () => {
210
+ blackboard.set("items", ["apple", "banana", "cherry"]);
211
+
212
+ const node = new LogMessage({
213
+ id: "log-11",
214
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: This is intentional - LogMessage processes ${} syntax
215
+ message: "Items: ${items}",
216
+ });
217
+
218
+ const result = await node.tick(context);
219
+
220
+ expect(result).toBe(NodeStatus.SUCCESS);
221
+ expect(consoleLogSpy).toHaveBeenCalledWith(
222
+ expect.stringContaining(
223
+ '[LogMessage:log-11] Items: ["apple","banana","cherry"]',
224
+ ),
225
+ );
226
+ });
227
+
228
+ describe("LOG event emission", () => {
229
+ it("should emit LOG event when executed with event emitter", async () => {
230
+ const eventEmitter = new NodeEventEmitter();
231
+ const receivedEvents: unknown[] = [];
232
+
233
+ eventEmitter.on(NodeEventType.LOG, (event) => {
234
+ receivedEvents.push(event);
235
+ });
236
+
237
+ const contextWithEmitter: TemporalContext = {
238
+ ...context,
239
+ eventEmitter,
240
+ };
241
+
242
+ const node = new LogMessage({
243
+ id: "log-event",
244
+ name: "EventTest",
245
+ message: "Event test message",
246
+ level: "warn",
247
+ });
248
+
249
+ await node.tick(contextWithEmitter);
250
+
251
+ expect(receivedEvents).toHaveLength(1);
252
+ const event = receivedEvents[0] as {
253
+ type: string;
254
+ nodeId: string;
255
+ data: { level: string; message: string };
256
+ };
257
+ expect(event.type).toBe(NodeEventType.LOG);
258
+ expect(event.nodeId).toBe("log-event");
259
+ expect(event.data.level).toBe("warn");
260
+ expect(event.data.message).toBe("Event test message");
261
+ });
262
+
263
+ it("should not fail when no event emitter present", async () => {
264
+ const node = new LogMessage({
265
+ id: "log-no-emitter",
266
+ message: "No emitter test",
267
+ });
268
+
269
+ // Context without eventEmitter
270
+ const result = await node.tick(context);
271
+
272
+ expect(result).toBe(NodeStatus.SUCCESS);
273
+ });
274
+ });
275
+ });
@@ -0,0 +1,134 @@
1
+ /**
2
+ * LogMessage Node - Log messages for debugging and test visibility
3
+ *
4
+ * Utility node that logs messages to console with optional variable resolution.
5
+ * Useful for debugging test flows and tracking execution progress.
6
+ *
7
+ * Supports variable resolution:
8
+ * - ${key} - Shorthand for blackboard (backward compatible)
9
+ * - ${input.key} - Workflow input parameters
10
+ * - ${bb.key} - Blackboard values
11
+ * - ${env.KEY} - Environment variables
12
+ * - ${param.key} - Test data parameters
13
+ *
14
+ * Use Cases:
15
+ * - Log intermediate values during test execution
16
+ * - Debug blackboard state at specific points
17
+ * - Add visibility to test steps
18
+ * - Track execution flow
19
+ *
20
+ * Examples:
21
+ * - Log static message: <LogMessage message="Starting form submission" />
22
+ * - Log blackboard value: <LogMessage message="Current URL: ${currentUrl}" />
23
+ * - Log input value: <LogMessage message="Order ID: ${input.orderId}" />
24
+ * - Log with level: <LogMessage message="Error occurred" level="error" />
25
+ */
26
+
27
+ import { ActionNode } from "../base-node.js";
28
+ import { NodeEventType } from "../events.js";
29
+ import {
30
+ type TemporalContext,
31
+ type NodeConfiguration,
32
+ NodeStatus,
33
+ } from "../types.js";
34
+ import { resolveString, type VariableContext } from "./variable-resolver.js";
35
+
36
+ /**
37
+ * Configuration for LogMessage node
38
+ */
39
+ export interface LogMessageConfig extends NodeConfiguration {
40
+ message: string; // Message to log (supports ${key}, ${input.key}, ${bb.key}, ${env.KEY})
41
+ level?: "info" | "warn" | "error" | "debug"; // Log level (default: 'info')
42
+ }
43
+
44
+ /**
45
+ * LogMessage Node - Logs messages with optional variable resolution
46
+ */
47
+ export class LogMessage extends ActionNode {
48
+ private message: string;
49
+ private level: "info" | "warn" | "error" | "debug";
50
+
51
+ constructor(config: LogMessageConfig) {
52
+ super(config);
53
+ this.message = config.message;
54
+ this.level = config.level || "info";
55
+ }
56
+
57
+ async executeTick(context: TemporalContext): Promise<NodeStatus> {
58
+ try {
59
+ // Resolve variable references in message
60
+ const resolvedMessage = this.resolveMessage(this.message, context);
61
+
62
+ // Log based on level
63
+ switch (this.level) {
64
+ case "warn":
65
+ console.warn(`[LogMessage:${this.name}] ${resolvedMessage}`);
66
+ break;
67
+ case "error":
68
+ console.error(`[LogMessage:${this.name}] ${resolvedMessage}`);
69
+ break;
70
+ case "debug":
71
+ console.debug(`[LogMessage:${this.name}] ${resolvedMessage}`);
72
+ break;
73
+ default:
74
+ console.log(`[LogMessage:${this.name}] ${resolvedMessage}`);
75
+ break;
76
+ }
77
+
78
+ // Emit LOG event for collection
79
+ context.eventEmitter?.emit({
80
+ type: NodeEventType.LOG,
81
+ nodeId: this.id,
82
+ nodeName: this.name,
83
+ nodeType: this.type,
84
+ timestamp: Date.now(),
85
+ data: { level: this.level, message: resolvedMessage },
86
+ });
87
+
88
+ this._status = NodeStatus.SUCCESS;
89
+ return NodeStatus.SUCCESS;
90
+ } catch (error) {
91
+ const errorMessage =
92
+ error instanceof Error ? error.message : String(error);
93
+ console.error(
94
+ `[LogMessage:${this.name}] Failed to log message: ${errorMessage}`,
95
+ );
96
+ this._status = NodeStatus.FAILURE;
97
+ return NodeStatus.FAILURE;
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Resolve variable references in message string
103
+ * Supports ${key}, ${input.key}, ${bb.key}, ${env.KEY}, ${param.key}
104
+ */
105
+ private resolveMessage(message: string, context: TemporalContext): string {
106
+ const varCtx: VariableContext = {
107
+ blackboard: context.blackboard,
108
+ input: context.input,
109
+ testData: context.testData,
110
+ };
111
+
112
+ const resolved = resolveString(message, varCtx);
113
+
114
+ // Always return a string for logging
115
+ if (typeof resolved === "string") {
116
+ return resolved;
117
+ }
118
+
119
+ // If resolved to non-string (shouldn't happen for message templates), stringify
120
+ if (resolved === null) {
121
+ return "null";
122
+ }
123
+
124
+ if (typeof resolved === "object") {
125
+ try {
126
+ return JSON.stringify(resolved);
127
+ } catch {
128
+ return String(resolved);
129
+ }
130
+ }
131
+
132
+ return String(resolved);
133
+ }
134
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Tests for RegexExtract Node
3
+ */
4
+
5
+ import { beforeEach, describe, expect, it } from "vitest";
6
+ import {
7
+ type TemporalContext,
8
+ NodeStatus,
9
+ ScopedBlackboard,
10
+ } from "../index.js";
11
+ import { RegexExtract } from "./regex-extract.js";
12
+
13
+ describe("RegexExtract", () => {
14
+ let blackboard: ScopedBlackboard;
15
+ let context: TemporalContext;
16
+
17
+ beforeEach(() => {
18
+ blackboard = new ScopedBlackboard();
19
+ context = {
20
+ blackboard: blackboard,
21
+ timestamp: Date.now(),
22
+ deltaTime: 0,
23
+ };
24
+ });
25
+
26
+ it("should extract all matches when matchIndex is not specified", async () => {
27
+ blackboard.set("text", "Contact: support@example.com, sales@example.com");
28
+
29
+ const node = new RegexExtract({
30
+ id: "extract-emails",
31
+ input: "text",
32
+ pattern: "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}",
33
+ outputKey: "emails",
34
+ });
35
+
36
+ const result = await node.tick(context);
37
+
38
+ expect(result).toBe(NodeStatus.SUCCESS);
39
+ const emails = blackboard.get("emails");
40
+ expect(emails).toEqual(["support@example.com", "sales@example.com"]);
41
+ });
42
+
43
+ it("should extract specific match when matchIndex is specified", async () => {
44
+ blackboard.set("text", "Price: $99.99 and $149.99");
45
+
46
+ const node = new RegexExtract({
47
+ id: "extract-price",
48
+ input: "text",
49
+ pattern: "\\$\\d+\\.\\d{2}",
50
+ outputKey: "firstPrice",
51
+ matchIndex: 0,
52
+ });
53
+
54
+ const result = await node.tick(context);
55
+
56
+ expect(result).toBe(NodeStatus.SUCCESS);
57
+ const price = blackboard.get("firstPrice");
58
+ expect(price).toBe("$99.99");
59
+ });
60
+
61
+ it("should return null when matchIndex is out of bounds", async () => {
62
+ blackboard.set("text", "No numbers here");
63
+
64
+ const node = new RegexExtract({
65
+ id: "extract-number",
66
+ input: "text",
67
+ pattern: "\\d+",
68
+ outputKey: "number",
69
+ matchIndex: 0,
70
+ });
71
+
72
+ const result = await node.tick(context);
73
+
74
+ expect(result).toBe(NodeStatus.SUCCESS);
75
+ const number = blackboard.get("number");
76
+ expect(number).toBeNull();
77
+ });
78
+
79
+ it("should return empty array when no matches found", async () => {
80
+ blackboard.set("text", "No emails here");
81
+
82
+ const node = new RegexExtract({
83
+ id: "extract-emails",
84
+ input: "text",
85
+ pattern: "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}",
86
+ outputKey: "emails",
87
+ });
88
+
89
+ const result = await node.tick(context);
90
+
91
+ expect(result).toBe(NodeStatus.SUCCESS);
92
+ const emails = blackboard.get("emails");
93
+ expect(emails).toEqual([]);
94
+ });
95
+
96
+ it("should fail when input is not a string", async () => {
97
+ blackboard.set("text", 123);
98
+
99
+ const node = new RegexExtract({
100
+ id: "extract",
101
+ input: "text",
102
+ pattern: "\\d+",
103
+ outputKey: "result",
104
+ });
105
+
106
+ const result = await node.tick(context);
107
+
108
+ expect(result).toBe(NodeStatus.FAILURE);
109
+ });
110
+
111
+ it("should fail when input is not found in blackboard", async () => {
112
+ const node = new RegexExtract({
113
+ id: "extract",
114
+ input: "missing",
115
+ pattern: "\\d+",
116
+ outputKey: "result",
117
+ });
118
+
119
+ const result = await node.tick(context);
120
+
121
+ expect(result).toBe(NodeStatus.FAILURE);
122
+ });
123
+
124
+ it("should fail when regex pattern is invalid", async () => {
125
+ blackboard.set("text", "test");
126
+
127
+ const node = new RegexExtract({
128
+ id: "extract",
129
+ input: "text",
130
+ pattern: "[invalid",
131
+ outputKey: "result",
132
+ });
133
+
134
+ const result = await node.tick(context);
135
+
136
+ expect(result).toBe(NodeStatus.FAILURE);
137
+ });
138
+ });
@@ -0,0 +1,108 @@
1
+ /**
2
+ * RegexExtract Node - Extract text using regular expressions
3
+ *
4
+ * Data manipulation utility node that operates on blackboard values.
5
+ * Extracts matches from a text string using a regex pattern.
6
+ *
7
+ * Use Cases:
8
+ * - Extract email addresses from text
9
+ * - Extract numbers/IDs from strings
10
+ * - Parse structured data from unstructured text
11
+ * - Extract URLs from content
12
+ */
13
+
14
+ import { ActionNode } from "../base-node.js";
15
+ import { ConfigurationError } from "../errors.js";
16
+ import {
17
+ type TemporalContext,
18
+ type NodeConfiguration,
19
+ NodeStatus,
20
+ } from "../types.js";
21
+
22
+ /**
23
+ * Configuration for RegexExtract node
24
+ */
25
+ export interface RegexExtractConfig extends NodeConfiguration {
26
+ input: string; // Blackboard key containing text to extract from
27
+ pattern: string; // Regular expression pattern
28
+ outputKey: string; // Blackboard key to store extracted matches
29
+ flags?: string; // Regex flags (default: "g" for global)
30
+ matchIndex?: number; // Which match to return (0 = first, undefined = all matches array)
31
+ }
32
+
33
+ /**
34
+ * RegexExtract Node - Extracts text using regular expressions
35
+ */
36
+ export class RegexExtract extends ActionNode {
37
+ private input: string;
38
+ private pattern: string;
39
+ private outputKey: string;
40
+ private flags: string;
41
+ private matchIndex?: number;
42
+
43
+ constructor(config: RegexExtractConfig) {
44
+ super(config);
45
+ this.input = config.input;
46
+ this.pattern = config.pattern;
47
+ this.outputKey = config.outputKey;
48
+ this.flags = config.flags || "g";
49
+ this.matchIndex = config.matchIndex;
50
+ }
51
+
52
+ async executeTick(context: TemporalContext): Promise<NodeStatus> {
53
+ try {
54
+ // Get input text from blackboard
55
+ const text = context.blackboard.get(this.input);
56
+
57
+ // Validate input
58
+ if (text === undefined || text === null) {
59
+ throw new ConfigurationError(
60
+ `Input '${this.input}' not found in blackboard`,
61
+ );
62
+ }
63
+
64
+ if (typeof text !== "string") {
65
+ throw new ConfigurationError(
66
+ `Input '${this.input}' must be a string, got ${typeof text}`,
67
+ );
68
+ }
69
+
70
+ // Create regex from pattern and flags
71
+ let regex: RegExp;
72
+ try {
73
+ regex = new RegExp(this.pattern, this.flags);
74
+ } catch (error) {
75
+ const errorMessage =
76
+ error instanceof Error ? error.message : String(error);
77
+ throw new Error(`Invalid regex pattern: ${errorMessage}`);
78
+ }
79
+
80
+ // Extract matches
81
+ const matches = text.match(regex) || [];
82
+
83
+ // Determine output value
84
+ let result: string | string[] | null;
85
+ if (this.matchIndex !== undefined && this.matchIndex >= 0) {
86
+ // Return specific match
87
+ result = matches[this.matchIndex] || null;
88
+ this.log(`Extracted match at index ${this.matchIndex}: ${result}`);
89
+ } else {
90
+ // Return all matches as array
91
+ result = matches;
92
+ this.log(`Extracted ${matches.length} match(es) from input`);
93
+ }
94
+
95
+ // Store result in blackboard
96
+ this.setOutput(context, this.outputKey, result);
97
+
98
+ this._status = NodeStatus.SUCCESS;
99
+ return NodeStatus.SUCCESS;
100
+ } catch (error) {
101
+ const errorMessage =
102
+ error instanceof Error ? error.message : String(error);
103
+ this.log(`RegexExtract failed: ${errorMessage}`);
104
+ this._status = NodeStatus.FAILURE;
105
+ return NodeStatus.FAILURE;
106
+ }
107
+ }
108
+ }